Repository navigation
feat(narratives): replace the parallel chart config with unified synthesis and block streaming - #576
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors the narratives agent's synthesis and chart generation pipeline by replacing the parallel background chart configuration and validation workflow with inline, block-based synthesis. The agent now streams interleaved text and chart blocks, resolving chart specifications in place against the turn cache using a new BlockSplitter and BlockContentFrame. Additionally, a code-owned synthesis contract has been introduced to enforce citation and chart block grammar, telemetry has been updated to track emitted and rejected charts, and configuration models have been simplified by removing the obsolete chart configuration and validation roles. No reviewer comments were provided for evaluation, but the implementation is clean, robust, and well-covered by updated unit tests.
Overview
#574 added the four unified-synthesis modules (
workflows/charts.py,workflows/chart_defaults.py,workflows/synthesis.py, andworkflows/blocks.py) without runtime callers. This PR connects them into the live agent loop, replacing the separate parallel chart-planning and data-validation Gemini calls (workflows/chart_config.py) and the{text}and{chart_config}SSEcontentframes with a single synthesis stream that emits interleavedTextBlockandChartBlockitems as{block}SSEcontentframes.workflows/chat_pipeline.py,workflows/sse_frames.py):run_synthesis_phasebuilds typed prompt inputs withbuild_synthesis_inputsandrender_synthesis_message, composes the synthesis system instruction fromconfig.prompts.synthesis, the packagedsynthesiscontract, and the pattern's synthesis directive, bounds both opening the stream and reading each chunk bySYNTHESIS_CHUNK_TIMEOUT_SECONDS, and streams the model output throughBlockSplitter, gating inline chart blocks ondiscovery.has_dataanddiscovery.tablesbefore resolving them againstTurnCache. Completed prose paragraphs and validatedchartblocks stream asBlockContentFrame({"block": ...}) events; when a data-bearing turn produces no valid model-authored chart block,build_default_chartsappends at most one fallback chart; andcharts_emittedandcharts_rejectedare recorded onTurnTelemetryinfinally:.run_clarificationemits its single question as aTextBlockBlockContentFrame, andcollect_follow_up_topicsderives follow-up topics from the digest results, emitted chart titles, and data-gap missing-data notes.workflows/contracts/synthesis.md,workflows/prompt_contracts.py,defaults/prompts/synthesis.md): Splits the synthesis prompt into configurable prose and grounding guidance (defaults/prompts/synthesis.md) and a code-owned contract (workflows/contracts/synthesis.md) appended bycompose_system_instruction, defining citation formatting, answer structure, and the per-kind fencedchartblock grammar so prompt overrides cannot drift fromChartSpecification.workflows/state_slots.py,config_model.py,gemini/schemas.py,telemetry.py): Updatesbuild_state_slotsto acceptSequence[ResolvedChart]directly instead of untypedchart_configdictionaries, deletesworkflows/chart_config.py, removes theCHART_CONFIGandCHART_VALIDATIONroles, model settings, and structured-output schemas, and addscharts_emittedandcharts_rejectedto thechat_turnJSON log and OpenTelemetry span attributes.The core files to review are
workflows/chat_pipeline.py,workflows/contracts/synthesis.md,defaults/prompts/synthesis.md,workflows/state_slots.py, andworkflows/sse_frames.py. This PR and the next PR in this sequence form a back-to-back pair: this PR switches the server's SSEcontentframe payload from{text}and{chart_config}to{block}, and the next PR updates the React UI consumer to render interleaved text and chart blocks, so the two PRs should be reviewed together and merged back to back.Related Issues
This is the sixteenth step in the continued agent loop update sequence, following #547, #549, #550, #558, #559, #562, #563, #564, #565, #566, #568, #569, #570, #571, #572, and #574.
Changes Made
workflows/chat_pipeline.py): Replaces the parallel chart task and plain-text streaming with a single synthesis stream that splits the response into interleaved text and validated chart blocks, appending a default chart when a data-bearing turn produces none. Clarification turns emit their question as a single text block, turn signing signs the prose without chart fences alongside the emitted charts' state slots, and follow-up generation draws topics from the digest results, chart titles, and data-gap notes.workflows/contracts/synthesis.md,workflows/prompt_contracts.py,defaults/prompts/synthesis.md): Moves citation formatting, answer structure, and the fencedchartblock grammar into a code-ownedsynthesiscontract appended to every synthesis prompt so instance overrides cannot drift fromChartSpecification. Replacesdefaults/prompts/synthesis.mdwith the five-section input description and eight grounding rules for answering from the digest.workflows/chart_config.py,workflows/chart_config_test.py): Deletes the standalone chart-planning and data-validation model calls and their unit tests now that charts are selected and validated inline during synthesis.workflows/sse_frames.py,workflows/state_slots.py,workflows/follow_up.py,workflows/messages.py,workflows/mcp_loop.py,config_model.py,gemini/schemas.py,telemetry.py,schemas/agent-config.example.json,schemas/agent-config.schema.json): Replaces the text and chart-config SSE frames withBlockContentFrameand updatesbuild_state_slotsto read scopes directly fromResolvedChartinstances instead of untyped dictionaries. Retires theCHART_CONFIGandCHART_VALIDATIONGemini roles, model settings, and structured-output schemas, and recordscharts_emittedandcharts_rejectedin turn telemetry.workflows/chat_pipeline_test.py,workflows/sse_frames_test.py,workflows/state_slots_test.py,workflows/prompt_contracts_test.py,server/routes/chat_test.py,config_model_test.py,gemini/schemas_test.py,gemini/client_test.py,telemetry_test.py): Updates and expands unit coverage for block streaming, inline chart resolution and fallback defaults,ResolvedChartstate-slot grounding, synthesis contract alignment withChartSpecification, and chart telemetry.narratives/AGENTS.md,narratives/README.md,docs/architecture.md,docs/deployment.md,docs/development.md,deploy/terraform-custom-datacommons/modules/main.tf): Updates the architecture, deployment, and developer guides to describe unified synthesis,{block}SSE frames, the retired chart roles, and the new chart telemetry fields.Testing Done
pnpm test:agent(uv run pytest): 1,706 passed (1,751 onmain; deletingworkflows/chart_config_test.pyand the retiredCHART_CONFIGandCHART_VALIDATIONschema and role tests and replacing the pipeline, SSE-frame, and state-slot suites accounts for the difference).pnpm test:ui(vitest run): 28 files, 304 passed.pnpm lint:ui(tsc --noEmitandbiome check): passed across 53 checked files.pnpm lint:agent(ruff format --check . ../deploy,ruff check . ../deploy, andmypy --strict): passed across all 124 source files (126 onmain).pnpm buildandnode scripts/stage_static.mjs --config-only: passed.pnpm run lint:shell,pnpm run lint:docker, andterraform fmt -check -recursive deploy/terraform-custom-datacommons/: passed (terraform validatefails locally with a pre-existing backend configuration error onmain).schemas/agent-config.example.jsonanddefaults/agent-config.jsonagainstschemas/agent-config.schema.jsonandschemas/theme.example.jsonanddefaults/theme.jsonagainstschemas/theme.schema.jsonwithDraft202012Validator, and confirmedchart_config_modelandchart_validation_modelare rejected by the updated schema.uv --directory agent run python -m narratives_agent.analytics.golden.regenerate_golden --check: exits 0;core_golden.jsonis unchanged.sse_frames.py,state_slots.py,prompt_contracts.py,contracts/synthesis.md,defaults/prompts/synthesis.md,telemetry.py, andchat_pipeline.pyeach fail the unit suite before being reverted.Checklist:
Manual
From
narratives/agent, ranrun_turn,run_clarification, andbuild_state_slotsviauv run pythonwith Gemini and MCP stubbed and verified each scenario by hand:chartblock emits threeBlockContentFrameevents (text,chart,text), signs only the prose paragraphs inturn.model_responsewithout the chart fence, records grounded table and chartQueryScopeentries inturn.state_slots, recordscharts_emitted=1andcharts_rejected=0inTurnTelemetry, and passes the digest scopes and emitted chart title into follow-up generation.chartblock drops the invalid block, appends the deterministiclinefallback chart frombuild_default_charts, and recordscharts_emitted=1andcharts_rejected=1inTurnTelemetry.BlockContentFramecarryingTextBlock(index=0, markdown=question)and signs an emptystate_slotslist.build_state_slotsacrossResolvedLineChart(date_range=None), named-place and cohortResolvedBarChart,ResolvedRankingChart,ResolvedMapChart,ResolvedScatterChart, andResolvedHighlightChartemits groundedQueryScopeentries for each chart kind and respectsMAX_SCOPES_PER_TURN.Risk & Rollback
There are no live instances of this application yet. This PR changes the SSE
contentevent payload from{"text": ...}and{"chart_config": ...}to{"block": ...}, which the current UI ignores until the paired next PR updatesuse_sse_chat.tsandcard_response.tsx; review and merge the two PRs back to back. Existingagent-config.jsonfiles that still setgemini.chart_config_modelorgemini.chart_validation_modelcontinue to load becauseparse_agent_configignores unknown keys. Rollback is reverting the PR.Follow-ups
{block}SSEcontentevents and render interleaved Markdown text and chart blocks.Checklist
AGENTS.mdand followedCODING_GUIDELINES.md, plusFRONTEND.mdfor UI changes.Note: Only Maintainers can approve and merge PRs. Expected initial review time: 3 business days.