ishaq101's picture
/fix help and tools analyze (#14)
17e31c6
Raw
History Blame Contribute Delete
5.6 kB
"""Builds the planner LLM human-message content.
The system prompt (`config/prompts/planner.md`) carries the role, invariants,
and planning principles. This module assembles the per-call human content:
business context + condensed catalog + available tools + constraints + the
few-shot examples + the question (+ the prior error on retry).
Few-shot examples are rendered from `examples.py` (which builds them from the
real `TaskList` schema) so they cannot drift from the output contract.
"""
from __future__ import annotations
from .contracts import BusinessContext, ToolRegistry
from .examples import render_examples
from .inputs import CatalogSummary, Constraints
def render_business_context(context: BusinessContext) -> str:
lines = [
f"Project: {context.project_id} (industry: {context.industry}, "
f"context completeness: {context.completeness})",
f"Business: {context.business_description}",
f"Scale & scope: {context.scale_and_scope}",
]
if context.key_terms:
lines.append("Key terms:")
lines.extend(f" - {kt.term}: {kt.meaning}" for kt in context.key_terms)
if context.data_overview:
lines.append("Data overview:")
lines.extend(
f" - {n.table_name}: {n.row_represents}" for n in context.data_overview
)
if context.data_column_notes:
lines.append("Column notes:")
lines.extend(
f" - {n.column_name}: {n.meaning}" for n in context.data_column_notes
)
if context.whats_normal:
lines.append(f"What's normal: {context.whats_normal}")
if context.recent_events:
lines.append(f"Recent events: {context.recent_events}")
if context.things_to_watch_for:
lines.append(f"Things to watch for: {context.things_to_watch_for}")
if context.open_questions:
lines.append("Known open questions:")
lines.extend(f" - {q}" for q in context.open_questions)
return "\n".join(lines)
def render_registry(tools: ToolRegistry) -> str:
if not tools.tools:
return "(no tools available)"
blocks: list[str] = []
for spec in tools.tools:
# Render EVERY allowed arg (from input_schema.properties), flagging which
# are required. The validator (check #8) rejects any arg outside this set,
# so the planner must see the full set — not just the required subset — or
# it invents args like `limit`/`order_by` on a tool that doesn't take them
# and fails validation.
props = spec.input_schema.get("properties", {})
required = set(spec.input_schema.get("required", []))
args = (
", ".join(
f"{name}{' (required)' if name in required else ''}" for name in props
)
or "(none)"
)
blocks.append(
f"- {spec.name} (category: {spec.category}, returns: {spec.output_kind})\n"
f" args: {args}\n"
f" {spec.description}"
)
return "\n".join(blocks)
def render_constraints(constraints: Constraints) -> str:
lines = [
f"- max_tasks: {constraints.max_tasks}",
f"- modeling_allowed: {constraints.modeling_allowed} "
"(no modeling tools exist in v1 — do not emit modeling tasks)",
f"- row_budget: {constraints.row_budget}",
]
if constraints.token_budget is not None:
lines.append(f"- token_budget: {constraints.token_budget}")
if constraints.time_budget_seconds is not None:
lines.append(f"- time_budget_seconds: {constraints.time_budget_seconds}")
return "\n".join(lines)
def build_planner_prompt(
context: BusinessContext,
catalog: CatalogSummary,
tools: ToolRegistry,
query: str,
constraints: Constraints,
previous_errors: list[str] | None = None,
reply_language: str | None = None,
) -> str:
"""Return the human-message content for the planner LLM.
The system prompt (`config/prompts/planner.md`) is loaded separately by
`PlannerService`. `previous_errors` is the full history of prior validation
failures (oldest first) so a retry fixes ALL of them at once instead of fixing
one and reintroducing an earlier one. `reply_language` (the pipeline's detected
user language) is surfaced so the only user-facing free text the planner emits
— `infeasible_reason` — comes back in that language instead of always English.
"""
sections = [
f"# Business context\n\n{render_business_context(context)}",
f"# Catalog\n\n{catalog.render()}",
f"# Available tools\n\n{render_registry(tools)}",
f"# Constraints\n\n{render_constraints(constraints)}",
f"# Examples\n\n{render_examples()}",
f"# Question\n\n{query}",
]
if reply_language:
sections.append(
f"# Reply language\n\n"
f"The user writes in {reply_language}. If (and only if) you return an "
f"infeasible plan, write `infeasible_reason` in {reply_language} — it is "
f"shown to the user verbatim. The plan structure itself is unaffected."
)
if previous_errors:
joined = "\n".join(f"- {err}" for err in previous_errors)
sections.append(
"# Previous attempts failed validation\n\n"
"Fix ALL of these. Do NOT reintroduce an earlier error while fixing a "
"newer one:\n"
f"{joined}\n\n"
"Re-read the tools' `args` lists and the QueryIR filter rules above, "
"then emit a corrected TaskList."
)
return "\n\n".join(sections)