Q/A history
This commit is contained in:
parent
5a1cb253e5
commit
14873862f8
3 changed files with 104 additions and 13 deletions
|
|
@ -2,7 +2,7 @@ from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from pydantic_ai import Agent, RunContext
|
from pydantic_ai import Agent, RunContext, format_as_xml
|
||||||
|
|
||||||
from haiku.rag.client import HaikuRAG
|
from haiku.rag.client import HaikuRAG
|
||||||
from haiku.rag.config.models import AppConfig
|
from haiku.rag.config.models import AppConfig
|
||||||
|
|
@ -26,11 +26,38 @@ class CitationInfo(BaseModel):
|
||||||
content: str
|
content: str
|
||||||
|
|
||||||
|
|
||||||
|
class QAResponse(BaseModel):
|
||||||
|
"""A Q&A pair from conversation history."""
|
||||||
|
|
||||||
|
question: str
|
||||||
|
answer: str
|
||||||
|
sources: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
class ChatSessionState(BaseModel):
|
class ChatSessionState(BaseModel):
|
||||||
"""State shared between frontend and agent via AG-UI."""
|
"""State shared between frontend and agent via AG-UI."""
|
||||||
|
|
||||||
session_id: str = ""
|
session_id: str = ""
|
||||||
citations: list[CitationInfo] = []
|
citations: list[CitationInfo] = []
|
||||||
|
qa_history: list[QAResponse] = []
|
||||||
|
|
||||||
|
|
||||||
|
def format_conversation_context(qa_history: list[QAResponse]) -> str:
|
||||||
|
"""Format conversation history as XML for inclusion in prompts."""
|
||||||
|
if not qa_history:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
context_data = {
|
||||||
|
"previous_qa": [
|
||||||
|
{
|
||||||
|
"question": qa.question,
|
||||||
|
"answer": qa.answer,
|
||||||
|
"sources": qa.sources,
|
||||||
|
}
|
||||||
|
for qa in qa_history
|
||||||
|
],
|
||||||
|
}
|
||||||
|
return format_as_xml(context_data, root_tag="conversation_context")
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -41,6 +68,7 @@ class ChatDeps:
|
||||||
config: AppConfig
|
config: AppConfig
|
||||||
agui_emitter: "AGUIEmitter | None" = None
|
agui_emitter: "AGUIEmitter | None" = None
|
||||||
search_results: list[SearchResult] | None = None
|
search_results: list[SearchResult] | None = None
|
||||||
|
session_state: ChatSessionState | None = None
|
||||||
|
|
||||||
|
|
||||||
CHAT_SYSTEM_PROMPT = """You are a helpful research assistant powered by haiku.rag, a knowledge base system.
|
CHAT_SYSTEM_PROMPT = """You are a helpful research assistant powered by haiku.rag, a knowledge base system.
|
||||||
|
|
@ -53,7 +81,7 @@ CRITICAL RULES:
|
||||||
3. NEVER make up information - always use tools to get facts from the knowledge base
|
3. NEVER make up information - always use tools to get facts from the knowledge base
|
||||||
|
|
||||||
How to decide which tool to use:
|
How to decide which tool to use:
|
||||||
- "ask" - DEFAULT CHOICE for any question. Use this for questions like "What is X?", "How does Y work?", "Explain Z", etc. Returns answers with citations.
|
- "ask" - DEFAULT CHOICE for any question. Use this for questions like "What is X?", "How does Y work?", "Explain Z", etc. Returns answers with citations. The ask tool maintains conversation context, so follow-up questions benefit from previous answers.
|
||||||
- "search" - ONLY use when explicitly exploring/browsing the knowledge base, or when the user asks to "search for" or "find" something without needing an answer.
|
- "search" - ONLY use when explicitly exploring/browsing the knowledge base, or when the user asks to "search for" or "find" something without needing an answer.
|
||||||
|
|
||||||
Be friendly and conversational. When you use tools, summarize the key findings for the user."""
|
Be friendly and conversational. When you use tools, summarize the key findings for the user."""
|
||||||
|
|
@ -122,10 +150,40 @@ def create_chat_agent(config: AppConfig) -> Agent[ChatDeps, str]:
|
||||||
if ctx.deps.agui_emitter:
|
if ctx.deps.agui_emitter:
|
||||||
ctx.deps.agui_emitter.log(f"Answering: {question}")
|
ctx.deps.agui_emitter.log(f"Answering: {question}")
|
||||||
|
|
||||||
answer, citations = await ctx.deps.client.ask(question, filter=document_filter)
|
# Build context-aware system prompt if we have history
|
||||||
|
system_prompt = None
|
||||||
|
if ctx.deps.session_state and ctx.deps.session_state.qa_history:
|
||||||
|
from haiku.rag.qa.prompts import QA_SYSTEM_PROMPT
|
||||||
|
|
||||||
# Emit citations via AG-UI state for frontend rendering
|
context_xml = format_conversation_context(ctx.deps.session_state.qa_history)
|
||||||
if citations and ctx.deps.agui_emitter:
|
system_prompt = (
|
||||||
|
f"{QA_SYSTEM_PROMPT}\n\n"
|
||||||
|
f"{context_xml}\n\n"
|
||||||
|
"Use this conversation context to provide informed answers. "
|
||||||
|
"Reference previous answers when relevant."
|
||||||
|
)
|
||||||
|
|
||||||
|
answer, citations = await ctx.deps.client.ask(
|
||||||
|
question, system_prompt=system_prompt, filter=document_filter
|
||||||
|
)
|
||||||
|
|
||||||
|
# Accumulate Q&A in session state
|
||||||
|
if ctx.deps.session_state is not None:
|
||||||
|
sources = (
|
||||||
|
[c.document_title or c.document_uri for c in citations]
|
||||||
|
if citations
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
qa_response = QAResponse(
|
||||||
|
question=question,
|
||||||
|
answer=answer,
|
||||||
|
sources=list(dict.fromkeys(sources)), # dedupe preserving order
|
||||||
|
)
|
||||||
|
ctx.deps.session_state.qa_history.append(qa_response)
|
||||||
|
|
||||||
|
# Build citation infos for frontend
|
||||||
|
citation_infos = []
|
||||||
|
if citations:
|
||||||
citation_infos = [
|
citation_infos = [
|
||||||
CitationInfo(
|
CitationInfo(
|
||||||
index=i + 1,
|
index=i + 1,
|
||||||
|
|
@ -139,8 +197,23 @@ def create_chat_agent(config: AppConfig) -> Agent[ChatDeps, str]:
|
||||||
)
|
)
|
||||||
for i, c in enumerate(citations)
|
for i, c in enumerate(citations)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Emit updated state with citations AND accumulated qa_history
|
||||||
|
if ctx.deps.agui_emitter:
|
||||||
ctx.deps.agui_emitter.update_state(
|
ctx.deps.agui_emitter.update_state(
|
||||||
ChatSessionState(citations=citation_infos)
|
ChatSessionState(
|
||||||
|
session_id=(
|
||||||
|
ctx.deps.session_state.session_id
|
||||||
|
if ctx.deps.session_state
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
citations=citation_infos,
|
||||||
|
qa_history=(
|
||||||
|
ctx.deps.session_state.qa_history
|
||||||
|
if ctx.deps.session_state
|
||||||
|
else []
|
||||||
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Format answer with citation references
|
# Format answer with citation references
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from agent import ChatDeps, ChatSessionState, create_chat_agent
|
from agent import ChatDeps, ChatSessionState, QAResponse, create_chat_agent
|
||||||
from anyio import create_memory_object_stream, create_task_group
|
from anyio import create_memory_object_stream, create_task_group
|
||||||
from anyio.streams.memory import MemoryObjectSendStream
|
from anyio.streams.memory import MemoryObjectSendStream
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
@ -122,16 +122,26 @@ async def stream_chat(request: Request) -> StreamingResponse:
|
||||||
effective_db_path = Path(input_data.config["db_path"])
|
effective_db_path = Path(input_data.config["db_path"])
|
||||||
client = get_client(effective_db_path)
|
client = get_client(effective_db_path)
|
||||||
|
|
||||||
# Create deps
|
# Parse incoming state to restore qa_history
|
||||||
|
initial_qa_history: list[QAResponse] = []
|
||||||
|
if input_data.state and "qa_history" in input_data.state:
|
||||||
|
initial_qa_history = [
|
||||||
|
QAResponse(**qa)
|
||||||
|
for qa in input_data.state.get("qa_history", [])
|
||||||
|
]
|
||||||
|
|
||||||
|
# Create initial state with restored history
|
||||||
|
initial_state = ChatSessionState(
|
||||||
|
session_id=input_data.thread_id or "",
|
||||||
|
qa_history=initial_qa_history,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create deps with session state
|
||||||
deps = ChatDeps(
|
deps = ChatDeps(
|
||||||
client=client,
|
client=client,
|
||||||
config=Config,
|
config=Config,
|
||||||
agui_emitter=emitter,
|
agui_emitter=emitter,
|
||||||
)
|
session_state=initial_state,
|
||||||
|
|
||||||
# Start run with empty state
|
|
||||||
initial_state = ChatSessionState(
|
|
||||||
session_id=input_data.thread_id or "",
|
|
||||||
)
|
)
|
||||||
emitter.start_run(initial_state=initial_state)
|
emitter.start_run(initial_state=initial_state)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,16 @@ interface Citation {
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface QAResponse {
|
||||||
|
question: string;
|
||||||
|
answer: string;
|
||||||
|
sources: string[];
|
||||||
|
}
|
||||||
|
|
||||||
interface ChatSessionState {
|
interface ChatSessionState {
|
||||||
session_id: string;
|
session_id: string;
|
||||||
citations: Citation[];
|
citations: Citation[];
|
||||||
|
qa_history: QAResponse[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChatContent() {
|
function ChatContent() {
|
||||||
|
|
@ -32,6 +39,7 @@ function ChatContent() {
|
||||||
initialState: {
|
initialState: {
|
||||||
session_id: "",
|
session_id: "",
|
||||||
citations: [],
|
citations: [],
|
||||||
|
qa_history: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue