Remove unused conversational output mode from research graph
This commit is contained in:
parent
e6310fc484
commit
eb9436eb2a
11 changed files with 53 additions and 304 deletions
|
|
@ -16,7 +16,7 @@ from starlette.routing import Route
|
||||||
from haiku.rag.client import HaikuRAG
|
from haiku.rag.client import HaikuRAG
|
||||||
from haiku.rag.config import load_yaml_config
|
from haiku.rag.config import load_yaml_config
|
||||||
from haiku.rag.config.models import AppConfig
|
from haiku.rag.config.models import AppConfig
|
||||||
from haiku.rag.skills.rag import create_skill
|
from haiku.rag.skills.rag import AGENT_PREAMBLE, create_skill
|
||||||
from haiku.skills import SkillDeps, SkillToolset
|
from haiku.skills import SkillDeps, SkillToolset
|
||||||
|
|
||||||
load_dotenv(find_dotenv(usecwd=True))
|
load_dotenv(find_dotenv(usecwd=True))
|
||||||
|
|
@ -66,16 +66,6 @@ def get_client() -> HaikuRAG:
|
||||||
skill = create_skill(db_path=db_path, config=Config)
|
skill = create_skill(db_path=db_path, config=Config)
|
||||||
toolset = SkillToolset(skills=[skill])
|
toolset = SkillToolset(skills=[skill])
|
||||||
|
|
||||||
AGENT_PREAMBLE = """You are a helpful research assistant powered by haiku.rag, a knowledge base system.
|
|
||||||
|
|
||||||
CRITICAL RULES:
|
|
||||||
1. For greetings or casual chat: respond directly WITHOUT using any tools
|
|
||||||
2. NEVER make up information - always use tools to get facts from the knowledge base
|
|
||||||
3. For questions: Use the "ask" tool - it handles search and citation automatically
|
|
||||||
4. For searches: Use the "search" tool - copy the ENTIRE tool response to your output INCLUDING content snippets
|
|
||||||
5. When you use the "ask" tool, summarize the key findings and always include citations in your response
|
|
||||||
"""
|
|
||||||
|
|
||||||
agent = Agent(
|
agent = Agent(
|
||||||
os.getenv("HAIKU_CHAT_MODEL", "openai:gpt-4o"),
|
os.getenv("HAIKU_CHAT_MODEL", "openai:gpt-4o"),
|
||||||
instructions=AGENT_PREAMBLE + toolset.system_prompt,
|
instructions=AGENT_PREAMBLE + toolset.system_prompt,
|
||||||
|
|
|
||||||
|
|
@ -84,15 +84,6 @@ When prior answers are provided, the planner uses a context-aware prompt that ev
|
||||||
- **search_one**: Answers a single question using the knowledge base (up to 3 search calls per question). Each answer is added to `ResearchContext.qa_responses` for the next planning iteration.
|
- **search_one**: Answers a single question using the knowledge base (up to 3 search calls per question). Each answer is added to `ResearchContext.qa_responses` for the next planning iteration.
|
||||||
- **synthesize**: Generates the final output from all gathered evidence.
|
- **synthesize**: Generates the final output from all gathered evidence.
|
||||||
|
|
||||||
**Output modes:**
|
|
||||||
|
|
||||||
The graph supports two output modes via `build_research_graph(output_mode=...)`:
|
|
||||||
|
|
||||||
| Mode | Output type | Used by |
|
|
||||||
|------|-------------|---------|
|
|
||||||
| `"report"` | `ResearchReport` (title, executive summary, findings, conclusions, recommendations) | CLI `haiku-rag research`, Python API |
|
|
||||||
| `"conversational"` | `ConversationalAnswer` (answer, citations, confidence) | Chat agent's `ask` tool |
|
|
||||||
|
|
||||||
**Iterative flow:**
|
**Iterative flow:**
|
||||||
|
|
||||||
- Each iteration: planner evaluates context → proposes one question → search answers it → loop back
|
- Each iteration: planner evaluates context → proposes one question → search answers it → loop back
|
||||||
|
|
@ -160,40 +151,6 @@ async with HaikuRAG(path_to_db) as client:
|
||||||
report = await graph.run(state=state, deps=deps)
|
report = await graph.run(state=state, deps=deps)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Conversational mode with prior answers:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
from haiku.rag.config import Config
|
|
||||||
from haiku.rag.agents.research.dependencies import ResearchContext
|
|
||||||
from haiku.rag.agents.research.graph import build_research_graph
|
|
||||||
from haiku.rag.agents.research.models import SearchAnswer
|
|
||||||
from haiku.rag.agents.research.state import ResearchDeps, ResearchState
|
|
||||||
|
|
||||||
# Conversational mode returns ConversationalAnswer instead of ResearchReport
|
|
||||||
graph = build_research_graph(config=Config, output_mode="conversational")
|
|
||||||
|
|
||||||
# Pass session context and prior answers from conversation history
|
|
||||||
context = ResearchContext(
|
|
||||||
original_question="How does it handle authentication?",
|
|
||||||
session_context="User is building a Python web app with FastAPI.",
|
|
||||||
qa_responses=[
|
|
||||||
SearchAnswer(
|
|
||||||
query="What authentication methods are supported?",
|
|
||||||
answer="JWT and OAuth2 are supported.",
|
|
||||||
confidence=0.95,
|
|
||||||
cited_chunks=["chunk-1"],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
state = ResearchState.from_config(context=context, config=Config)
|
|
||||||
deps = ResearchDeps(client=client)
|
|
||||||
|
|
||||||
result = await graph.run(state=state, deps=deps)
|
|
||||||
print(result.answer) # Direct conversational answer
|
|
||||||
print(result.confidence) # 0.0-1.0
|
|
||||||
print(result.citations) # Deduplicated citations from all searches
|
|
||||||
```
|
|
||||||
|
|
||||||
### Filtering Documents
|
### Filtering Documents
|
||||||
|
|
||||||
Restrict searches to specific documents via the `search_filter` parameter:
|
Restrict searches to specific documents via the `search_filter` parameter:
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ flowchart TB
|
||||||
- Proposes one question at a time, evaluates the answer, then decides whether to continue
|
- Proposes one question at a time, evaluates the answer, then decides whether to continue
|
||||||
- Session context resolves ambiguous references
|
- Session context resolves ambiguous references
|
||||||
- Prior answers let the planner skip redundant searches
|
- Prior answers let the planner skip redundant searches
|
||||||
- Synthesizes structured report or conversational answer
|
- Synthesizes structured report
|
||||||
|
|
||||||
**RLM Agent** - Complex analytical tasks via code execution:
|
**RLM Agent** - Complex analytical tasks via code execution:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Literal, overload
|
|
||||||
|
|
||||||
from pydantic_ai import Agent, RunContext, format_as_xml
|
from pydantic_ai import Agent, RunContext, format_as_xml
|
||||||
from pydantic_ai.output import ToolOutput
|
from pydantic_ai.output import ToolOutput
|
||||||
|
|
@ -7,15 +6,12 @@ from pydantic_graph.beta import Graph, GraphBuilder, StepContext
|
||||||
|
|
||||||
from haiku.rag.agents.research.dependencies import ResearchContext, ResearchDependencies
|
from haiku.rag.agents.research.dependencies import ResearchContext, ResearchDependencies
|
||||||
from haiku.rag.agents.research.models import (
|
from haiku.rag.agents.research.models import (
|
||||||
Citation,
|
|
||||||
ConversationalAnswer,
|
|
||||||
IterativePlanResult,
|
IterativePlanResult,
|
||||||
RawSearchAnswer,
|
RawSearchAnswer,
|
||||||
ResearchReport,
|
ResearchReport,
|
||||||
SearchAnswer,
|
SearchAnswer,
|
||||||
)
|
)
|
||||||
from haiku.rag.agents.research.prompts import (
|
from haiku.rag.agents.research.prompts import (
|
||||||
CONVERSATIONAL_SYNTHESIS_PROMPT,
|
|
||||||
ITERATIVE_PLAN_PROMPT,
|
ITERATIVE_PLAN_PROMPT,
|
||||||
ITERATIVE_PLAN_PROMPT_WITH_CONTEXT,
|
ITERATIVE_PLAN_PROMPT_WITH_CONTEXT,
|
||||||
SEARCH_PROMPT,
|
SEARCH_PROMPT,
|
||||||
|
|
@ -169,29 +165,13 @@ async def _search_one_step_logic(
|
||||||
return SearchAnswer(query=sub_q, answer="", confidence=0.0)
|
return SearchAnswer(query=sub_q, answer="", confidence=0.0)
|
||||||
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def build_research_graph(
|
|
||||||
config: AppConfig = ...,
|
|
||||||
output_mode: Literal["report"] = ...,
|
|
||||||
) -> Graph[ResearchState, ResearchDeps, None, ResearchReport]: ...
|
|
||||||
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def build_research_graph(
|
|
||||||
config: AppConfig = ...,
|
|
||||||
output_mode: Literal["conversational"] = ...,
|
|
||||||
) -> Graph[ResearchState, ResearchDeps, None, ConversationalAnswer]: ...
|
|
||||||
|
|
||||||
|
|
||||||
def build_research_graph(
|
def build_research_graph(
|
||||||
config: AppConfig = Config,
|
config: AppConfig = Config,
|
||||||
output_mode: Literal["report", "conversational"] = "report",
|
) -> Graph[ResearchState, ResearchDeps, None, ResearchReport]:
|
||||||
) -> Graph[ResearchState, ResearchDeps, None, ResearchReport | ConversationalAnswer]:
|
|
||||||
"""Build the iterative research graph.
|
"""Build the iterative research graph.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: AppConfig object (uses config.research for provider, model, and graph parameters)
|
config: AppConfig object (uses config.research for provider, model, and graph parameters)
|
||||||
output_mode: Output format - "report" for ResearchReport, "conversational" for ConversationalAnswer
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Configured research graph with iterative planning
|
Configured research graph with iterative planning
|
||||||
|
|
@ -199,18 +179,14 @@ def build_research_graph(
|
||||||
model_config = config.research.model
|
model_config = config.research.model
|
||||||
|
|
||||||
search_prompt = build_prompt(SEARCH_PROMPT, config)
|
search_prompt = build_prompt(SEARCH_PROMPT, config)
|
||||||
|
synthesis_prompt = build_prompt(
|
||||||
if output_mode == "report":
|
config.prompts.synthesis or SYNTHESIS_PROMPT, config
|
||||||
synthesis_prompt = build_prompt(
|
)
|
||||||
config.prompts.synthesis or SYNTHESIS_PROMPT, config
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
synthesis_prompt = build_prompt(CONVERSATIONAL_SYNTHESIS_PROMPT, config)
|
|
||||||
|
|
||||||
g = GraphBuilder(
|
g = GraphBuilder(
|
||||||
state_type=ResearchState,
|
state_type=ResearchState,
|
||||||
deps_type=ResearchDeps,
|
deps_type=ResearchDeps,
|
||||||
output_type=ResearchReport if output_mode == "report" else ConversationalAnswer,
|
output_type=ResearchReport,
|
||||||
)
|
)
|
||||||
|
|
||||||
@g.step
|
@g.step
|
||||||
|
|
@ -236,81 +212,35 @@ def build_research_graph(
|
||||||
confidence=0.0,
|
confidence=0.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
if output_mode == "report":
|
@g.step
|
||||||
|
async def synthesize(
|
||||||
|
ctx: StepContext[ResearchState, ResearchDeps, IterativePlanResult],
|
||||||
|
) -> ResearchReport:
|
||||||
|
"""Generate final research report."""
|
||||||
|
state = ctx.state
|
||||||
|
deps = ctx.deps
|
||||||
|
|
||||||
@g.step
|
agent: Agent[ResearchDependencies, ResearchReport] = Agent( # type: ignore[assignment]
|
||||||
async def synthesize(
|
model=get_model(model_config, config),
|
||||||
ctx: StepContext[ResearchState, ResearchDeps, IterativePlanResult],
|
output_type=ResearchReport,
|
||||||
) -> ResearchReport:
|
instructions=synthesis_prompt,
|
||||||
"""Generate final research report."""
|
retries=3,
|
||||||
state = ctx.state
|
output_retries=3,
|
||||||
deps = ctx.deps
|
deps_type=ResearchDependencies,
|
||||||
|
)
|
||||||
|
|
||||||
agent: Agent[ResearchDependencies, ResearchReport] = Agent( # type: ignore[assignment]
|
context_xml = format_context_for_prompt(state.context)
|
||||||
model=get_model(model_config, config),
|
prompt = (
|
||||||
output_type=ResearchReport,
|
"Generate a comprehensive research report based on all gathered information.\n\n"
|
||||||
instructions=synthesis_prompt,
|
f"{context_xml}\n\n"
|
||||||
retries=3,
|
"Create a detailed report that synthesizes all findings into a coherent response."
|
||||||
output_retries=3,
|
)
|
||||||
deps_type=ResearchDependencies,
|
agent_deps = ResearchDependencies(
|
||||||
)
|
client=deps.client,
|
||||||
|
context=state.context,
|
||||||
context_xml = format_context_for_prompt(state.context)
|
)
|
||||||
prompt = (
|
result = await agent.run(prompt, deps=agent_deps)
|
||||||
"Generate a comprehensive research report based on all gathered information.\n\n"
|
return result.output
|
||||||
f"{context_xml}\n\n"
|
|
||||||
"Create a detailed report that synthesizes all findings into a coherent response."
|
|
||||||
)
|
|
||||||
agent_deps = ResearchDependencies(
|
|
||||||
client=deps.client,
|
|
||||||
context=state.context,
|
|
||||||
)
|
|
||||||
result = await agent.run(prompt, deps=agent_deps)
|
|
||||||
return result.output
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
@g.step
|
|
||||||
async def synthesize(
|
|
||||||
ctx: StepContext[ResearchState, ResearchDeps, IterativePlanResult],
|
|
||||||
) -> ConversationalAnswer:
|
|
||||||
"""Generate conversational answer from gathered evidence."""
|
|
||||||
state = ctx.state
|
|
||||||
deps = ctx.deps
|
|
||||||
|
|
||||||
agent: Agent[ResearchDependencies, ConversationalAnswer] = Agent( # type: ignore[assignment]
|
|
||||||
model=get_model(model_config, config),
|
|
||||||
output_type=ConversationalAnswer,
|
|
||||||
instructions=synthesis_prompt,
|
|
||||||
retries=3,
|
|
||||||
output_retries=3,
|
|
||||||
deps_type=ResearchDependencies,
|
|
||||||
)
|
|
||||||
|
|
||||||
context_xml = format_context_for_prompt(state.context)
|
|
||||||
prompt = (
|
|
||||||
f"Answer the question based on the gathered evidence.\n\n{context_xml}"
|
|
||||||
)
|
|
||||||
agent_deps = ResearchDependencies(
|
|
||||||
client=deps.client,
|
|
||||||
context=state.context,
|
|
||||||
)
|
|
||||||
result = await agent.run(prompt, deps=agent_deps)
|
|
||||||
|
|
||||||
# Collect unique citations from qa_responses (dedupe by chunk_id)
|
|
||||||
seen_chunks: set[str] = set()
|
|
||||||
unique_citations: list[Citation] = []
|
|
||||||
for qa in state.context.qa_responses:
|
|
||||||
for c in qa.citations:
|
|
||||||
if c.chunk_id not in seen_chunks:
|
|
||||||
seen_chunks.add(c.chunk_id)
|
|
||||||
unique_citations.append(c)
|
|
||||||
|
|
||||||
return ConversationalAnswer(
|
|
||||||
answer=result.output.answer,
|
|
||||||
citations=unique_citations,
|
|
||||||
confidence=result.output.confidence,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Build graph edges: iterative loop
|
# Build graph edges: iterative loop
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -111,18 +111,6 @@ def resolve_citations(
|
||||||
return citations
|
return citations
|
||||||
|
|
||||||
|
|
||||||
class ConversationalAnswer(BaseModel):
|
|
||||||
"""Conversational answer for chat context."""
|
|
||||||
|
|
||||||
answer: str = Field(description="Direct answer to the question")
|
|
||||||
citations: list[Citation] = Field(
|
|
||||||
default_factory=list, description="Citations supporting the answer"
|
|
||||||
)
|
|
||||||
confidence: float = Field(
|
|
||||||
default=1.0, description="Confidence score (0-1)", ge=0.0, le=1.0
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ResearchReport(BaseModel):
|
class ResearchReport(BaseModel):
|
||||||
"""Final research report structure."""
|
"""Final research report structure."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -115,21 +115,3 @@ Style:
|
||||||
- Be professional, objective, and specific.
|
- Be professional, objective, and specific.
|
||||||
- NEVER use meta-commentary like "This report covers..." or "The findings show...".
|
- NEVER use meta-commentary like "This report covers..." or "The findings show...".
|
||||||
Instead, state the actual information directly."""
|
Instead, state the actual information directly."""
|
||||||
|
|
||||||
CONVERSATIONAL_SYNTHESIS_PROMPT = """Generate a direct, conversational answer
|
|
||||||
to the question based on the gathered evidence.
|
|
||||||
|
|
||||||
Output:
|
|
||||||
- answer: Direct, comprehensive answer with a natural, helpful tone.
|
|
||||||
Write the actual answer, not a description of what you found.
|
|
||||||
Use as many sentences as needed to fully address the question.
|
|
||||||
- confidence: Score from 0.0 to 1.0 indicating answer quality.
|
|
||||||
|
|
||||||
Guidelines:
|
|
||||||
- Base your answer solely on the evidence provided in the context.
|
|
||||||
- If a <background> section is provided, use it to frame your answer appropriately.
|
|
||||||
- Be thorough - include all relevant information from the evidence.
|
|
||||||
- Use formatting (bullet points, numbered lists) when it improves clarity.
|
|
||||||
- Do NOT use meta-commentary like "Based on the research..." or "The evidence shows..."
|
|
||||||
Instead, directly state the information.
|
|
||||||
- If the evidence is incomplete, acknowledge limitations briefly."""
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from haiku.rag.client import HaikuRAG
|
from haiku.rag.client import HaikuRAG
|
||||||
from haiku.rag.config import get_config
|
from haiku.rag.config import get_config
|
||||||
from haiku.rag.skills.rag import RAGState
|
from haiku.rag.skills.rag import AGENT_PREAMBLE, RAGState
|
||||||
from haiku.skills.agent import SkillToolset
|
from haiku.skills.agent import SkillToolset
|
||||||
from haiku.skills.models import Skill
|
from haiku.skills.models import Skill
|
||||||
|
|
||||||
|
|
@ -55,16 +55,6 @@ except ImportError: # pragma: no cover
|
||||||
|
|
||||||
RAG_STATE_NAMESPACE = "rag"
|
RAG_STATE_NAMESPACE = "rag"
|
||||||
|
|
||||||
AGENT_PREAMBLE = """You are a helpful research assistant powered by haiku.rag, a knowledge base system.
|
|
||||||
|
|
||||||
CRITICAL RULES:
|
|
||||||
1. For greetings or casual chat: respond directly WITHOUT using any tools
|
|
||||||
2. NEVER make up information - always use tools to get facts from the knowledge base
|
|
||||||
3. For questions: Use the "ask" tool - it handles search and citation automatically
|
|
||||||
4. For searches: Use the "search" tool - copy the ENTIRE tool response to your output INCLUDING content snippets
|
|
||||||
5. When you use the "ask" tool, summarize the key findings and always include citations in your response
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class ChatApp(App):
|
class ChatApp(App):
|
||||||
"""Textual TUI for conversational RAG."""
|
"""Textual TUI for conversational RAG."""
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Literal, overload
|
from typing import TYPE_CHECKING, overload
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
@ -33,7 +33,6 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
from haiku.rag.agents.research.models import (
|
from haiku.rag.agents.research.models import (
|
||||||
Citation,
|
Citation,
|
||||||
ConversationalAnswer,
|
|
||||||
ResearchReport,
|
ResearchReport,
|
||||||
)
|
)
|
||||||
from haiku.rag.agents.rlm.models import RLMResult
|
from haiku.rag.agents.rlm.models import RLMResult
|
||||||
|
|
@ -1328,50 +1327,28 @@ class HaikuRAG:
|
||||||
qa_agent = get_qa_agent(self, config=self._config, system_prompt=system_prompt)
|
qa_agent = get_qa_agent(self, config=self._config, system_prompt=system_prompt)
|
||||||
return await qa_agent.answer(question, filter=filter)
|
return await qa_agent.answer(question, filter=filter)
|
||||||
|
|
||||||
@overload
|
|
||||||
async def research(
|
async def research(
|
||||||
self,
|
self,
|
||||||
question: str,
|
question: str,
|
||||||
*,
|
*,
|
||||||
output_mode: Literal["report"] = ...,
|
|
||||||
filter: str | None = ...,
|
|
||||||
max_iterations: int | None = ...,
|
|
||||||
) -> "ResearchReport": ...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
async def research(
|
|
||||||
self,
|
|
||||||
question: str,
|
|
||||||
*,
|
|
||||||
output_mode: Literal["conversational"],
|
|
||||||
filter: str | None = ...,
|
|
||||||
max_iterations: int | None = ...,
|
|
||||||
) -> "ConversationalAnswer": ...
|
|
||||||
|
|
||||||
async def research(
|
|
||||||
self,
|
|
||||||
question: str,
|
|
||||||
*,
|
|
||||||
output_mode: Literal["report", "conversational"] = "report",
|
|
||||||
filter: str | None = None,
|
filter: str | None = None,
|
||||||
max_iterations: int | None = None,
|
max_iterations: int | None = None,
|
||||||
) -> "ResearchReport | ConversationalAnswer":
|
) -> "ResearchReport":
|
||||||
"""Run multi-agent research to investigate a question.
|
"""Run multi-agent research to investigate a question.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
question: The research question to investigate.
|
question: The research question to investigate.
|
||||||
output_mode: "report" for ResearchReport, "conversational" for ConversationalAnswer.
|
|
||||||
filter: SQL WHERE clause to filter documents.
|
filter: SQL WHERE clause to filter documents.
|
||||||
max_iterations: Override max iterations (None uses config default).
|
max_iterations: Override max iterations (None uses config default).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ResearchReport or ConversationalAnswer based on output_mode.
|
ResearchReport with structured findings.
|
||||||
"""
|
"""
|
||||||
from haiku.rag.agents.research.dependencies import ResearchContext
|
from haiku.rag.agents.research.dependencies import ResearchContext
|
||||||
from haiku.rag.agents.research.graph import build_research_graph
|
from haiku.rag.agents.research.graph import build_research_graph
|
||||||
from haiku.rag.agents.research.state import ResearchDeps, ResearchState
|
from haiku.rag.agents.research.state import ResearchDeps, ResearchState
|
||||||
|
|
||||||
graph = build_research_graph(config=self._config, output_mode=output_mode)
|
graph = build_research_graph(config=self._config)
|
||||||
context = ResearchContext(original_question=question)
|
context = ResearchContext(original_question=question)
|
||||||
state = ResearchState.from_config(
|
state = ResearchState.from_config(
|
||||||
context=context, config=self._config, max_iterations=max_iterations
|
context=context, config=self._config, max_iterations=max_iterations
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,16 @@ from haiku.skills.models import Skill, SkillSource
|
||||||
from haiku.skills.parser import parse_skill_md
|
from haiku.skills.parser import parse_skill_md
|
||||||
from haiku.skills.state import SkillRunDeps
|
from haiku.skills.state import SkillRunDeps
|
||||||
|
|
||||||
|
AGENT_PREAMBLE = """You are a helpful research assistant powered by haiku.rag, a knowledge base system.
|
||||||
|
|
||||||
|
CRITICAL RULES:
|
||||||
|
1. For greetings or casual chat: respond directly WITHOUT using any tools
|
||||||
|
2. NEVER make up information - always use tools to get facts from the knowledge base
|
||||||
|
3. For questions: Use the "ask" tool - it handles search and citation automatically
|
||||||
|
4. For searches: Use the "search" tool - copy the ENTIRE tool response to your output INCLUDING content snippets
|
||||||
|
5. When you use the "ask" tool, summarize the key findings and always include citations in your response
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ResearchEntry(BaseModel):
|
class ResearchEntry(BaseModel):
|
||||||
question: str
|
question: str
|
||||||
|
|
|
||||||
|
|
@ -68,64 +68,15 @@ def test_iterative_plan_result_model():
|
||||||
assert continue_result.next_question == "What are the specific requirements?"
|
assert continue_result.next_question == "What are the specific requirements?"
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
def test_build_research_graph_returns_graph():
|
||||||
# Conversational Graph Tests
|
"""Test build_research_graph returns a valid Graph instance."""
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
|
|
||||||
def test_build_research_graph_conversational_mode_returns_graph():
|
|
||||||
"""Test build_research_graph with output_mode='conversational' returns a valid Graph instance."""
|
|
||||||
from pydantic_graph.beta import Graph
|
from pydantic_graph.beta import Graph
|
||||||
|
|
||||||
graph = build_research_graph(output_mode="conversational")
|
graph = build_research_graph()
|
||||||
assert graph is not None
|
assert graph is not None
|
||||||
assert isinstance(graph, Graph)
|
assert isinstance(graph, Graph)
|
||||||
|
|
||||||
|
|
||||||
def test_build_research_graph_report_mode_returns_graph():
|
|
||||||
"""Test build_research_graph with output_mode='report' returns a valid Graph instance."""
|
|
||||||
from pydantic_graph.beta import Graph
|
|
||||||
|
|
||||||
graph = build_research_graph(output_mode="report")
|
|
||||||
assert graph is not None
|
|
||||||
assert isinstance(graph, Graph)
|
|
||||||
|
|
||||||
|
|
||||||
def test_conversational_answer_model():
|
|
||||||
"""Test ConversationalAnswer model can be created with all fields."""
|
|
||||||
from haiku.rag.agents.research.models import Citation, ConversationalAnswer
|
|
||||||
|
|
||||||
citation = Citation(
|
|
||||||
index=1,
|
|
||||||
document_id="doc-1",
|
|
||||||
chunk_id="chunk-1",
|
|
||||||
document_uri="test.md",
|
|
||||||
document_title="Test Doc",
|
|
||||||
content="Test content",
|
|
||||||
)
|
|
||||||
|
|
||||||
answer = ConversationalAnswer(
|
|
||||||
answer="The answer is 42.",
|
|
||||||
citations=[citation],
|
|
||||||
confidence=0.95,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert answer.answer == "The answer is 42."
|
|
||||||
assert len(answer.citations) == 1
|
|
||||||
assert answer.confidence == 0.95
|
|
||||||
|
|
||||||
|
|
||||||
def test_conversational_answer_default_values():
|
|
||||||
"""Test ConversationalAnswer uses correct default values."""
|
|
||||||
from haiku.rag.agents.research.models import ConversationalAnswer
|
|
||||||
|
|
||||||
answer = ConversationalAnswer(answer="Just the answer.")
|
|
||||||
|
|
||||||
assert answer.answer == "Just the answer."
|
|
||||||
assert answer.citations == []
|
|
||||||
assert answer.confidence == 1.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_format_context_for_prompt_basic():
|
def test_format_context_for_prompt_basic():
|
||||||
"""Test format_context_for_prompt with basic context."""
|
"""Test format_context_for_prompt with basic context."""
|
||||||
from haiku.rag.agents.research.dependencies import ResearchContext
|
from haiku.rag.agents.research.dependencies import ResearchContext
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from haiku.rag.agents.research.models import ConversationalAnswer, ResearchReport
|
from haiku.rag.agents.research.models import ResearchReport
|
||||||
from haiku.rag.client import HaikuRAG
|
from haiku.rag.client import HaikuRAG
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -32,9 +32,6 @@ async def test_client_research_report(temp_db_path):
|
||||||
|
|
||||||
assert result is mock_report
|
assert result is mock_report
|
||||||
mock_build.assert_called_once()
|
mock_build.assert_called_once()
|
||||||
# Verify output_mode passed correctly
|
|
||||||
_, kwargs = mock_build.call_args
|
|
||||||
assert kwargs["output_mode"] == "report"
|
|
||||||
|
|
||||||
# Verify graph.run was called with correct state/deps
|
# Verify graph.run was called with correct state/deps
|
||||||
mock_graph.run.assert_called_once()
|
mock_graph.run.assert_called_once()
|
||||||
|
|
@ -43,29 +40,6 @@ async def test_client_research_report(temp_db_path):
|
||||||
assert isinstance(call_kwargs["deps"].client, HaikuRAG)
|
assert isinstance(call_kwargs["deps"].client, HaikuRAG)
|
||||||
|
|
||||||
|
|
||||||
async def test_client_research_conversational(temp_db_path):
|
|
||||||
"""Test client.research() with conversational output mode."""
|
|
||||||
mock_answer = ConversationalAnswer(
|
|
||||||
answer="The answer is 42.",
|
|
||||||
confidence=0.95,
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch("haiku.rag.agents.research.graph.build_research_graph") as mock_build:
|
|
||||||
mock_graph = AsyncMock()
|
|
||||||
mock_graph.run = AsyncMock(return_value=mock_answer)
|
|
||||||
mock_build.return_value = mock_graph
|
|
||||||
|
|
||||||
async with HaikuRAG(temp_db_path, create=True) as client:
|
|
||||||
result = await client.research(
|
|
||||||
question="What is X?",
|
|
||||||
output_mode="conversational",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result is mock_answer
|
|
||||||
_, kwargs = mock_build.call_args
|
|
||||||
assert kwargs["output_mode"] == "conversational"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_client_research_passes_filter(temp_db_path):
|
async def test_client_research_passes_filter(temp_db_path):
|
||||||
"""Test client.research() passes filter to state."""
|
"""Test client.research() passes filter to state."""
|
||||||
mock_report = ResearchReport(
|
mock_report = ResearchReport(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue