From 5e7e18404029834dc514b5044e95540b3d2b43bb Mon Sep 17 00:00:00 2001 From: Bhagya-Dxdy Date: Wed, 21 Jan 2026 23:18:12 +0530 Subject: [PATCH 01/12] final assignment --- comprehensive_backend_test.py | 115 +++++ frontend/index.html | 492 +++++++++++++++++++++ requirements.txt | Bin 0 -> 4242 bytes src/app/api.py | 10 + src/app/core/agents/agents.py | 242 +++++++++- src/app/core/agents/graph.py | 10 +- src/app/core/agents/prompts.py | 49 ++ src/app/core/agents/state.py | 2 + src/app/core/agents/test_planning_agent.py | 56 +++ src/app/models.py | 3 + src/app/quick_test.py | 68 +++ src/app/test_complete_flow | 73 +++ 12 files changed, 1105 insertions(+), 15 deletions(-) create mode 100644 comprehensive_backend_test.py create mode 100644 frontend/index.html create mode 100644 requirements.txt create mode 100644 src/app/core/agents/test_planning_agent.py create mode 100644 src/app/quick_test.py create mode 100644 src/app/test_complete_flow diff --git a/comprehensive_backend_test.py b/comprehensive_backend_test.py new file mode 100644 index 0000000..0786602 --- /dev/null +++ b/comprehensive_backend_test.py @@ -0,0 +1,115 @@ +""" +Comprehensive backend test for Query Planning feature +""" + +import requests +import time + +BASE_URL = "http://localhost:8001" + +test_cases = [ + { + "name": "Simple Question", + "question": "What is HNSW indexing?", + "expected_sub_questions": 1, # Should have 1-2 sub-questions + }, + { + "name": "Complex Multi-Part Question", + "question": "What are the advantages of vector databases compared to traditional databases, and how do they handle scalability?", + "expected_sub_questions": 3, # Should break into 3+ parts + }, + { + "name": "Medium Complexity", + "question": "How do embeddings work in semantic search?", + "expected_sub_questions": 2, # Should have 2-3 sub-questions + } +] + +def test_qa_endpoint(): + """Test the QA endpoint with various questions""" + + print("="*70) + print("COMPREHENSIVE BACKEND TEST - QUERY PLANNING FEATURE") + print("="*70) + + passed = 0 + failed = 0 + + for i, test in enumerate(test_cases, 1): + print(f"\nπŸ“ Test {i}/{len(test_cases)}: {test['name']}") + print(f"Question: {test['question']}") + print("-"*70) + + try: + # Make request + response = requests.post( + f"{BASE_URL}/qa", + json={"question": test['question']}, + timeout=60 + ) + + if response.status_code == 200: + data = response.json() + + print("βœ“ Status: 200 OK") + print(f"βœ“ Answer received: {data.get('answer', 'N/A')[:100]}...") + print(f"βœ“ Context received: {len(data.get('context', ''))} characters") + + # Check if plan is in response (if API was updated) + if 'plan' in data: + print(f"βœ“ Plan: {data['plan'][:100]}...") + + if 'sub_questions' in data: + print(f"βœ“ Sub-questions ({len(data['sub_questions'])}): {data['sub_questions']}") + + # Validate number of sub-questions + if len(data['sub_questions']) >= test['expected_sub_questions']: + print(f"βœ“ Sub-question count matches expectation") + else: + print(f"⚠ Warning: Expected {test['expected_sub_questions']}+ sub-questions, got {len(data['sub_questions'])}") + + passed += 1 + print("βœ“ TEST PASSED") + + else: + print(f"βœ— Error: Status {response.status_code}") + print(f"Response: {response.text}") + failed += 1 + print("βœ— TEST FAILED") + + except requests.exceptions.Timeout: + print("βœ— Timeout - request took too long") + failed += 1 + print("βœ— TEST FAILED") + + except Exception as e: + print(f"βœ— Error: {e}") + failed += 1 + print("βœ— TEST FAILED") + + print("-"*70) + + if i < len(test_cases): + time.sleep(2) # Wait between tests + + # Summary + print("\n" + "="*70) + print("TEST SUMMARY") + print("="*70) + print(f"Passed: {passed}/{len(test_cases)}") + print(f"Failed: {failed}/{len(test_cases)}") + + if failed == 0: + print("\nβœ“ ALL TESTS PASSED!") + return True + else: + print(f"\nβœ— {failed} TEST(S) FAILED") + return False + +if __name__ == "__main__": + print("Make sure the FastAPI server is running on http://localhost:8001") + print("Starting tests in 3 seconds...") + time.sleep(3) + + success = test_qa_endpoint() + exit(0 if success else 1) \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..cadeb51 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,492 @@ + + + + + + IKMS - Query Planning & RAG System + + + +
+
+

🧠 IKMS Query Planning System

+

Intelligent Multi-Agent RAG with Query Decomposition

+
+ +
+
+ + +
+ +
+ + +
+ Enable Planning: + +
+
+ + + +
+
+
+
πŸ“
+
Question
+
+
β†’
+
+
🧠
+
Planning
+
+
β†’
+
+
πŸ“š
+
Retrieval
+
+
β†’
+
+
πŸ’‘
+
Answer
+
+
+ +
+
+ 🎯 + Search Strategy +
+
+
+ +
+
+ ❓ + Sub-Questions Generated +
+
    +
    + +
    +
    + πŸ’¬ + Final Answer +
    +
    +
    + +
    +
    +
    0
    +
    Sub-Questions
    +
    +
    +
    0
    +
    Context Characters
    +
    +
    +
    0s
    +
    Response Time
    +
    +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..f22ab17ea60412bd7dd790f45dfa8ac886c8f99a GIT binary patch literal 4242 zcmai&OK&1q5QXnLQho{}gCCPw%wnV{t3;Y8tE?;x*kBu)foA;p@k!2Cr;FQQJdxGd zL)ERvsZ&+A=ih(EWl@?kFXOV-&r6xbbEAJh>StR%>t|Fxlq)^M@^kq~5}#;Gs}so2 zL&oR%35`M7$ZD(4m%X)5N7lBoc~@596^ld3FZ7&s_EwTLvN6^(E7LO2)09crI}1JZ z9o<{qR-V^7=WNjbcwqY_GQuxDhmmi3-pXSuUk^IJ>MW2cRuT_1HhZitC8@DbbZ#U0 zqTK2+nrHHW)-dwKACXU@3fKXyL1ers%fMt&Uc*kJjeX}kQI!@t`w+H&(f6)~EjE%i z@;?qcoF;BgUv)kkNtSUI36*vYr-AiYmLK)kHlo@ps(by_D7Lf(FFuB?sUo~7JJk^$ zA*YH&G%TO?8j~&_Fx`X}NZ{9KPL&OqZzN9}u*LP5m1lXG$P0+B~6R|~fhFI-u7 zh`Uy!h{bkp!#*yJc z)Og;XcZ9~FN(UYKc;__LbBz}20(vInA{Oethzh{ijpQ?Bhz6XeB96~GtvHv}1m&9h zq5L{5-iaw7)+(ZFS#>p%`PlYD4EK^!%ZwThP$kP2vO#oJO8p#JGHk8PTH!YjEi9jR zc~^}lSWsF;li*&gg1J>2hPrZaw9YI#0`9qE;UU(D&`c!9 zw>pFH?_Q!~^NgAE{uy`ZPF_IGos~Rry-)k9(Pw1t`BU1BGN-c4_Kb^D@1LNz;1>Mm zH>ZfoeQ183@&v-mKzgc)cp;n~d-DgJUAt3fsm!x5pd;2g2Oh4LCp!`I99p1@nFWjb5GJ8QWI)kg4NG zr%oBR1N}FbjyHBwo}w1m8HJ{M4mtCPeb5GWH^Pw`g5%o?!bj>v9?TgS>x?|{6Xm7q zNlmZ2+IUNHL&$A;SQR`mZ3l{Ht8**aMLStnb6d&#GHfG*S9Pl%^VO{D%Qpv`%BT{F z>rQVh<)2-}jx+v_)V9b!Wn<}qQEnUj7*#aD-|JX-ld1U*CJ>&_|pHN04 zu|BpqP=t>{$G2AiZk0DU!j9YmVB@=u zGh~lb#a0Yq=W`=@kbd2!*uC82i9TBs-}GU6$F4hXt^Bt5^EY^PXe!N>2iVjX;- zKw-UMNBCB_^LSiJ2iRU-<9b6 zp?trVU(0X%E^%jPHGVWv;k54?#I{!6w_ycl)m}rEcLDC!zGwGQCU)P$KFCTF9P^w# zKFfSz3ViQHTWi<%Q|F8O$ef^7u9xc#0)Lk;LgDv) zvfICT+X6b@gs|sQt?PL(#+5qO87N0{`9|lCXT5Qu6PYo5PWO?!H#|L%IWkY4dKcLW zKi(L4XQ;EW?uNh0pF-n{JiAMn!pR=6x#(EzNx*!|s`|Ew4%IgT*4oIL8D6{YGAiMG zm?`Ww_kPteE9iFDozAT{72le2UeY}eft}x{vb*~|_S&4-d_F A0{{R3 literal 0 HcmV?d00001 diff --git a/src/app/api.py b/src/app/api.py index 441698c..c0e35da 100644 --- a/src/app/api.py +++ b/src/app/api.py @@ -1,6 +1,7 @@ from pathlib import Path from fastapi import FastAPI, File, HTTPException, Request, UploadFile, status +from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from .models import QuestionRequest, QAResponse @@ -18,6 +19,13 @@ version="0.1.0", ) +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # In production, specify your frontend domain + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) @app.exception_handler(Exception) async def unhandled_exception_handler( @@ -66,6 +74,8 @@ async def qa_endpoint(payload: QuestionRequest) -> QAResponse: return QAResponse( answer=result.get("answer", ""), context=result.get("context", ""), + plan=result.get("plan"), + sub_questions=result.get("sub_questions") ) diff --git a/src/app/core/agents/agents.py b/src/app/core/agents/agents.py index e3beacc..3b7d7a2 100644 --- a/src/app/core/agents/agents.py +++ b/src/app/core/agents/agents.py @@ -8,12 +8,18 @@ from langchain.agents import create_agent from langchain_core.messages import AIMessage, HumanMessage, ToolMessage - +from langchain_openai import ChatOpenAI +from .state import QAState from ..llm.factory import create_chat_model + +#from ..llm.factory import create_chat_model +#from ..llm.factory import create_chat_model + from .prompts import ( RETRIEVAL_SYSTEM_PROMPT, SUMMARIZATION_SYSTEM_PROMPT, VERIFICATION_SYSTEM_PROMPT, + PLANNING_SYSTEM_PROMPT ) from .state import QAState from .tools import retrieval_tool @@ -29,7 +35,7 @@ def _extract_last_ai_content(messages: List[object]) -> str: # Define agents at module level for reuse retrieval_agent = create_agent( - model=create_chat_model(), + model=create_chat_model() , tools=[retrieval_tool], system_prompt=RETRIEVAL_SYSTEM_PROMPT, ) @@ -46,34 +52,244 @@ def _extract_last_ai_content(messages: List[object]) -> str: system_prompt=VERIFICATION_SYSTEM_PROMPT, ) +verification_agent = create_agent( + model=create_chat_model(), + tools=[], + system_prompt=PLANNING_SYSTEM_PROMPT, +) + +def create_planning_agent(): + """ + Creates the Query Planning Agent. + This agent analyzes questions and creates search strategies. + + Returns: + ChatOpenAI: Configured planning agent + """ + llm = ChatOpenAI( + model="gpt-3.5-turbo", # Fast and cost-effective for planning + temperature=0.0 # We want consistent, logical planning + ) + + return llm -def retrieval_node(state: QAState) -> QAState: - """Retrieval Agent node: gathers context from vector store. - This node: - - Sends the user's question to the Retrieval Agent. - - The agent uses the attached retrieval tool to fetch document chunks. - - Extracts the tool's content (CONTEXT string) from the ToolMessage. - - Stores the consolidated context string in `state["context"]`. +def planning_agent_node(state: dict) -> dict: """ + Node function that executes the planning agent. + + This is what gets called in the graph. + + Args: + state: Current QAState + + Returns: + dict: Updated state with plan and sub_questions + """ + # Get the user's question question = state["question"] + + # Create the planning agent + agent = create_planning_agent() + + # Create the message for the agent + messages = [ + {"role": "system", "content": PLANNING_SYSTEM_PROMPT}, + {"role": "user", "content": f"Question: {question}"} + ] + + # Invoke the agent + response = agent.invoke(messages) + + # Extract the response content + plan_response = response.content + + # Parse the response to extract plan and sub-questions + plan, sub_questions = parse_planning_response(plan_response) + + # Print for debugging + print("\n" + "="*60) + print("🧠 PLANNING AGENT OUTPUT") + print("="*60) + print(f"Original Question: {question}") + print(f"\nPlan:\n{plan}") + print(f"\nSub-questions: {sub_questions}") + print("="*60 + "\n") + + # Return updated state + return { + "plan": plan, + "sub_questions": sub_questions + } + - result = retrieval_agent.invoke({"messages": [HumanMessage(content=question)]}) +def parse_planning_response(response: str) -> tuple[str, list[str]]: + """ + Parse the planning agent's response to extract plan and sub-questions. + + Args: + response: Raw response from planning agent + + Returns: + tuple: (plan_text, list_of_sub_questions) + """ + plan = "" + sub_questions = [] + + lines = response.split('\n') + current_section = None + + for line in lines: + line = line.strip() + + # Detect sections + if 'PLAN:' in line.upper(): + current_section = 'plan' + # Get text after "PLAN:" + plan_text = line.split(':', 1)[-1].strip() + if plan_text: + plan = plan_text + continue + + if 'SUB-QUESTION' in line.upper() or 'SUB QUESTION' in line.upper(): + current_section = 'sub_questions' + continue + + # Collect content + if current_section == 'plan' and line: + if not line.startswith(('1.', '2.', '3.', '4.', '5.', '-')): + plan += " " + line + + elif current_section == 'sub_questions' and line: + # Extract sub-question (remove numbering and quotes) + if line[0].isdigit() or line.startswith('-'): + # Remove leading number/dash and quotes + cleaned = line.lstrip('0123456789.-) ').strip('"\'') + if cleaned: + sub_questions.append(cleaned) + + # Fallback: if parsing failed, use the whole response as plan + if not plan and not sub_questions: + plan = response + # Try to extract any quoted strings as sub-questions + import re + quoted = re.findall(r'"([^"]*)"', response) + sub_questions = quoted if quoted else [response] + + return plan.strip(), sub_questions + + +# def retrieval_node(state: QAState) -> QAState: +# """Retrieval Agent node: gathers context from vector store. + +# This node: +# - Sends the user's question to the Retrieval Agent. +# - The agent uses the attached retrieval tool to fetch document chunks. +# - Extracts the tool's content (CONTEXT string) from the ToolMessage. +# - Stores the consolidated context string in `state["context"]`. +# """ +# question = state["question"] + +# result = retrieval_agent.invoke({"messages": [HumanMessage(content=question)]}) + +# messages = result.get("messages", []) +# context = "" + +# # Prefer the last ToolMessage content (from retrieval_tool) +# for msg in reversed(messages): +# if isinstance(msg, ToolMessage): +# context = str(msg.content) +# break + +# return { +# "context": context, +# } + +def retrieval_node(state: QAState) -> dict: + """ + Enhanced Retrieval Agent node: gathers context from vector store using planning. + This node: + - Reads the user's question AND the planning output (plan, sub_questions) + - Sends an enhanced message to the Retrieval Agent that includes: + * Original question + * Search strategy from planning + * Decomposed sub-questions + - The agent uses the retrieval tool to fetch document chunks + - Extracts the tool's content (CONTEXT string) from ToolMessage + - Stores the consolidated context string in state["context"] + + The planning information helps the agent make more targeted, + comprehensive retrieval calls. + """ + # Get data from state + question = state["question"] + plan = state.get("plan", "") + sub_questions = state.get("sub_questions", []) + + # Debug logging + print("\n" + "="*70) + print("πŸ“š RETRIEVAL NODE - Enhanced with Planning") + print("="*70) + print(f"Original Question: {question}") + print(f"Has Plan: {bool(plan)}") + print(f"Sub-questions: {len(sub_questions) if sub_questions else 0}") + print("="*70) + + # Build enhanced retrieval message + # If we have planning information, use it. Otherwise, use just the question. + if plan and sub_questions: + # ENHANCED MODE: Include planning information + retrieval_message = f"""You are retrieving information to answer this question: {question} + +SEARCH STRATEGY: +{plan} + +FOCUS AREAS (Sub-questions to address): +""" + for i, sub_q in enumerate(sub_questions, 1): + retrieval_message += f"{i}. {sub_q}\n" + + retrieval_message += """ +Use the retrieval tool to search for relevant information. You may: +- Make multiple retrieval calls for different aspects +- Search for each sub-question if needed +- Gather comprehensive context covering all focus areas + +Focus on retrieving diverse, relevant chunks that address all aspects of the question.""" + + else: + # FALLBACK MODE: No planning available, use original question + retrieval_message = question + print("ℹ️ No planning information available - using direct question") + + print(f"\nπŸ“€ Sending to Retrieval Agent:") + print(f"{retrieval_message[:200]}..." if len(retrieval_message) > 200 else retrieval_message) + print() + + # Invoke the retrieval agent + result = retrieval_agent.invoke({"messages": [HumanMessage(content=retrieval_message)]}) + messages = result.get("messages", []) context = "" - + + # Extract context from ToolMessage(s) # Prefer the last ToolMessage content (from retrieval_tool) + tool_messages_found = 0 for msg in reversed(messages): if isinstance(msg, ToolMessage): context = str(msg.content) + tool_messages_found += 1 break - + + print(f"βœ“ Retrieved context: {len(context)} characters") + print(f"βœ“ Tool messages found: {tool_messages_found}") + print("="*70 + "\n") + return { "context": context, } - def summarization_node(state: QAState) -> QAState: """Summarization Agent node: generates draft answer from context. diff --git a/src/app/core/agents/graph.py b/src/app/core/agents/graph.py index e7907ca..5b922e5 100644 --- a/src/app/core/agents/graph.py +++ b/src/app/core/agents/graph.py @@ -8,7 +8,7 @@ from .agents import retrieval_node, summarization_node, verification_node from .state import QAState - +from .agents import planning_agent_node def create_qa_graph() -> Any: """Create and compile the linear multi-agent QA graph. @@ -27,14 +27,20 @@ def create_qa_graph() -> Any: builder.add_node("retrieval", retrieval_node) builder.add_node("summarization", summarization_node) builder.add_node("verification", verification_node) + builder.add_node("planning", planning_agent_node) + + #builder.set_entry_point("planning") # Define linear flow: START -> retrieval -> summarization -> verification -> END - builder.add_edge(START, "retrieval") + builder.add_edge(START, "planning") + builder.add_edge("planning", "retrieval") builder.add_edge("retrieval", "summarization") builder.add_edge("summarization", "verification") builder.add_edge("verification", END) return builder.compile() +app = create_qa_graph() + @lru_cache(maxsize=1) diff --git a/src/app/core/agents/prompts.py b/src/app/core/agents/prompts.py index 09bbe93..ddf19d4 100644 --- a/src/app/core/agents/prompts.py +++ b/src/app/core/agents/prompts.py @@ -38,3 +38,52 @@ - Ensure the final answer is accurate and grounded in the source material. - Return ONLY the final, corrected answer text (no explanations or meta-commentary). """ + + +PLANNING_SYSTEM_PROMPT = """You are an intelligent Query Planning Agent. Your job is to analyze +user questions and create a structured search strategy. +Your tasks: +1. Identify the key concepts and entities in the question +2. Rephrase ambiguous or unclear parts +3. Decompose complex, multi-part questions into focused sub-questions +4. Create a search plan that will help retrieve the most relevant information + +For each question, provide: +1. A PLAN: A brief strategy for how to search for information +2. SUB-QUESTIONS: A list of 2-5 focused search queries (only if the question is complex) + +Guidelines: +- For simple, single-concept questions: Just rephrase clearly, minimal sub-questions +- For complex, multi-part questions: Break into focused sub-questions +- Each sub-question should target ONE specific concept +- Use clear, search-friendly language +- Focus on keywords and concepts, not full sentences + +Example 1 - Complex Question: +Question: "What are the advantages of vector databases compared to traditional databases, and how do they handle scalability?" + +PLAN: This question has two distinct parts: (1) advantages and comparisons, (2) scalability mechanisms. We need to search for each aspect separately to get comprehensive information. + +SUB-QUESTIONS: +1. "vector database advantages benefits" +2. "vector database vs relational database comparison" +3. "vector database scalability architecture" + +Example 2 - Simple Question: +Question: "What is HNSW indexing?" + +PLAN: This is a straightforward definitional question about a specific concept. A single focused search should suffice. + +SUB-QUESTIONS: +1. "HNSW indexing algorithm" + +Example 3 - Moderately Complex: +Question: "How do embeddings work in semantic search?" + +PLAN: This question asks about the mechanism. We should search for embedding concepts and their application in semantic search. + +SUB-QUESTIONS: +1. "embeddings vectors semantic meaning" +2. "semantic search how embeddings work" + +Now analyze the user's question and provide your PLAN and SUB-QUESTIONS.""" \ No newline at end of file diff --git a/src/app/core/agents/state.py b/src/app/core/agents/state.py index 73fccb9..296c4dd 100644 --- a/src/app/core/agents/state.py +++ b/src/app/core/agents/state.py @@ -16,3 +16,5 @@ class QAState(TypedDict): context: str | None draft_answer: str | None answer: str | None + plan: str | None + sub_questions: list[str] | None diff --git a/src/app/core/agents/test_planning_agent.py b/src/app/core/agents/test_planning_agent.py new file mode 100644 index 0000000..f820386 --- /dev/null +++ b/src/app/core/agents/test_planning_agent.py @@ -0,0 +1,56 @@ +""" +Test the planning agent independently +Run this to make sure it works before integrating into graph +""" + +import os +from dotenv import load_dotenv +from src.app.core.agents.agents import planning_agent_node + +# Load environment +load_dotenv() + +def test_planning(): + """Test the planning agent with sample questions""" + + test_questions = [ + "What is HNSW indexing?", + "What are the advantages of vector databases compared to traditional databases, and how do they handle scalability?", + "How do embeddings work in machine learning?" + ] + + print("Testing Planning Agent") + print("="*70) + + for i, question in enumerate(test_questions, 1): + print(f"\nπŸ“ Test {i}/{len(test_questions)}") + print(f"Question: {question}") + print("-"*70) + + # Create minimal state + state = { + "question": question, + "context": None, + "answer": None, + "plan": None, + "sub_questions": None + } + + # Run planning node + result = planning_agent_node(state) + + print(f"βœ“ Planning complete!") + print(f"Plan: {result['plan'][:200]}...") + print(f"Sub-questions ({len(result['sub_questions'])}): {result['sub_questions']}") + print("="*70) + + input("Press Enter for next test...") + +if __name__ == "__main__": + if not os.getenv("OPENAI_API_KEY"): + print("❌ Error: OPENAI_API_KEY not found in environment") + print("Make sure .env file has your OpenAI API key") + exit(1) + + test_planning() + print("\nβœ“ All tests complete!") \ No newline at end of file diff --git a/src/app/models.py b/src/app/models.py index c733f26..d421bb6 100644 --- a/src/app/models.py +++ b/src/app/models.py @@ -1,3 +1,4 @@ +from typing import Optional from pydantic import BaseModel @@ -21,3 +22,5 @@ class QAResponse(BaseModel): answer: str context: str + plan: Optional[str] = None + sub_questions: Optional[list[str]] = None diff --git a/src/app/quick_test.py b/src/app/quick_test.py new file mode 100644 index 0000000..0a9654a --- /dev/null +++ b/src/app/quick_test.py @@ -0,0 +1,68 @@ +""" +Quick test of LangChain and LangGraph functionality +NOTE: Requires OPENAI_API_KEY in environment +""" + +import os +from typing import TypedDict +from dotenv import load_dotenv +from langchain_openai import ChatOpenAI +from langgraph.graph import StateGraph, END + +# Load environment variables +load_dotenv() + +# Check for API key +if not os.getenv("OPENAI_API_KEY"): + print("Warning: OPENAI_API_KEY not found in environment") + print("Set it in .env file or export it: export OPENAI_API_KEY='your-key'") + exit(1) + +# Define a simple state +class SimpleState(TypedDict): + message: str + count: int + +# Create a simple agent node +def agent_node(state: SimpleState) -> dict: + llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0) + response = llm.invoke(f"Say hello in a creative way. Current count: {state['count']}") + return { + "message": response.content, + "count": state["count"] + 1 + } + +# Build the graph +def test_langgraph(): + print("Testing LangGraph with LangChain...\n") + + # Create graph + graph = StateGraph(SimpleState) + + # Add node + graph.add_node("agent", agent_node) + + # Set entry point + graph.set_entry_point("agent") + + # Add edge to end + graph.add_edge("agent", END) + + # Compile + app = graph.compile() + + # Run + initial_state = { + "message": "", + "count": 0 + } + + print("Running graph...") + result = app.invoke(initial_state) + + print(f"\nβœ“ Graph executed successfully!") + print(f"Message: {result['message']}") + print(f"Count: {result['count']}") + +if __name__ == "__main__": + test_langgraph() \ No newline at end of file diff --git a/src/app/test_complete_flow b/src/app/test_complete_flow new file mode 100644 index 0000000..32b25be --- /dev/null +++ b/src/app/test_complete_flow @@ -0,0 +1,73 @@ +""" +Test the complete flow: Planning β†’ Retrieval β†’ Summarization β†’ Verification +""" + +import os +from dotenv import load_dotenv +from core.agents.graph import app +#from core.agents.graph import app + +load_dotenv() + +def test_flow(): + """Test complete graph with planning""" + + print("="*70) + print("TESTING COMPLETE FLOW WITH QUERY PLANNING") + print("="*70) + + # Test question + question = "What are the advantages of vector databases compared to traditional databases?" + + print(f"\nπŸ“ Question: {question}\n") + + # Create initial state + initial_state = { + "question": question, + "context": None, + "answer": None, + "plan": None, + "sub_questions": None + } + + # Run graph + print("Running graph...") + print("-"*70) + + try: + result = app.invoke(initial_state) + + print("result:", result) + print("\n" + "="*70) + print("FINAL RESULT") + print("="*70) + print(f"\nπŸ“‹ Plan Generated:") + print(result.get('plan', 'No plan')) + print(f"\n❓ Sub-questions:") + for i, sq in enumerate(result.get('sub_questions', []), 1): + print(f" {i}. {sq}") + print(f"\nπŸ“š Context Retrieved:") + print(result.get('context', 'No context')[:300] + "...") + print(f"\nπŸ’‘ Final Answer:") + print(result.get('answer', 'No answer')) + print("\n" + "="*70) + + return True + + except Exception as e: + print(f"\n❌ Error: {e}") + import traceback + traceback.print_exc() + return False + +if __name__ == "__main__": + if not os.getenv("Open_AI_API_KEY"): + print("❌ Error: Open_AI_API_KEY not set") + exit(1) + + success = test_flow() + + if success: + print("\nβœ“ Complete flow test PASSED!") + else: + print("\nβœ— Complete flow test FAILED - check errors above") \ No newline at end of file From 4ce7496b2ed75c3e84a70195d985948da0be5195 Mon Sep 17 00:00:00 2001 From: Bhagya-Wanasinghe Date: Thu, 22 Jan 2026 02:44:06 +0530 Subject: [PATCH 02/12] health check --- src/app/api.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/app/api.py b/src/app/api.py index c0e35da..580aa69 100644 --- a/src/app/api.py +++ b/src/app/api.py @@ -19,6 +19,18 @@ version="0.1.0", ) +@app.get("/") +def root(): + return { + "status": "ok", + "message": "IKMS API is running πŸš€" + } + +@app.get("/health") +def health(): + return {"status": "healthy"} + + app.add_middleware( CORSMiddleware, allow_origins=["*"], # In production, specify your frontend domain From c92ec61e84353e972523fcb61e8b7ab8c459a930 Mon Sep 17 00:00:00 2001 From: Bhagya-Wanasinghe Date: Thu, 22 Jan 2026 02:59:00 +0530 Subject: [PATCH 03/12] added url --- .gitignore | 3 ++- frontend/index.html | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 8ea2d2e..63fec86 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ wheels/ .vscode -data/ \ No newline at end of file +data/ +.vercel diff --git a/frontend/index.html b/frontend/index.html index cadeb51..08f8f27 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -400,7 +400,8 @@

    🧠 IKMS Query Planning System

    \ No newline at end of file diff --git a/src/app/core/config.py b/src/app/core/config.py index 7ea56b1..4e9cfe6 100644 --- a/src/app/core/config.py +++ b/src/app/core/config.py @@ -13,7 +13,7 @@ class Settings(BaseSettings): # OpenAI Configuration openai_api_key: str openai_model_name: str = "gpt-4o-mini" - openai_embedding_model_name: str = "text-embedding-3-large" + openai_embedding_model_name: str = "text-embedding-3-small" # Pinecone Configuration pinecone_api_key: str diff --git a/src/app/core/retrieval/vector_store.py b/src/app/core/retrieval/vector_store.py index 5c1a414..4173902 100644 --- a/src/app/core/retrieval/vector_store.py +++ b/src/app/core/retrieval/vector_store.py @@ -61,7 +61,7 @@ def retrieve(query: str, k: int | None = None) -> List[Document]: retriever = get_retriever(k=k) return retriever.invoke(query) -def index_documents(file_path: Path) -> int: +def index_documents(docs: List[Document]) -> int: """Index a list of Document objects into the Pinecone vector store. Args: @@ -70,12 +70,10 @@ def index_documents(file_path: Path) -> int: Returns: The number of documents indexed. """ - loader = PyPDFLoader(str(file_path), mode="single") - docs = loader.load() - text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50) texts = text_splitter.split_documents(docs) vector_store = _get_vector_store() vector_store.add_documents(texts) + return len(texts) \ No newline at end of file diff --git a/src/app/services/indexing_service.py b/src/app/services/indexing_service.py index eb37c12..5c1a2d3 100644 --- a/src/app/services/indexing_service.py +++ b/src/app/services/indexing_service.py @@ -18,4 +18,6 @@ def index_pdf_file(file_path: Path) -> int: """ loader = PyPDFLoader(str(file_path)) docs = loader.load() + + # Pass the loaded documents to the indexing function return index_documents(docs)