ishaq101's picture
fix/ check and new chart tool (#16)
5a60e93
Raw
History Blame
5.02 kB
"""Builds the Assembler LLM human-message content.
The system prompt (`config/prompts/assembler.md`) carries the role and rules. This
module assembles the per-call human content: the business context + the executed
`RunState` (task objectives, statuses, and structured tool outputs) + the original
question. Tool outputs are rendered compactly as data — the model turns them into
prose and markdown tables.
"""
from __future__ import annotations
from ..planner.contracts import BusinessContext, ToolOutput
from ..planner.prompt import render_business_context
from .schemas import RunAssessment, RunState, TaskResult
_MAX_ROWS = 20
def render_run_state(run_state: RunState) -> str:
lines = [f"Plan: {run_state.plan_id}"]
if run_state.open_questions:
lines.append("Open questions carried from the plan:")
lines.extend(f" - {q}" for q in run_state.open_questions)
lines.append("")
lines.append("Task results (in execution order):")
for task_id, result in run_state.results.items():
lines.append(_render_task(task_id, result))
return "\n".join(lines)
def _render_task(task_id: str, result: TaskResult) -> str:
lines = [f"- [{result.status}] {task_id}: {result.objective}"]
if result.error:
lines.append(f" note: {result.error}")
for output in result.outputs:
lines.append(f" {_render_output(output)}")
return "\n".join(lines)
def _render_output(output: ToolOutput) -> str:
if output.kind == "error":
return f"({output.tool}) error: {output.error}"
if output.kind == "table" and output.columns is not None:
header = ", ".join(output.columns)
rows = output.rows or []
preview = "; ".join(
" | ".join(str(cell) for cell in row) for row in rows[:_MAX_ROWS]
)
more = "" if len(rows) <= _MAX_ROWS else f" … (+{len(rows) - _MAX_ROWS} more rows)"
return f"({output.tool}) table [{header}]: {preview}{more}"
if output.kind == "chart" and isinstance(output.value, dict):
# W2 (SPINE_V2_PLAN §4.3): one-line summary only — the raw spec's x/y
# arrays would flood the prompt. The chart itself reaches the user via
# GET /api/v1/charts; the narrative only refers to it.
chart_type = output.value.get("chart_type", "chart")
title = output.value.get("title") or ""
plotly = output.value.get("plotly")
traces = plotly.get("data") if isinstance(plotly, dict) else None
n_traces = len(traces) if isinstance(traces, list) else 0
return (
f"({output.tool}) chart: {chart_type} \"{title}\" ({n_traces} series) — "
"shown to the user alongside this answer; refer to it, do not restate "
"its data points"
)
meta = f" meta={output.meta}" if output.meta else ""
return f"({output.tool}) {output.kind}: {output.value}{meta}"
def render_assessment(assessment: RunAssessment) -> str | None:
"""The S1a checkpoint's flags as a short block (SPINE_V2_PLAN §3) — only tasks
that carry specific notes; a clean run renders nothing (no behavior change)."""
flagged = [t for t in assessment.tasks if t.notes]
if not flagged:
return None
lines = [f"Overall execution: {assessment.overall}."]
for t in flagged:
lines.extend(f"- [{t.verdict}] {t.task_id}: {note}" for note in t.notes)
lines.append(
"Name these limitations specifically in the answer — say what was affected "
"and how it scopes the findings. Do not present partial results as complete, "
"and never fall back to a generic \"couldn't compute\"."
)
return "\n".join(lines)
def build_assembler_prompt(
run_state: RunState,
context: BusinessContext,
question: str | None = None,
reply_language: str | None = None,
assessment: RunAssessment | None = None,
) -> str:
sections = [
f"# Business context\n\n{render_business_context(context)}",
f"# Analysis results\n\n{render_run_state(run_state)}",
]
if assessment is not None:
block = render_assessment(assessment)
if block:
sections.append(f"# Execution assessment\n\n{block}")
if question:
sections.append(f"# Original question\n\n{question}")
if reply_language:
# Imperative + resolved language name (a bare "[Reply language]: X" label is too
# weak for the structured-output call — the English data render drowns it out).
# The full rule (incl. explicit-request exception) lives in assembler.md.
sections.append(
f"# Reply language (MANDATORY)\n\n"
f"Write `chat_answer` AND every narrative field entirely in **{reply_language}**. "
f"The results above use English column names and labels — do NOT let that switch "
f"your reply to English. (If the user explicitly asked for a different language, "
f"follow that instead.)"
)
return "\n\n".join(sections)