Files
Stirling-PDF/engine/src/stirling/agents/math_presentation.py
T

85 lines
3.1 KiB
Python

"""
Math-auditor presentation helpers.
Used by ``PdfQuestionAgent`` and ``PdfReviewAgent`` to (a) decide whether
a request needs the math auditor at all, and (b) pull a Verdict back out
of the resume-turn artifacts.
Intent classification is language-agnostic — a small LLM call rather than
an English regex — so a request like "vérifiez les totaux" routes to the
math path the same as "check the totals".
"""
from __future__ import annotations
from pydantic import Field
from pydantic_ai import Agent
from stirling.agents.output_mode import output_retries
from stirling.contracts import (
MathAuditorToolReportArtifact,
OrchestratorRequest,
Verdict,
)
from stirling.models import ApiModel
from stirling.services import AppRuntime
def extract_math_verdict(request: OrchestratorRequest) -> Verdict | None:
"""Find a math-auditor Verdict in the request's artifacts, if any.
Meta-agents call this on resume to detect whether the specialist has
already run. The Verdict is already type-validated by the time it lands
in :class:`MathAuditorToolReportArtifact` — pydantic rejected the whole
request earlier if the payload was malformed.
"""
for artifact in request.artifacts:
if isinstance(artifact, MathAuditorToolReportArtifact):
return artifact.report
return None
_MATH_INTENT_SYSTEM_PROMPT = (
"Decide whether the user's prompt is asking for verification of "
"numerical content — math correctness, audit, recalculation, totals, "
"sums, percentages, balances, arithmetic, or financial figures. "
"Set is_math=true if so, otherwise false. Decide from the meaning of "
"the prompt, not specific keywords; the prompt may be in any language."
)
class _MathIntentDecision(ApiModel):
is_math: bool = Field(
description=(
"True if the prompt is about verifying numerical content "
"(math, audit, calculations, totals, percentages, etc.)."
),
)
class MathIntentClassifier:
"""Tiny LLM classifier that returns whether a prompt needs the math auditor.
Shared between ``PdfQuestionAgent`` and ``PdfReviewAgent`` so both delegates
use the same decision shape and prompt. One agent instance per consumer
(cheap; matches the existing pattern of per-request agent construction).
"""
def __init__(self, runtime: AppRuntime) -> None:
self._agent: Agent[None, _MathIntentDecision] = Agent(
model=runtime.fast_model,
output_type=_MathIntentDecision,
# Local models emit valid structured output only intermittently (they
# sometimes wrap the JSON in prose); a few extra retries make this
# hot-path classifier reliable. No-op for real providers.
retries=output_retries(runtime.settings.chat_provider),
system_prompt=_MATH_INTENT_SYSTEM_PROMPT,
model_settings=runtime.fast_model_settings,
)
async def classify(self, user_message: str) -> bool:
if not user_message:
return False
result = await self._agent.run(user_message)
return result.output.is_math