Skip to content

Commit ab0a98a

Browse files
author
raminmohammadi
committed
added query agent which response with respect to the available KB
1 parent f35d091 commit ab0a98a

File tree

10 files changed

+286
-43
lines changed

10 files changed

+286
-43
lines changed

clean_env.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/bin/bash
2+
3+
echo "🧹 Cleaning environment..."
4+
5+
# Remove virtual environment
6+
rm -rf .venv
7+
rm -rf build/
8+
rm -rf dist/
9+
10+
# Remove Python cache
11+
find . -name '*.pyc' -delete
12+
find . -type d -name '__pycache__' -exec rm -rf {} +
13+
pip cache purge
14+
15+
# Remove test and mypy caches
16+
rm -rf __pycache__ .pytest_cache .mypy_cache
17+
18+
echo "✅ Clean complete."

frontend/package-lock.json

Lines changed: 34 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@
1313
"clsx": "^2.0.0",
1414
"lucide-react": "^0.293.0",
1515
"next": "^14.2.28",
16+
"next-themes": "^0.4.6",
1617
"postcss": "^8.4.24",
1718
"radix-ui": "^1.0.0",
1819
"react": "18.2.0",
1920
"react-dom": "18.2.0",
2021
"shadcn-ui": "^0.9.0",
21-
"tailwindcss": "^3.4.1"
22+
"tailwindcss": "^3.4.1",
23+
"tailwindcss-animate": "^1.0.7"
2224
},
2325
"devDependencies": {
2426
"@types/node": "22.14.0",
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
from langchain.chains import (
2+
create_history_aware_retriever,
3+
)
4+
from langchain.chains.combine_documents import create_stuff_documents_chain
5+
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
6+
from langchain_google_vertexai import ChatVertexAI
7+
from langchain_ai_agent.retriever.vector_store import DocumentEmbedder
8+
from langchain_core.runnables import RunnableLambda, RunnableMap
9+
10+
# LangGraph memory imports
11+
from langgraph.checkpoint.memory import MemorySaver
12+
from langchain.prompts import PromptTemplate
13+
from langgraph.graph import START, END, StateGraph
14+
from langchain_core.messages import AIMessage, HumanMessage
15+
from typing import TypedDict, Annotated, Sequence
16+
import operator
17+
import logging
18+
19+
# Configure logging
20+
logger = logging.getLogger(__name__)
21+
logging.basicConfig(level=logging.INFO)
22+
23+
class AgentState(TypedDict):
24+
messages: Annotated[Sequence[HumanMessage | AIMessage], operator.add]
25+
question: str
26+
graph_output: str
27+
28+
29+
def get_chat_agent_with_memory(persist_dir: str):
30+
"""
31+
Creates and returns an agent that maintains persistent conversation memory.
32+
The agent is invoked using .ainvoke({"question": ...}, config={"configurable": {"thread_id": ...}})
33+
"""
34+
embedder = DocumentEmbedder(persist_dir=persist_dir)
35+
retriever = embedder.get_retriever(k=10)
36+
37+
llm = ChatVertexAI(
38+
model_name="gemini-2.0-flash-lite",
39+
temperature=0.3,
40+
max_output_tokens=1024,
41+
)
42+
43+
contextualize_q_system_prompt = (
44+
"Given a chat history and the latest user question "
45+
"which might reference context in the chat history, "
46+
"formulate a standalone question that can be understood "
47+
"without the chat history. Do NOT answer the question; just "
48+
"reformulate it if needed and otherwise return it as is."
49+
)
50+
contextualize_q_prompt = ChatPromptTemplate.from_messages(
51+
[
52+
("system", contextualize_q_system_prompt),
53+
MessagesPlaceholder(variable_name="chat_history"),
54+
("human", "{input}"),
55+
]
56+
)
57+
58+
try:
59+
history_aware_retriever = create_history_aware_retriever(
60+
llm, retriever, contextualize_q_prompt
61+
)
62+
except Exception as e:
63+
logger.info(f"[Retriever] {e}")
64+
65+
qa_system_prompt = (
66+
"You are an assistant for question-answering tasks. Use the following "
67+
"pieces of retrieved context to answer the question. If you don't know "
68+
"the answer, just say that you don't know. Use three sentences maximum and "
69+
"keep the answer concise."
70+
)
71+
72+
stuff_prompt = PromptTemplate(
73+
template="""You are an assistant for question-answering tasks.
74+
Use the following pieces of context to answer the question. If you don't know the answer, just say that you don't know.
75+
Context:
76+
{context}
77+
Question:
78+
{question}
79+
Answer:""",
80+
input_variables=["context", "question"]
81+
)
82+
83+
try:
84+
combine_docs_chain = create_stuff_documents_chain(llm, stuff_prompt)
85+
except Exception as e:
86+
logger.info(f"[Chain] {e}")
87+
88+
retrieval_chain = RunnableMap({
89+
"context": lambda x: retriever.invoke(x["input"]),
90+
"question": lambda x: x["input"]
91+
}) | combine_docs_chain
92+
93+
94+
graph_builder = StateGraph(AgentState)
95+
96+
def call_model(state: AgentState) -> dict:
97+
logger.info(f"[call_model] Full state: {state}")
98+
question = state.get("question", "")
99+
logger.info(f"[call_model] Extracted question: {question}")
100+
101+
if not question:
102+
return {
103+
"messages": [AIMessage(content="[call_model] Empty or missing question.")],
104+
"graph_output": "[call_model] Empty or missing question."
105+
}
106+
107+
chat_history = state.get("messages", [])
108+
109+
chain_input = {
110+
"input": question,
111+
"chat_history": chat_history
112+
}
113+
114+
chain_output = retrieval_chain.invoke(chain_input)
115+
answer_text = chain_output
116+
117+
logger.info(f"[Agent] Response: {answer_text}")
118+
119+
return {
120+
"messages": [AIMessage(content=answer_text or "[call_model] No answer generated.")],
121+
"graph_output": answer_text or "[call_model] No answer generated."
122+
}
123+
124+
graph_builder.add_node("model", call_model)
125+
graph_builder.set_entry_point("model")
126+
graph_builder.add_edge("model", END)
127+
128+
memory = MemorySaver()
129+
app = graph_builder.compile(checkpointer=memory)
130+
return app
Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,31 @@
11
from fastapi import APIRouter, Query, HTTPException
2-
from langchain_ai_agent.retriever.vector_store import DocumentEmbedder
2+
from fastapi.responses import StreamingResponse
3+
from langchain_ai_agent.agents.chat_agent import get_chat_agent_with_memory
4+
import traceback, logging
5+
from fastapi.responses import JSONResponse
6+
7+
logger = logging.getLogger(__name__)
8+
logging.basicConfig(level=logging.INFO)
39

410
router = APIRouter()
511

612
@router.get("/api/query")
7-
def query_kb(question: str = Query(...), namespace: str = Query("default")):
13+
async def query_kb(question: str = Query(...), namespace: str = Query("default")):
814
try:
9-
embedder = DocumentEmbedder(persist_dir=f"faiss_index/{namespace}")
10-
docs = embedder.query(question)
11-
return {"results": [doc.page_content for doc in docs]}
15+
agent = get_chat_agent_with_memory(persist_dir=f"faiss_index/{namespace}")
16+
result = await agent.ainvoke(
17+
{"question": question},
18+
config={"configurable": {"thread_id": "query-session"}}
19+
)
20+
21+
answer = result.get("graph_output", "").strip()
22+
if not answer:
23+
raise HTTPException(status_code=500, detail="Agent returned no answer.")
24+
25+
# ✅ Send response as JSON with a results array
26+
return JSONResponse(content={"results": [answer]})
27+
1228
except Exception as e:
29+
logger.error(f"Agent execution failed: {e}")
30+
logger.error(traceback.format_exc())
1331
raise HTTPException(status_code=500, detail=str(e))

langchain_ai_agent/config/ingestion_config.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ supported_extensions:
44
- .pdf
55
- .docx
66
- .txt
7-
- .eml
7+
- .eml
8+
- .html

langchain_ai_agent/ingestion/reader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def __init__(self, config_path: str = "config/ingestion_config.yaml"):
1616
self.config = self._load_config(config_path)
1717
self.chunk_size = self.config.get("chunk_size", 500)
1818
self.chunk_overlap = self.config.get("chunk_overlap", 50)
19-
self.supported_extensions = set(self.config.get("supported_extensions", [".pdf", ".docx", ".txt", ".eml"]))
19+
self.supported_extensions = set(self.config.get("supported_extensions", [".pdf", ".docx", ".txt", ".eml", ".html"]))
2020
self.text_splitter = RecursiveCharacterTextSplitter(
2121
chunk_size=self.chunk_size,
2222
chunk_overlap=self.chunk_overlap

0 commit comments

Comments
 (0)