Merge pull request #60 from ggozad/feat/research-summary
Provide pre-search summary for research orchestrator
This commit is contained in:
commit
b49543f235
12 changed files with 110 additions and 15 deletions
|
|
@ -43,6 +43,8 @@ The research workflow coordinates specialized agents to plan, search, analyze, a
|
||||||
Components:
|
Components:
|
||||||
|
|
||||||
- Orchestrator: Plans, coordinates, and loops until confidence is sufficient
|
- Orchestrator: Plans, coordinates, and loops until confidence is sufficient
|
||||||
|
- Presearch Survey: Runs a quick KB scan and summarizes relevant chunk text to
|
||||||
|
ground the initial plan (plain-text summary; no URIs or scores)
|
||||||
- Search Specialist: Performs targeted RAG searches and answers sub‑questions
|
- Search Specialist: Performs targeted RAG searches and answers sub‑questions
|
||||||
- Analysis & Evaluation: Extracts insights, identifies gaps, proposes new questions
|
- Analysis & Evaluation: Extracts insights, identifies gaps, proposes new questions
|
||||||
- Synthesis: Produces a final structured research report
|
- Synthesis: Produces a final structured research report
|
||||||
|
|
|
||||||
|
|
@ -122,12 +122,7 @@ class HaikuRAGApp:
|
||||||
self.console.print(f"• {finding}")
|
self.console.print(f"• {finding}")
|
||||||
self.console.print()
|
self.console.print()
|
||||||
|
|
||||||
# Themes
|
# (Themes section removed)
|
||||||
if report.themes:
|
|
||||||
self.console.print("[bold cyan]Key Themes:[/bold cyan]")
|
|
||||||
for theme, explanation in report.themes.items():
|
|
||||||
self.console.print(f"• [bold]{theme}[/bold]: {explanation}")
|
|
||||||
self.console.print()
|
|
||||||
|
|
||||||
# Conclusions
|
# Conclusions
|
||||||
if report.conclusions:
|
if report.conclusions:
|
||||||
|
|
@ -261,7 +256,7 @@ class HaikuRAGApp:
|
||||||
elif transport == "sse":
|
elif transport == "sse":
|
||||||
await server.run_sse_async()
|
await server.run_sse_async()
|
||||||
else:
|
else:
|
||||||
await server.run_http_async("streamable-http")
|
await server.run_http_async(transport="streamable-http")
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,9 @@ class QuestionAnswerAgent:
|
||||||
limit: int = 3,
|
limit: int = 3,
|
||||||
) -> list[SearchResult]:
|
) -> list[SearchResult]:
|
||||||
"""Search the knowledge base for relevant documents."""
|
"""Search the knowledge base for relevant documents."""
|
||||||
|
|
||||||
|
# Remove quotes from queries as this requires positional indexing in lancedb
|
||||||
|
query = query.replace('"', "")
|
||||||
search_results = await ctx.deps.client.search(query, limit=limit)
|
search_results = await ctx.deps.client.search(query, limit=limit)
|
||||||
expanded_results = await ctx.deps.client.expand_context(search_results)
|
expanded_results = await ctx.deps.client.expand_context(search_results)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from haiku.rag.research.evaluation_agent import (
|
||||||
EvaluationResult,
|
EvaluationResult,
|
||||||
)
|
)
|
||||||
from haiku.rag.research.orchestrator import ResearchOrchestrator, ResearchPlan
|
from haiku.rag.research.orchestrator import ResearchOrchestrator, ResearchPlan
|
||||||
|
from haiku.rag.research.presearch_agent import PresearchSurveyAgent
|
||||||
from haiku.rag.research.search_agent import SearchSpecialistAgent
|
from haiku.rag.research.search_agent import SearchSpecialistAgent
|
||||||
from haiku.rag.research.synthesis_agent import ResearchReport, SynthesisAgent
|
from haiku.rag.research.synthesis_agent import ResearchReport, SynthesisAgent
|
||||||
|
|
||||||
|
|
@ -25,6 +26,7 @@ __all__ = [
|
||||||
# Specialized agents
|
# Specialized agents
|
||||||
"SearchAnswer",
|
"SearchAnswer",
|
||||||
"SearchSpecialistAgent",
|
"SearchSpecialistAgent",
|
||||||
|
"PresearchSurveyAgent",
|
||||||
"AnalysisEvaluationAgent",
|
"AnalysisEvaluationAgent",
|
||||||
"EvaluationResult",
|
"EvaluationResult",
|
||||||
"SynthesisAgent",
|
"SynthesisAgent",
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,18 @@ class BaseResearchAgent[T](ABC):
|
||||||
# Import deps type lazily to avoid circular import during module load
|
# Import deps type lazily to avoid circular import during module load
|
||||||
from haiku.rag.research.dependencies import ResearchDependencies
|
from haiku.rag.research.dependencies import ResearchDependencies
|
||||||
|
|
||||||
|
# If the agent is expected to return plain text, pass `str` directly.
|
||||||
|
# Otherwise, wrap the model with ToolOutput for robust tool-handling retries.
|
||||||
|
agent_output_type: Any
|
||||||
|
if self.output_type is str: # plain text output
|
||||||
|
agent_output_type = str
|
||||||
|
else:
|
||||||
|
agent_output_type = ToolOutput(self.output_type, max_retries=3)
|
||||||
|
|
||||||
self._agent = Agent(
|
self._agent = Agent(
|
||||||
model=model_obj,
|
model=model_obj,
|
||||||
deps_type=ResearchDependencies,
|
deps_type=ResearchDependencies,
|
||||||
output_type=ToolOutput(self.output_type, max_retries=3),
|
output_type=agent_output_type,
|
||||||
system_prompt=self.get_system_prompt(),
|
system_prompt=self.get_system_prompt(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ class EvaluationResult(BaseModel):
|
||||||
description="Main insights extracted from the research so far"
|
description="Main insights extracted from the research so far"
|
||||||
)
|
)
|
||||||
new_questions: list[str] = Field(
|
new_questions: list[str] = Field(
|
||||||
description="New sub-questions to add to the research (max 3)", max_length=3
|
description="New sub-questions to add to the research (max 3)",
|
||||||
|
max_length=3,
|
||||||
|
default=[],
|
||||||
)
|
)
|
||||||
confidence_score: float = Field(
|
confidence_score: float = Field(
|
||||||
description="Confidence level in the completeness of research (0-1)",
|
description="Confidence level in the completeness of research (0-1)",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from haiku.rag.research.evaluation_agent import (
|
||||||
AnalysisEvaluationAgent,
|
AnalysisEvaluationAgent,
|
||||||
EvaluationResult,
|
EvaluationResult,
|
||||||
)
|
)
|
||||||
|
from haiku.rag.research.presearch_agent import PresearchSurveyAgent
|
||||||
from haiku.rag.research.prompts import ORCHESTRATOR_PROMPT
|
from haiku.rag.research.prompts import ORCHESTRATOR_PROMPT
|
||||||
from haiku.rag.research.search_agent import SearchSpecialistAgent
|
from haiku.rag.research.search_agent import SearchSpecialistAgent
|
||||||
from haiku.rag.research.synthesis_agent import ResearchReport, SynthesisAgent
|
from haiku.rag.research.synthesis_agent import ResearchReport, SynthesisAgent
|
||||||
|
|
@ -41,6 +42,9 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
|
||||||
self.search_agent: SearchSpecialistAgent = SearchSpecialistAgent(
|
self.search_agent: SearchSpecialistAgent = SearchSpecialistAgent(
|
||||||
provider, model
|
provider, model
|
||||||
)
|
)
|
||||||
|
self.presearch_agent: PresearchSurveyAgent = PresearchSurveyAgent(
|
||||||
|
provider, model
|
||||||
|
)
|
||||||
self.evaluation_agent: AnalysisEvaluationAgent = AnalysisEvaluationAgent(
|
self.evaluation_agent: AnalysisEvaluationAgent = AnalysisEvaluationAgent(
|
||||||
provider, model
|
provider, model
|
||||||
)
|
)
|
||||||
|
|
@ -61,7 +65,12 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
|
||||||
"original_question": context.original_question,
|
"original_question": context.original_question,
|
||||||
"unanswered_questions": context.sub_questions,
|
"unanswered_questions": context.sub_questions,
|
||||||
"qa_responses": [
|
"qa_responses": [
|
||||||
{"question": qa.query, "answer": qa.answer}
|
{
|
||||||
|
"question": qa.query,
|
||||||
|
"answer": qa.answer,
|
||||||
|
"context_snippets": qa.context,
|
||||||
|
"sources": qa.sources,
|
||||||
|
}
|
||||||
for qa in context.qa_responses
|
for qa in context.qa_responses
|
||||||
],
|
],
|
||||||
"insights": context.insights,
|
"insights": context.insights,
|
||||||
|
|
@ -99,12 +108,38 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
|
||||||
# Use provided console or create a new one
|
# Use provided console or create a new one
|
||||||
console = console or Console() if verbose else None
|
console = console or Console() if verbose else None
|
||||||
|
|
||||||
|
# Run a simple presearch survey to summarize KB context
|
||||||
|
if console:
|
||||||
|
console.print(
|
||||||
|
"\n[bold cyan]🔎 Presearch: summarizing KB context...[/bold cyan]"
|
||||||
|
)
|
||||||
|
|
||||||
|
presearch_result = await self.presearch_agent.run(question, deps=deps)
|
||||||
|
|
||||||
# Create initial research plan
|
# Create initial research plan
|
||||||
if console:
|
if console:
|
||||||
console.print("\n[bold cyan]📋 Creating research plan...[/bold cyan]")
|
console.print("\n[bold cyan]📋 Creating research plan...[/bold cyan]")
|
||||||
|
|
||||||
|
# Include the presearch summary to ground the planning step.
|
||||||
|
|
||||||
|
planning_context_xml = format_as_xml(
|
||||||
|
{
|
||||||
|
"original_question": question,
|
||||||
|
"presearch_summary": presearch_result.output or "",
|
||||||
|
},
|
||||||
|
root_tag="planning_context",
|
||||||
|
)
|
||||||
|
|
||||||
|
plan_prompt = (
|
||||||
|
"Create a research plan for the main question below.\n\n"
|
||||||
|
f"Main question: {question}\n\n"
|
||||||
|
"Use this brief presearch summary to inform the plan. Focus the 3 sub-questions "
|
||||||
|
"on the most important aspects not already obvious from the current KB context.\n\n"
|
||||||
|
f"{planning_context_xml}"
|
||||||
|
)
|
||||||
|
|
||||||
plan_result: AgentRunResult[ResearchPlan] = await self.run(
|
plan_result: AgentRunResult[ResearchPlan] = await self.run(
|
||||||
f"Create a research plan for: {question}", deps=deps
|
plan_prompt, deps=deps
|
||||||
)
|
)
|
||||||
|
|
||||||
context.sub_questions = plan_result.output.sub_questions
|
context.sub_questions = plan_result.output.sub_questions
|
||||||
|
|
|
||||||
34
src/haiku/rag/research/presearch_agent.py
Normal file
34
src/haiku/rag/research/presearch_agent.py
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
from pydantic_ai import RunContext
|
||||||
|
from pydantic_ai.run import AgentRunResult
|
||||||
|
|
||||||
|
from haiku.rag.research.base import BaseResearchAgent
|
||||||
|
from haiku.rag.research.dependencies import ResearchDependencies
|
||||||
|
from haiku.rag.research.prompts import PRESEARCH_AGENT_PROMPT
|
||||||
|
|
||||||
|
|
||||||
|
class PresearchSurveyAgent(BaseResearchAgent[str]):
|
||||||
|
"""Presearch agent that gathers verbatim context and summarizes it."""
|
||||||
|
|
||||||
|
def __init__(self, provider: str, model: str) -> None:
|
||||||
|
super().__init__(provider, model, str)
|
||||||
|
|
||||||
|
async def run(
|
||||||
|
self, prompt: str, deps: ResearchDependencies, **kwargs
|
||||||
|
) -> AgentRunResult[str]:
|
||||||
|
return await super().run(prompt, deps, **kwargs)
|
||||||
|
|
||||||
|
def get_system_prompt(self) -> str:
|
||||||
|
return PRESEARCH_AGENT_PROMPT
|
||||||
|
|
||||||
|
def register_tools(self) -> None:
|
||||||
|
@self.agent.tool
|
||||||
|
async def gather_context(
|
||||||
|
ctx: RunContext[ResearchDependencies],
|
||||||
|
query: str,
|
||||||
|
limit: int = 6,
|
||||||
|
) -> str:
|
||||||
|
"""Return verbatim concatenation of relevant chunk texts."""
|
||||||
|
query = query.replace('"', "")
|
||||||
|
results = await ctx.deps.client.search(query, limit=limit)
|
||||||
|
expanded = await ctx.deps.client.expand_context(results)
|
||||||
|
return "\n\n".join(chunk.content for chunk, _ in expanded)
|
||||||
|
|
@ -114,3 +114,16 @@ Focus on creating a report that provides clear value to the reader by:
|
||||||
- Highlighting the most important findings
|
- Highlighting the most important findings
|
||||||
- Explaining the implications of the research
|
- Explaining the implications of the research
|
||||||
- Suggesting concrete next steps"""
|
- Suggesting concrete next steps"""
|
||||||
|
|
||||||
|
PRESEARCH_AGENT_PROMPT = """You are a rapid research surveyor.
|
||||||
|
|
||||||
|
Task:
|
||||||
|
- Call the gather_context tool once with the main question to obtain a
|
||||||
|
relevant texts from the Knowledge Base (KB).
|
||||||
|
- Read that context and produce a brief natural-language summary describing
|
||||||
|
what the KB appears to contain relative to the question.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Base the summary strictly on the provided text; do not invent.
|
||||||
|
- Output only the summary as plain text (one short paragraph).
|
||||||
|
"""
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ class SearchSpecialistAgent(BaseResearchAgent[SearchAnswer]):
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Search the KB and return a concise context pack."""
|
"""Search the KB and return a concise context pack."""
|
||||||
# Remove quotes from queries as this requires positional indexing in lancedb
|
# Remove quotes from queries as this requires positional indexing in lancedb
|
||||||
|
# XXX: Investigate how to do that with lancedb
|
||||||
query = query.replace('"', "")
|
query = query.replace('"', "")
|
||||||
search_results = await ctx.deps.client.search(query, limit=limit)
|
search_results = await ctx.deps.client.search(query, limit=limit)
|
||||||
expanded = await ctx.deps.client.expand_context(search_results)
|
expanded = await ctx.deps.client.expand_context(search_results)
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,12 @@ class ResearchReport(BaseModel):
|
||||||
main_findings: list[str] = Field(
|
main_findings: list[str] = Field(
|
||||||
description="Primary research findings with supporting evidence"
|
description="Primary research findings with supporting evidence"
|
||||||
)
|
)
|
||||||
themes: dict[str, str] = Field(description="Major themes and their explanations")
|
|
||||||
conclusions: list[str] = Field(description="Evidence-based conclusions")
|
conclusions: list[str] = Field(description="Evidence-based conclusions")
|
||||||
limitations: list[str] = Field(description="Limitations of the current research")
|
limitations: list[str] = Field(
|
||||||
|
description="Limitations of the current research", default=[]
|
||||||
|
)
|
||||||
recommendations: list[str] = Field(
|
recommendations: list[str] = Field(
|
||||||
description="Actionable recommendations based on findings"
|
description="Actionable recommendations based on findings", default=[]
|
||||||
)
|
)
|
||||||
sources_summary: str = Field(
|
sources_summary: str = Field(
|
||||||
description="Summary of sources used and their reliability"
|
description="Summary of sources used and their reliability"
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,6 @@ class TestResearchOrchestrator:
|
||||||
assert report.title
|
assert report.title
|
||||||
assert report.executive_summary
|
assert report.executive_summary
|
||||||
assert isinstance(report.main_findings, list)
|
assert isinstance(report.main_findings, list)
|
||||||
assert isinstance(report.themes, dict)
|
|
||||||
assert isinstance(report.conclusions, list)
|
assert isinstance(report.conclusions, list)
|
||||||
assert isinstance(report.limitations, list)
|
assert isinstance(report.limitations, list)
|
||||||
assert isinstance(report.recommendations, list)
|
assert isinstance(report.recommendations, list)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue