haiku.rag 0.10.2__py3-none-any.whl → 0.11.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of haiku.rag might be problematic. Click here for more details.

@@ -0,0 +1,181 @@
1
+ from dataclasses import dataclass
2
+
3
+ from pydantic_ai import Agent
4
+ from pydantic_graph import BaseNode, GraphRunContext
5
+
6
+ from haiku.rag.research.common import (
7
+ format_analysis_for_prompt,
8
+ format_context_for_prompt,
9
+ get_model,
10
+ log,
11
+ )
12
+ from haiku.rag.research.dependencies import ResearchDependencies
13
+ from haiku.rag.research.models import EvaluationResult, InsightAnalysis, ResearchReport
14
+ from haiku.rag.research.nodes.synthesize import SynthesizeNode
15
+ from haiku.rag.research.prompts import DECISION_AGENT_PROMPT, INSIGHT_AGENT_PROMPT
16
+ from haiku.rag.research.state import ResearchDeps, ResearchState
17
+
18
+
19
+ @dataclass
20
+ class AnalyzeInsightsNode(BaseNode[ResearchState, ResearchDeps, ResearchReport]):
21
+ provider: str
22
+ model: str
23
+
24
+ async def run(
25
+ self, ctx: GraphRunContext[ResearchState, ResearchDeps]
26
+ ) -> BaseNode[ResearchState, ResearchDeps, ResearchReport]:
27
+ state = ctx.state
28
+ deps = ctx.deps
29
+
30
+ log(
31
+ deps,
32
+ state,
33
+ "\n[bold cyan]🧭 Synthesizing new insights and gap status...[/bold cyan]",
34
+ )
35
+
36
+ agent = Agent(
37
+ model=get_model(self.provider, self.model),
38
+ output_type=InsightAnalysis,
39
+ instructions=INSIGHT_AGENT_PROMPT,
40
+ retries=3,
41
+ deps_type=ResearchDependencies,
42
+ )
43
+
44
+ context_xml = format_context_for_prompt(state.context)
45
+ prompt = (
46
+ "Review the latest research context and update the shared ledger of insights, gaps,"
47
+ " and follow-up questions.\n\n"
48
+ f"{context_xml}"
49
+ )
50
+ agent_deps = ResearchDependencies(
51
+ client=deps.client,
52
+ context=state.context,
53
+ console=deps.console,
54
+ stream=deps.stream,
55
+ )
56
+ result = await agent.run(prompt, deps=agent_deps)
57
+ analysis: InsightAnalysis = result.output
58
+
59
+ state.context.integrate_analysis(analysis)
60
+ state.last_analysis = analysis
61
+
62
+ if analysis.commentary:
63
+ log(deps, state, f" Summary: {analysis.commentary}")
64
+ if analysis.highlights:
65
+ log(deps, state, " [bold]Updated insights:[/bold]")
66
+ for insight in analysis.highlights:
67
+ label = insight.status.value
68
+ log(
69
+ deps,
70
+ state,
71
+ f" • ({label}) {insight.summary}",
72
+ )
73
+ if analysis.gap_assessments:
74
+ log(deps, state, " [bold yellow]Gap updates:[/bold yellow]")
75
+ for gap in analysis.gap_assessments:
76
+ status = "resolved" if gap.resolved else "open"
77
+ severity = gap.severity.value
78
+ log(
79
+ deps,
80
+ state,
81
+ f" • ({severity}/{status}) {gap.description}",
82
+ )
83
+ if analysis.resolved_gaps:
84
+ log(deps, state, " [green]Resolved gaps:[/green]")
85
+ for resolved in analysis.resolved_gaps:
86
+ log(deps, state, f" • {resolved}")
87
+ if analysis.new_questions:
88
+ log(deps, state, " [cyan]Proposed follow-ups:[/cyan]")
89
+ for question in analysis.new_questions:
90
+ log(deps, state, f" • {question}")
91
+
92
+ return DecisionNode(self.provider, self.model)
93
+
94
+
95
+ @dataclass
96
+ class DecisionNode(BaseNode[ResearchState, ResearchDeps, ResearchReport]):
97
+ provider: str
98
+ model: str
99
+
100
+ async def run(
101
+ self, ctx: GraphRunContext[ResearchState, ResearchDeps]
102
+ ) -> BaseNode[ResearchState, ResearchDeps, ResearchReport]:
103
+ state = ctx.state
104
+ deps = ctx.deps
105
+
106
+ log(
107
+ deps,
108
+ state,
109
+ "\n[bold cyan]📊 Evaluating research sufficiency...[/bold cyan]",
110
+ )
111
+
112
+ agent = Agent(
113
+ model=get_model(self.provider, self.model),
114
+ output_type=EvaluationResult,
115
+ instructions=DECISION_AGENT_PROMPT,
116
+ retries=3,
117
+ deps_type=ResearchDependencies,
118
+ )
119
+
120
+ context_xml = format_context_for_prompt(state.context)
121
+ analysis_xml = format_analysis_for_prompt(state.last_analysis)
122
+ prompt_parts = [
123
+ "Assess whether the research now answers the original question with adequate confidence.",
124
+ context_xml,
125
+ analysis_xml,
126
+ ]
127
+ if state.last_eval is not None:
128
+ prev = state.last_eval
129
+ prompt_parts.append(
130
+ "<previous_evaluation>"
131
+ f"<confidence>{prev.confidence_score:.2f}</confidence>"
132
+ f"<is_sufficient>{str(prev.is_sufficient).lower()}</is_sufficient>"
133
+ f"<reasoning>{prev.reasoning}</reasoning>"
134
+ "</previous_evaluation>"
135
+ )
136
+ prompt = "\n\n".join(part for part in prompt_parts if part)
137
+
138
+ agent_deps = ResearchDependencies(
139
+ client=deps.client,
140
+ context=state.context,
141
+ console=deps.console,
142
+ stream=deps.stream,
143
+ )
144
+ decision_result = await agent.run(prompt, deps=agent_deps)
145
+ output = decision_result.output
146
+
147
+ state.last_eval = output
148
+ state.iterations += 1
149
+
150
+ for new_q in output.new_questions:
151
+ if new_q not in state.context.sub_questions:
152
+ state.context.sub_questions.append(new_q)
153
+
154
+ if output.key_insights:
155
+ log(deps, state, " [bold]Key insights:[/bold]")
156
+ for insight in output.key_insights:
157
+ log(deps, state, f" • {insight}")
158
+
159
+ if output.gaps:
160
+ log(deps, state, " [bold yellow]Remaining gaps:[/bold yellow]")
161
+ for gap in output.gaps:
162
+ log(deps, state, f" • {gap}")
163
+
164
+ log(
165
+ deps,
166
+ state,
167
+ f" Confidence: [yellow]{output.confidence_score:.1%}[/yellow]",
168
+ )
169
+ status = "[green]Yes[/green]" if output.is_sufficient else "[red]No[/red]"
170
+ log(deps, state, f" Sufficient: {status}")
171
+
172
+ from haiku.rag.research.nodes.search import SearchDispatchNode
173
+
174
+ if (
175
+ output.is_sufficient
176
+ and output.confidence_score >= state.confidence_threshold
177
+ ) or state.iterations >= state.max_iterations:
178
+ log(deps, state, "\n[bold green]✅ Stopping research.[/bold green]")
179
+ return SynthesizeNode(self.provider, self.model)
180
+
181
+ return SearchDispatchNode(self.provider, self.model)
@@ -22,7 +22,7 @@ class PlanNode(BaseNode[ResearchState, ResearchDeps, ResearchReport]):
22
22
  state = ctx.state
23
23
  deps = ctx.deps
24
24
 
25
- log(deps.console, "\n[bold cyan]📋 Creating research plan...[/bold cyan]")
25
+ log(deps, state, "\n[bold cyan]📋 Creating research plan...[/bold cyan]")
26
26
 
27
27
  plan_agent = Agent(
28
28
  model=get_model(self.provider, self.model),
@@ -45,19 +45,26 @@ class PlanNode(BaseNode[ResearchState, ResearchDeps, ResearchReport]):
45
45
 
46
46
  prompt = (
47
47
  "Plan a focused research approach for the main question.\n\n"
48
- f"Main question: {state.question}"
48
+ f"Main question: {state.context.original_question}"
49
49
  )
50
50
 
51
51
  agent_deps = ResearchDependencies(
52
- client=deps.client, context=state.context, console=deps.console
52
+ client=deps.client,
53
+ context=state.context,
54
+ console=deps.console,
55
+ stream=deps.stream,
53
56
  )
54
57
  plan_result = await plan_agent.run(prompt, deps=agent_deps)
55
- state.sub_questions = list(plan_result.output.sub_questions)
58
+ state.context.sub_questions = list(plan_result.output.sub_questions)
56
59
 
57
- log(deps.console, "\n[bold green]✅ Research Plan Created:[/bold green]")
58
- log(deps.console, f" [bold]Main Question:[/bold] {state.question}")
59
- log(deps.console, " [bold]Sub-questions:[/bold]")
60
- for i, sq in enumerate(state.sub_questions, 1):
61
- log(deps.console, f" {i}. {sq}")
60
+ log(deps, state, "\n[bold green]✅ Research Plan Created:[/bold green]")
61
+ log(
62
+ deps,
63
+ state,
64
+ f" [bold]Main Question:[/bold] {state.context.original_question}",
65
+ )
66
+ log(deps, state, " [bold]Sub-questions:[/bold]")
67
+ for i, sq in enumerate(state.context.sub_questions, 1):
68
+ log(deps, state, f" {i}. {sq}")
62
69
 
63
70
  return SearchDispatchNode(self.provider, self.model)
@@ -24,20 +24,21 @@ class SearchDispatchNode(BaseNode[ResearchState, ResearchDeps, ResearchReport]):
24
24
  ) -> BaseNode[ResearchState, ResearchDeps, ResearchReport]:
25
25
  state = ctx.state
26
26
  deps = ctx.deps
27
- if not state.sub_questions:
28
- from haiku.rag.research.nodes.evaluate import EvaluateNode
27
+ if not state.context.sub_questions:
28
+ from haiku.rag.research.nodes.analysis import AnalyzeInsightsNode
29
29
 
30
- return EvaluateNode(self.provider, self.model)
30
+ return AnalyzeInsightsNode(self.provider, self.model)
31
31
 
32
32
  # Take up to max_concurrency questions and answer them concurrently
33
33
  take = max(1, state.max_concurrency)
34
34
  batch: list[str] = []
35
- while state.sub_questions and len(batch) < take:
36
- batch.append(state.sub_questions.pop(0))
35
+ while state.context.sub_questions and len(batch) < take:
36
+ batch.append(state.context.sub_questions.pop(0))
37
37
 
38
38
  async def answer_one(sub_q: str) -> SearchAnswer | None:
39
39
  log(
40
- deps.console,
40
+ deps,
41
+ state,
41
42
  f"\n[bold cyan]🔍 Searching & Answering:[/bold cyan] {sub_q}",
42
43
  )
43
44
  agent = Agent(
@@ -71,12 +72,15 @@ class SearchDispatchNode(BaseNode[ResearchState, ResearchDeps, ResearchReport]):
71
72
  return format_as_xml(entries, root_tag="snippets")
72
73
 
73
74
  agent_deps = ResearchDependencies(
74
- client=deps.client, context=state.context, console=deps.console
75
+ client=deps.client,
76
+ context=state.context,
77
+ console=deps.console,
78
+ stream=deps.stream,
75
79
  )
76
80
  try:
77
81
  result = await agent.run(sub_q, deps=agent_deps)
78
82
  except Exception as e:
79
- log(deps.console, f"[red]Search failed:[/red] {e}")
83
+ log(deps, state, f"[red]Search failed:[/red] {e}")
80
84
  return None
81
85
 
82
86
  return result.output
@@ -86,8 +90,7 @@ class SearchDispatchNode(BaseNode[ResearchState, ResearchDeps, ResearchReport]):
86
90
  if ans is None:
87
91
  continue
88
92
  state.context.add_qa_response(ans)
89
- if deps.console:
90
- preview = ans.answer[:150] + ("…" if len(ans.answer) > 150 else "")
91
- log(deps.console, f" [green]✓[/green] {preview}")
93
+ preview = ans.answer[:150] + ("…" if len(ans.answer) > 150 else "")
94
+ log(deps, state, f" [green]✓[/green] {preview}")
92
95
 
93
96
  return SearchDispatchNode(self.provider, self.model)
@@ -24,7 +24,8 @@ class SynthesizeNode(BaseNode[ResearchState, ResearchDeps, ResearchReport]):
24
24
  deps = ctx.deps
25
25
 
26
26
  log(
27
- deps.console,
27
+ deps,
28
+ state,
28
29
  "\n[bold cyan]📝 Generating final research report...[/bold cyan]",
29
30
  )
30
31
 
@@ -43,9 +44,12 @@ class SynthesizeNode(BaseNode[ResearchState, ResearchDeps, ResearchReport]):
43
44
  "Create a detailed report that synthesizes all findings into a coherent response."
44
45
  )
45
46
  agent_deps = ResearchDependencies(
46
- client=deps.client, context=state.context, console=deps.console
47
+ client=deps.client,
48
+ context=state.context,
49
+ console=deps.console,
50
+ stream=deps.stream,
47
51
  )
48
52
  result = await agent.run(prompt, deps=agent_deps)
49
53
 
50
- log(deps.console, "[bold green]✅ Research complete![/bold green]")
54
+ log(deps, state, "[bold green]✅ Research complete![/bold green]")
51
55
  return End(result.output)
@@ -44,38 +44,77 @@ Answering rules:
44
44
  - Prefer concise phrasing; avoid copying long passages.
45
45
  - When evidence is partial, state the limits explicitly in the answer."""
46
46
 
47
- EVALUATION_AGENT_PROMPT = """You are an analysis and evaluation specialist for
48
- the research workflow.
47
+ INSIGHT_AGENT_PROMPT = """You are the insight aggregation specialist for the
48
+ research workflow.
49
49
 
50
50
  Inputs available:
51
- - Original research question
52
- - Question–answer pairs produced by search
53
- - Raw search results and source metadata
54
- - Previously identified insights
55
-
56
- ANALYSIS:
57
- 1. Extract the most important, non‑obvious insights from the collected evidence.
58
- 2. Identify patterns, agreements, and disagreements across sources.
59
- 3. Note material uncertainties and assumptions.
60
-
61
- EVALUATION:
62
- 1. Decide if we have sufficient information to answer the original question.
63
- 2. Provide a confidence_score in [0,1] considering:
64
- - Coverage of the main question’s aspects
65
- - Quality, consistency, and diversity of sources
66
- - Depth and specificity of evidence
67
- 3. List concrete gaps that still need investigation.
68
- 4. Propose up to 3 new sub_questions that would close the highest‑value gaps.
51
+ - Original research question and sub-questions
52
+ - Question–answer pairs with supporting snippets and sources
53
+ - Existing insights and gaps (with status metadata)
54
+
55
+ Tasks:
56
+ 1. Extract new or refined insights that advance understanding of the question.
57
+ 2. Update gap status, creating new gap entries when necessary and marking
58
+ resolved ones explicitly.
59
+ 3. Suggest up to 3 high-impact follow-up sub_questions that would close the
60
+ most important remaining gaps.
61
+
62
+ Output format (map directly to fields):
63
+ - highlights: list of insights with fields {summary, status, supporting_sources,
64
+ originating_questions, notes}. Use status one of {validated, open, tentative}.
65
+ - gap_assessments: list of gaps with fields {description, severity, blocking,
66
+ resolved, resolved_by, supporting_sources, notes}. Severity must be one of
67
+ {low, medium, high}. resolved_by may reference related insight summaries if no
68
+ stable identifier yet.
69
+ - resolved_gaps: list of identifiers or descriptions for gaps now closed.
70
+ - new_questions: up to 3 standalone, specific sub-questions (no duplicates with
71
+ existing ones).
72
+ - commentary: 1–3 sentences summarizing what changed this round.
73
+
74
+ Guidance:
75
+ - Be concise and avoid repeating previously recorded information unless it
76
+ changed materially.
77
+ - Tie supporting_sources to the evidence used; omit if unavailable.
78
+ - Only propose new sub_questions that directly address remaining gaps.
79
+ - When marking a gap as resolved, ensure the rationale is clear via
80
+ resolved_by or notes."""
81
+
82
+ DECISION_AGENT_PROMPT = """You are the research governor responsible for making
83
+ stop/go decisions.
84
+
85
+ Inputs available:
86
+ - Original research question and current plan
87
+ - Full insight ledger with status metadata
88
+ - Up-to-date gap tracker, including resolved indicators
89
+ - Latest insight analysis summary (highlights, gap changes, new questions)
90
+ - Previous evaluation decision (if any)
91
+
92
+ Tasks:
93
+ 1. Determine whether the collected evidence now answers the original question.
94
+ 2. Provide a confidence_score in [0,1] that reflects coverage, evidence quality,
95
+ and agreement across sources.
96
+ 3. List the highest-priority gaps that still block a confident answer. Reference
97
+ existing gap descriptions rather than inventing new ones.
98
+ 4. Optionally propose up to 3 new sub_questions only if they are not already in
99
+ the current backlog.
69
100
 
70
101
  Strictness:
71
- - Only mark research as sufficient when all major aspects are addressed with
72
- consistent, reliable evidence and no critical gaps remain.
73
-
74
- New sub_questions must:
75
- - Be genuinely new (not answered or duplicative; check qa_responses).
76
- - Be standalone and specific (entities, scope, timeframe/region if relevant).
77
- - Be actionable and scoped to the knowledge base (narrow if necessary).
78
- - Be ordered by expected impact (most valuable first)."""
102
+ - Only mark research as sufficient when every critical aspect of the main
103
+ question is addressed with reliable, corroborated evidence.
104
+ - Treat unresolved high-severity or blocking gaps as a hard stop.
105
+
106
+ Output fields must line up with EvaluationResult:
107
+ - key_insights: concise bullet-ready statements of the most decision-relevant
108
+ insights (cite status if helpful).
109
+ - new_questions: follow-up sub-questions (max 3) meeting the specificity rules.
110
+ - gaps: list remaining blockers; reuse wording from the tracked gaps when
111
+ possible to aid downstream reconciliation.
112
+ - confidence_score: numeric in [0,1].
113
+ - is_sufficient: true only when no blocking gaps remain.
114
+ - reasoning: short narrative tying the decision to evidence coverage.
115
+
116
+ Remember: prefer maintaining continuity with the structured context over
117
+ introducing new terminology."""
79
118
 
80
119
  SYNTHESIS_AGENT_PROMPT = """You are a synthesis specialist producing the final
81
120
  research report.
@@ -1,25 +1,32 @@
1
- from dataclasses import dataclass, field
1
+ from dataclasses import dataclass
2
2
 
3
3
  from rich.console import Console
4
4
 
5
5
  from haiku.rag.client import HaikuRAG
6
6
  from haiku.rag.research.dependencies import ResearchContext
7
- from haiku.rag.research.models import EvaluationResult
7
+ from haiku.rag.research.models import EvaluationResult, InsightAnalysis
8
+ from haiku.rag.research.stream import ResearchStream
8
9
 
9
10
 
10
11
  @dataclass
11
12
  class ResearchDeps:
12
13
  client: HaikuRAG
13
14
  console: Console | None = None
15
+ stream: ResearchStream | None = None
16
+
17
+ def emit_log(self, message: str, state: "ResearchState | None" = None) -> None:
18
+ if self.console:
19
+ self.console.print(message)
20
+ if self.stream:
21
+ self.stream.log(message, state)
14
22
 
15
23
 
16
24
  @dataclass
17
25
  class ResearchState:
18
- question: str
19
26
  context: ResearchContext
20
- sub_questions: list[str] = field(default_factory=list)
21
27
  iterations: int = 0
22
28
  max_iterations: int = 3
23
29
  max_concurrency: int = 1
24
30
  confidence_threshold: float = 0.8
25
31
  last_eval: EvaluationResult | None = None
32
+ last_analysis: InsightAnalysis | None = None
@@ -0,0 +1,177 @@
1
+ import asyncio
2
+ from collections.abc import AsyncIterator
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Literal
5
+
6
+ from haiku.rag.research.models import ResearchReport
7
+
8
+ if TYPE_CHECKING: # pragma: no cover
9
+ from haiku.rag.research.state import ResearchState
10
+
11
+
12
+ @dataclass(slots=True)
13
+ class ResearchStateSnapshot:
14
+ question: str
15
+ sub_questions: list[str]
16
+ iterations: int
17
+ max_iterations: int
18
+ max_concurrency: int
19
+ confidence_threshold: float
20
+ pending_sub_questions: int
21
+ answered_questions: int
22
+ insights: list[str]
23
+ gaps: list[str]
24
+ last_confidence: float | None
25
+ last_sufficient: bool | None
26
+
27
+ @classmethod
28
+ def from_state(cls, state: "ResearchState") -> "ResearchStateSnapshot":
29
+ context = state.context
30
+ last_confidence: float | None = None
31
+ last_sufficient: bool | None = None
32
+ if state.last_eval:
33
+ last_confidence = state.last_eval.confidence_score
34
+ last_sufficient = state.last_eval.is_sufficient
35
+
36
+ return cls(
37
+ question=context.original_question,
38
+ sub_questions=list(context.sub_questions),
39
+ iterations=state.iterations,
40
+ max_iterations=state.max_iterations,
41
+ max_concurrency=state.max_concurrency,
42
+ confidence_threshold=state.confidence_threshold,
43
+ pending_sub_questions=len(context.sub_questions),
44
+ answered_questions=len(context.qa_responses),
45
+ insights=[
46
+ f"{insight.status.value}:{insight.summary}"
47
+ for insight in context.insights
48
+ ],
49
+ gaps=[
50
+ f"{gap.severity.value}/{'resolved' if gap.resolved else 'open'}:{gap.description}"
51
+ for gap in context.gaps
52
+ ],
53
+ last_confidence=last_confidence,
54
+ last_sufficient=last_sufficient,
55
+ )
56
+
57
+
58
+ @dataclass(slots=True)
59
+ class ResearchStreamEvent:
60
+ type: Literal["log", "report", "error"]
61
+ message: str | None = None
62
+ state: ResearchStateSnapshot | None = None
63
+ report: ResearchReport | None = None
64
+ error: str | None = None
65
+
66
+
67
+ class ResearchStream:
68
+ """Queue-backed stream for research graph events."""
69
+
70
+ def __init__(self) -> None:
71
+ self._queue: asyncio.Queue[ResearchStreamEvent | None] = asyncio.Queue()
72
+ self._closed = False
73
+
74
+ def _snapshot(self, state: "ResearchState | None") -> ResearchStateSnapshot | None:
75
+ if state is None:
76
+ return None
77
+ return ResearchStateSnapshot.from_state(state)
78
+
79
+ def log(self, message: str, state: "ResearchState | None" = None) -> None:
80
+ if self._closed:
81
+ return
82
+ event = ResearchStreamEvent(
83
+ type="log", message=message, state=self._snapshot(state)
84
+ )
85
+ self._queue.put_nowait(event)
86
+
87
+ def report(self, report: ResearchReport, state: "ResearchState") -> None:
88
+ if self._closed:
89
+ return
90
+ event = ResearchStreamEvent(
91
+ type="report",
92
+ report=report,
93
+ state=self._snapshot(state),
94
+ )
95
+ self._queue.put_nowait(event)
96
+
97
+ def error(self, error: Exception, state: "ResearchState | None" = None) -> None:
98
+ if self._closed:
99
+ return
100
+ event = ResearchStreamEvent(
101
+ type="error",
102
+ message=str(error),
103
+ error=str(error),
104
+ state=self._snapshot(state),
105
+ )
106
+ self._queue.put_nowait(event)
107
+
108
+ async def close(self) -> None:
109
+ if self._closed:
110
+ return
111
+ self._closed = True
112
+ await self._queue.put(None)
113
+
114
+ def __aiter__(self) -> AsyncIterator[ResearchStreamEvent]:
115
+ return self._iter_events()
116
+
117
+ async def _iter_events(self) -> AsyncIterator[ResearchStreamEvent]:
118
+ while True:
119
+ event = await self._queue.get()
120
+ if event is None:
121
+ break
122
+ yield event
123
+
124
+
125
+ async def stream_research_graph(
126
+ graph,
127
+ start,
128
+ state: "ResearchState",
129
+ deps,
130
+ ) -> AsyncIterator[ResearchStreamEvent]:
131
+ """Run the research graph and yield streaming events as they occur."""
132
+
133
+ from contextlib import suppress
134
+
135
+ from haiku.rag.research.state import ResearchDeps # Local import to avoid cycle
136
+
137
+ if not isinstance(deps, ResearchDeps):
138
+ raise TypeError("deps must be an instance of ResearchDeps")
139
+
140
+ stream = ResearchStream()
141
+ deps.stream = stream
142
+
143
+ async def _execute() -> None:
144
+ try:
145
+ report = None
146
+ try:
147
+ result = await graph.run(start, state=state, deps=deps)
148
+ report = result.output
149
+ except Exception:
150
+ from pydantic_graph import End
151
+
152
+ async with graph.iter(start, state=state, deps=deps) as run:
153
+ node = run.next_node
154
+ while not isinstance(node, End):
155
+ node = await run.next(node)
156
+ if run.result:
157
+ report = run.result.output
158
+
159
+ if report is None:
160
+ raise RuntimeError("Graph did not produce a report")
161
+
162
+ stream.report(report, state)
163
+ except Exception as exc: # pragma: no cover - defensive path
164
+ stream.error(exc, state)
165
+ finally:
166
+ await stream.close()
167
+
168
+ runner = asyncio.create_task(_execute())
169
+
170
+ try:
171
+ async for event in stream:
172
+ yield event
173
+ finally:
174
+ if not runner.done():
175
+ runner.cancel()
176
+ with suppress(asyncio.CancelledError):
177
+ await runner
haiku/rag/store/engine.py CHANGED
@@ -157,7 +157,8 @@ class Store:
157
157
  current_version = metadata.version("haiku.rag")
158
158
  db_version = self.get_haiku_version()
159
159
 
160
- run_pending_upgrades(self, db_version, current_version)
160
+ if db_version != "0.0.0":
161
+ run_pending_upgrades(self, db_version, current_version)
161
162
 
162
163
  # After upgrades complete (or if none), set stored version
163
164
  # to the greater of the installed package version and the