Use StateSnapshot updates
This commit is contained in:
parent
663a7fac9a
commit
2c1f79614a
2 changed files with 89 additions and 101 deletions
|
|
@ -3,7 +3,6 @@
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
from haiku.rag.agui.events import AGUIEvent
|
from haiku.rag.agui.events import AGUIEvent
|
||||||
|
|
@ -23,7 +22,7 @@ class AGUIConsoleRenderer:
|
||||||
console: Optional Rich console instance (creates new one if not provided)
|
console: Optional Rich console instance (creates new one if not provided)
|
||||||
"""
|
"""
|
||||||
self.console = console or Console()
|
self.console = console or Console()
|
||||||
self._state: BaseModel | None = None
|
self._state: dict | None = None
|
||||||
|
|
||||||
async def render(self, events: AsyncIterator[AGUIEvent]) -> Any | None:
|
async def render(self, events: AsyncIterator[AGUIEvent]) -> Any | None:
|
||||||
"""Process events and render to console, return final result.
|
"""Process events and render to console, return final result.
|
||||||
|
|
@ -59,7 +58,9 @@ class AGUIConsoleRenderer:
|
||||||
elif event_type == "TEXT_MESSAGE_END":
|
elif event_type == "TEXT_MESSAGE_END":
|
||||||
pass # End of streaming message, no output needed
|
pass # End of streaming message, no output needed
|
||||||
elif event_type == "STATE_SNAPSHOT":
|
elif event_type == "STATE_SNAPSHOT":
|
||||||
self._state = event.get("snapshot")
|
new_state = event.get("snapshot")
|
||||||
|
self._render_state_snapshot(new_state)
|
||||||
|
self._state = new_state
|
||||||
elif event_type == "STATE_DELTA":
|
elif event_type == "STATE_DELTA":
|
||||||
self._apply_state_delta(event)
|
self._apply_state_delta(event)
|
||||||
elif event_type == "ACTIVITY_SNAPSHOT":
|
elif event_type == "ACTIVITY_SNAPSHOT":
|
||||||
|
|
@ -69,84 +70,72 @@ class AGUIConsoleRenderer:
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _render_run_started(self, event: AGUIEvent) -> None:
|
def _render_run_started(self, _event: AGUIEvent) -> None:
|
||||||
"""Render run start event.
|
"""Render run start event."""
|
||||||
|
|
||||||
Args:
|
|
||||||
event: RunStarted event
|
|
||||||
"""
|
|
||||||
# Currently silent - could render run metadata later
|
|
||||||
|
|
||||||
def _render_run_finished(self) -> None:
|
def _render_run_finished(self) -> None:
|
||||||
"""Render run completion."""
|
"""Render run completion."""
|
||||||
# Currently silent - the result is rendered separately
|
|
||||||
|
|
||||||
def _render_error(self, event: AGUIEvent) -> None:
|
def _render_error(self, event: AGUIEvent) -> None:
|
||||||
"""Render error event.
|
"""Render error event."""
|
||||||
|
|
||||||
Args:
|
|
||||||
event: RunError event
|
|
||||||
"""
|
|
||||||
message = event.get("message", "Unknown error")
|
message = event.get("message", "Unknown error")
|
||||||
self.console.print(f"[bold red]❌ Error:[/bold red] {message}")
|
self.console.print(f"[bold red][RUN_ERROR][/bold red] {message}")
|
||||||
|
|
||||||
def _render_step_started(self, event: AGUIEvent) -> None:
|
def _render_step_started(self, event: AGUIEvent) -> None:
|
||||||
"""Render step start event.
|
"""Render step start event."""
|
||||||
|
|
||||||
Args:
|
|
||||||
event: StepStarted event
|
|
||||||
"""
|
|
||||||
step_name = event.get("stepName", "")
|
step_name = event.get("stepName", "")
|
||||||
if step_name:
|
if step_name:
|
||||||
# Format step name for display
|
|
||||||
display_name = step_name.replace("_", " ").title()
|
display_name = step_name.replace("_", " ").title()
|
||||||
self.console.print(f"\n[bold cyan]{display_name}[/bold cyan]")
|
self.console.print(
|
||||||
|
f"\n[bold cyan][STEP_STARTED][/bold cyan] {display_name}"
|
||||||
|
)
|
||||||
|
|
||||||
def _render_step_finished(self, event: AGUIEvent) -> None:
|
def _render_step_finished(self, _event: AGUIEvent) -> None:
|
||||||
"""Render step finish event.
|
"""Render step finish event."""
|
||||||
|
|
||||||
Args:
|
|
||||||
event: StepFinished event
|
|
||||||
"""
|
|
||||||
# Step completion is implicit from the next step or activity
|
|
||||||
|
|
||||||
def _render_text_message(self, event: AGUIEvent) -> None:
|
def _render_text_message(self, event: AGUIEvent) -> None:
|
||||||
"""Render complete text message.
|
"""Render complete text message."""
|
||||||
|
|
||||||
Args:
|
|
||||||
event: TextMessageChunk event
|
|
||||||
"""
|
|
||||||
delta = event.get("delta", "")
|
delta = event.get("delta", "")
|
||||||
# The delta contains the text content to display
|
self.console.print(f"[magenta][TEXT_MESSAGE][/magenta] {delta}")
|
||||||
self.console.print(delta)
|
|
||||||
|
|
||||||
def _render_text_content(self, event: AGUIEvent) -> None:
|
def _render_text_content(self, event: AGUIEvent) -> None:
|
||||||
"""Render streaming text content delta.
|
"""Render streaming text content delta."""
|
||||||
|
|
||||||
Args:
|
|
||||||
event: TextMessageContent event
|
|
||||||
"""
|
|
||||||
delta = event.get("delta", "")
|
delta = event.get("delta", "")
|
||||||
# Print delta without newline for streaming effect
|
|
||||||
self.console.print(delta, end="")
|
self.console.print(delta, end="")
|
||||||
|
|
||||||
def _render_activity(self, event: AGUIEvent) -> None:
|
def _render_activity(self, event: AGUIEvent) -> None:
|
||||||
"""Render activity update.
|
"""Render activity update."""
|
||||||
|
|
||||||
Args:
|
|
||||||
event: ActivitySnapshot event
|
|
||||||
"""
|
|
||||||
content = event.get("content", "")
|
content = event.get("content", "")
|
||||||
|
|
||||||
# Render activity content with emphasis
|
|
||||||
if content:
|
if content:
|
||||||
self.console.print(f"[dim]{content}[/dim]")
|
self.console.print(f"[yellow][ACTIVITY][/yellow] {content}")
|
||||||
|
|
||||||
def _apply_state_delta(self, event: AGUIEvent) -> None:
|
def _render_state_snapshot(self, new_state: dict | None) -> None:
|
||||||
"""Apply state delta to current state.
|
"""Render state snapshot showing only what changed."""
|
||||||
|
if not new_state:
|
||||||
|
return
|
||||||
|
|
||||||
Args:
|
old_state = self._state or {}
|
||||||
event: StateDelta event
|
diff = self._compute_diff(old_state, new_state)
|
||||||
"""
|
|
||||||
# Currently not applying deltas - could implement state patching later
|
if not diff:
|
||||||
# For now, the text messages contain all the information we need to display
|
return
|
||||||
|
|
||||||
|
self.console.print("[blue][STATE_SNAPSHOT][/blue]")
|
||||||
|
self.console.print(diff, style="dim")
|
||||||
|
|
||||||
|
def _compute_diff(self, old: dict, new: dict) -> dict:
|
||||||
|
"""Compute difference between old and new state."""
|
||||||
|
diff = {}
|
||||||
|
for key, new_value in new.items():
|
||||||
|
old_value = old.get(key)
|
||||||
|
if old_value != new_value:
|
||||||
|
if isinstance(new_value, dict) and isinstance(old_value, dict):
|
||||||
|
nested_diff = self._compute_diff(old_value, new_value)
|
||||||
|
if nested_diff:
|
||||||
|
diff[key] = nested_diff
|
||||||
|
else:
|
||||||
|
diff[key] = new_value
|
||||||
|
return diff
|
||||||
|
|
||||||
|
def _apply_state_delta(self, _event: AGUIEvent) -> None:
|
||||||
|
"""Apply state delta to current state."""
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from pydantic_graph.beta.join import reduce_list_append
|
||||||
|
|
||||||
from haiku.rag.config import Config
|
from haiku.rag.config import Config
|
||||||
from haiku.rag.config.models import AppConfig
|
from haiku.rag.config.models import AppConfig
|
||||||
from haiku.rag.graph_common import get_model, log
|
from haiku.rag.graph_common import get_model
|
||||||
from haiku.rag.graph_common.models import ResearchPlan, SearchAnswer
|
from haiku.rag.graph_common.models import ResearchPlan, SearchAnswer
|
||||||
from haiku.rag.graph_common.prompts import PLAN_PROMPT, SEARCH_AGENT_PROMPT
|
from haiku.rag.graph_common.prompts import PLAN_PROMPT, SEARCH_AGENT_PROMPT
|
||||||
from haiku.rag.research.common import (
|
from haiku.rag.research.common import (
|
||||||
|
|
@ -90,11 +90,13 @@ def build_research_graph(
|
||||||
plan_result = await plan_agent.run(prompt, deps=agent_deps)
|
plan_result = await plan_agent.run(prompt, deps=agent_deps)
|
||||||
state.context.sub_questions = list(plan_result.output.sub_questions)
|
state.context.sub_questions = list(plan_result.output.sub_questions)
|
||||||
|
|
||||||
# Log the plan results
|
# State now contains the plan - emit state update and narrate
|
||||||
log(deps, state, f"Main Question: {state.context.original_question}")
|
if deps.agui_emitter:
|
||||||
log(deps, state, "Sub-questions:")
|
deps.agui_emitter.update_state(state)
|
||||||
for i, sq in enumerate(state.context.sub_questions, 1):
|
count = len(state.context.sub_questions)
|
||||||
log(deps, state, f" {i}. {sq}")
|
deps.agui_emitter.update_activity(
|
||||||
|
"planning", f"Created plan with {count} sub-questions"
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
if deps.agui_emitter:
|
if deps.agui_emitter:
|
||||||
deps.agui_emitter.finish_step()
|
deps.agui_emitter.finish_step()
|
||||||
|
|
@ -171,10 +173,18 @@ def build_research_graph(
|
||||||
answer = result.output
|
answer = result.output
|
||||||
if answer:
|
if answer:
|
||||||
state.context.add_qa_response(answer)
|
state.context.add_qa_response(answer)
|
||||||
log(deps, state, f"Answer: {answer.answer}")
|
# State updated with new answer - emit state update and narrate
|
||||||
|
if deps.agui_emitter:
|
||||||
|
deps.agui_emitter.update_state(state)
|
||||||
|
deps.agui_emitter.update_activity(
|
||||||
|
"searching",
|
||||||
|
f"Found answer with {answer.confidence:.0%} confidence",
|
||||||
|
)
|
||||||
return answer
|
return answer
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log(deps, state, f"Search failed: {e}")
|
# Narrate the error
|
||||||
|
if deps.agui_emitter:
|
||||||
|
deps.agui_emitter.update_activity("searching", f"Search failed: {e}")
|
||||||
failure_answer = SearchAnswer(
|
failure_answer = SearchAnswer(
|
||||||
query=sub_q,
|
query=sub_q,
|
||||||
answer=f"Search failed after retries: {str(e)}",
|
answer=f"Search failed after retries: {str(e)}",
|
||||||
|
|
@ -236,27 +246,21 @@ def build_research_graph(
|
||||||
state.context.integrate_analysis(analysis)
|
state.context.integrate_analysis(analysis)
|
||||||
state.last_analysis = analysis
|
state.last_analysis = analysis
|
||||||
|
|
||||||
if analysis.commentary:
|
# State updated with insights/gaps - emit state update and narrate
|
||||||
log(deps, state, f"Summary: {analysis.commentary}")
|
if deps.agui_emitter:
|
||||||
if analysis.highlights:
|
deps.agui_emitter.update_state(state)
|
||||||
log(deps, state, "Updated insights:")
|
highlights = len(analysis.highlights) if analysis.highlights else 0
|
||||||
for insight in analysis.highlights:
|
gaps = len(analysis.gap_assessments) if analysis.gap_assessments else 0
|
||||||
label = insight.status.value
|
resolved = len(analysis.resolved_gaps) if analysis.resolved_gaps else 0
|
||||||
log(deps, state, f" • ({label}) {insight.summary}")
|
parts = []
|
||||||
if analysis.gap_assessments:
|
if highlights:
|
||||||
log(deps, state, "Gap updates:")
|
parts.append(f"{highlights} insights")
|
||||||
for gap in analysis.gap_assessments:
|
if gaps:
|
||||||
status = "resolved" if gap.resolved else "open"
|
parts.append(f"{gaps} gaps")
|
||||||
severity = gap.severity.value
|
if resolved:
|
||||||
log(deps, state, f" • ({severity}/{status}) {gap.description}")
|
parts.append(f"{resolved} resolved")
|
||||||
if analysis.resolved_gaps:
|
summary = ", ".join(parts) if parts else "No updates"
|
||||||
log(deps, state, "Resolved gaps:")
|
deps.agui_emitter.update_activity("analyzing", f"Analysis: {summary}")
|
||||||
for resolved in analysis.resolved_gaps:
|
|
||||||
log(deps, state, f" • {resolved}")
|
|
||||||
if analysis.new_questions:
|
|
||||||
log(deps, state, "Proposed follow-ups:")
|
|
||||||
for question in analysis.new_questions:
|
|
||||||
log(deps, state, f" • {question}")
|
|
||||||
finally:
|
finally:
|
||||||
if deps.agui_emitter:
|
if deps.agui_emitter:
|
||||||
deps.agui_emitter.finish_step()
|
deps.agui_emitter.finish_step()
|
||||||
|
|
@ -314,19 +318,14 @@ def build_research_graph(
|
||||||
if new_q not in state.context.sub_questions:
|
if new_q not in state.context.sub_questions:
|
||||||
state.context.sub_questions.append(new_q)
|
state.context.sub_questions.append(new_q)
|
||||||
|
|
||||||
if output.key_insights:
|
# State updated with evaluation - emit state update and narrate
|
||||||
log(deps, state, "Key insights:")
|
if deps.agui_emitter:
|
||||||
for insight in output.key_insights:
|
deps.agui_emitter.update_state(state)
|
||||||
log(deps, state, f" • {insight}")
|
sufficient = "Yes" if output.is_sufficient else "No"
|
||||||
|
deps.agui_emitter.update_activity(
|
||||||
if output.gaps:
|
"evaluating",
|
||||||
log(deps, state, "Remaining gaps:")
|
f"Confidence: {output.confidence_score:.0%}, Sufficient: {sufficient}",
|
||||||
for gap in output.gaps:
|
)
|
||||||
log(deps, state, f" • {gap}")
|
|
||||||
|
|
||||||
log(deps, state, f"Confidence: {output.confidence_score:.1%}")
|
|
||||||
status = "Yes" if output.is_sufficient else "No"
|
|
||||||
log(deps, state, f"Sufficient: {status}")
|
|
||||||
|
|
||||||
should_continue = (
|
should_continue = (
|
||||||
not output.is_sufficient
|
not output.is_sufficient
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue