Merge pull request #60 from ggozad/feat/research-summary

Provide pre-search summary for research orchestrator
This commit is contained in:
Yiorgis Gozadinos 2025-09-17 18:36:10 +03:00 committed by GitHub
commit b49543f235
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 110 additions and 15 deletions

View file

@ -43,6 +43,8 @@ The research workflow coordinates specialized agents to plan, search, analyze, a
Components:
- 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 subquestions
- Analysis & Evaluation: Extracts insights, identifies gaps, proposes new questions
- Synthesis: Produces a final structured research report

View file

@ -122,12 +122,7 @@ class HaikuRAGApp:
self.console.print(f"{finding}")
self.console.print()
# Themes
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()
# (Themes section removed)
# Conclusions
if report.conclusions:
@ -261,7 +256,7 @@ class HaikuRAGApp:
elif transport == "sse":
await server.run_sse_async()
else:
await server.run_http_async("streamable-http")
await server.run_http_async(transport="streamable-http")
except KeyboardInterrupt:
pass
finally:

View file

@ -49,6 +49,9 @@ class QuestionAnswerAgent:
limit: int = 3,
) -> list[SearchResult]:
"""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)
expanded_results = await ctx.deps.client.expand_context(search_results)

View file

@ -12,6 +12,7 @@ from haiku.rag.research.evaluation_agent import (
EvaluationResult,
)
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.synthesis_agent import ResearchReport, SynthesisAgent
@ -25,6 +26,7 @@ __all__ = [
# Specialized agents
"SearchAnswer",
"SearchSpecialistAgent",
"PresearchSurveyAgent",
"AnalysisEvaluationAgent",
"EvaluationResult",
"SynthesisAgent",

View file

@ -33,10 +33,18 @@ class BaseResearchAgent[T](ABC):
# Import deps type lazily to avoid circular import during module load
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(
model=model_obj,
deps_type=ResearchDependencies,
output_type=ToolOutput(self.output_type, max_retries=3),
output_type=agent_output_type,
system_prompt=self.get_system_prompt(),
)

View file

@ -11,7 +11,9 @@ class EvaluationResult(BaseModel):
description="Main insights extracted from the research so far"
)
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(
description="Confidence level in the completeness of research (0-1)",

View file

@ -12,6 +12,7 @@ from haiku.rag.research.evaluation_agent import (
AnalysisEvaluationAgent,
EvaluationResult,
)
from haiku.rag.research.presearch_agent import PresearchSurveyAgent
from haiku.rag.research.prompts import ORCHESTRATOR_PROMPT
from haiku.rag.research.search_agent import SearchSpecialistAgent
from haiku.rag.research.synthesis_agent import ResearchReport, SynthesisAgent
@ -41,6 +42,9 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
self.search_agent: SearchSpecialistAgent = SearchSpecialistAgent(
provider, model
)
self.presearch_agent: PresearchSurveyAgent = PresearchSurveyAgent(
provider, model
)
self.evaluation_agent: AnalysisEvaluationAgent = AnalysisEvaluationAgent(
provider, model
)
@ -61,7 +65,12 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
"original_question": context.original_question,
"unanswered_questions": context.sub_questions,
"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
],
"insights": context.insights,
@ -99,12 +108,38 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
# Use provided console or create a new one
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
if console:
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(
f"Create a research plan for: {question}", deps=deps
plan_prompt, deps=deps
)
context.sub_questions = plan_result.output.sub_questions

View 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)

View file

@ -114,3 +114,16 @@ Focus on creating a report that provides clear value to the reader by:
- Highlighting the most important findings
- Explaining the implications of the research
- 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).
"""

View file

@ -42,6 +42,7 @@ class SearchSpecialistAgent(BaseResearchAgent[SearchAnswer]):
) -> str:
"""Search the KB and return a concise context pack."""
# Remove quotes from queries as this requires positional indexing in lancedb
# XXX: Investigate how to do that with lancedb
query = query.replace('"', "")
search_results = await ctx.deps.client.search(query, limit=limit)
expanded = await ctx.deps.client.expand_context(search_results)

View file

@ -12,11 +12,12 @@ class ResearchReport(BaseModel):
main_findings: list[str] = Field(
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")
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(
description="Actionable recommendations based on findings"
description="Actionable recommendations based on findings", default=[]
)
sources_summary: str = Field(
description="Summary of sources used and their reliability"

View file

@ -172,7 +172,6 @@ class TestResearchOrchestrator:
assert report.title
assert report.executive_summary
assert isinstance(report.main_findings, list)
assert isinstance(report.themes, dict)
assert isinstance(report.conclusions, list)
assert isinstance(report.limitations, list)
assert isinstance(report.recommendations, list)