Skip to content

symfonic.capabilities.triage.intent

intent

IntentClassifier -- deterministic pre-hydration intent routing.

Ported from :mod:symfonic.agent.triage.intent: a capability may import the kernel's contracts and its own package, so the classifier lives here rather than being imported from the agent layer. The scoring, the "2-of-3" action guard, and the confidence floor are unchanged -- the parity suite asserts full-verdict equality with legacy on every shared fixture.

Routes each turn into one of three lanes so the hydration path can skip semantic retrieval on pure action turns:

  • action — the user is asking the agent to do something (run a tool, call an API, perform a mutation). Full hydration wastes tokens.
  • knowledge — the user wants information or recall. Hydration is load-bearing.
  • ambiguous — the signals are mixed; treat as knowledge to stay safe (full hydration is the conservative fallback).

Deterministic by design: an English verb lexicon, overlap against the deployment's tool-trigger keywords, and zero LLM calls.

IntentClassifier

IntentClassifier(*, ambiguity_confidence_floor: float = 0.55, min_action_signals: int = 2)

Deterministic verb+keyword intent classifier.

Thread-safe and stateless. Safe to reuse across turns. Scoring lives in :mod:symfonic._intent_scoring; this package owns the lexicon and hands it over as values.

Parameters:

Name Type Description Default
ambiguity_confidence_floor float

Verdicts below this confidence value are labelled ambiguous regardless of which lane won. Default 0.55 — chosen so pure verb-lexicon hits (~0.6) stay decisive while contested turns fall through.

0.55
min_action_signals int

Minimum number of distinct signals required before action can win outright. Default 2 implements the "2-of-3" rule so a single action verb in a question turn does not flip to action.

2
Source code in src/symfonic/capabilities/triage/intent.py
def __init__(
    self,
    *,
    ambiguity_confidence_floor: float = 0.55,
    min_action_signals: int = 2,
) -> None:
    self._scoring = IntentScoring(
        _LEXICON,
        ambiguity_confidence_floor=ambiguity_confidence_floor,
        min_action_signals=min_action_signals,
    )

classify

classify(query: str, tool_keywords: Mapping[str, Sequence[str]] | None = None) -> IntentVerdict

Classify query into action / knowledge / ambiguous.

Never raises. On empty input returns an ambiguous verdict with confidence 0.0. tool_keywords maps tool names to their trigger phrases -- the same overlap legacy reads off the domain template, passed as values so this package imports nothing outside itself and the kernel's contracts.

Source code in src/symfonic/capabilities/triage/intent.py
def classify(
    self,
    query: str,
    tool_keywords: Mapping[str, Sequence[str]] | None = None,
) -> IntentVerdict:
    """Classify ``query`` into action / knowledge / ambiguous.

    Never raises. On empty input returns an ``ambiguous`` verdict
    with confidence ``0.0``. ``tool_keywords`` maps tool names to
    their trigger phrases -- the same overlap legacy reads off the
    domain template, passed as values so this package imports
    nothing outside itself and the kernel's contracts.
    """
    verdict = IntentVerdict(**self._scoring.score(query, tool_keywords).as_fields())
    logger.debug("intent verdict: %s", verdict)
    return verdict

IntentVerdict dataclass

IntentVerdict(label: IntentLabel, confidence: float, matched_tool_keywords: list[str] = list(), reasoning: str = '', action_score: float = 0.0, knowledge_score: float = 0.0)

Classifier verdict for a single turn.

Attributes:

Name Type Description
label IntentLabel

One of action / knowledge / ambiguous.

confidence float

0.0–1.0 heuristic confidence. Values below the floor are labelled ambiguous regardless of the winning lane so downstream consumers can gate on a single axis.

matched_tool_keywords list[str]

Tool names whose trigger keywords overlap with the current query.

reasoning str

Short human-readable note — which signals fired.

action_score float

Raw action-lane score prior to normalisation.

knowledge_score float

Raw knowledge-lane score prior to normalisation.

to_event_payload

to_event_payload() -> dict[str, object]

Return a minimal payload suitable for an operator event.

Source code in src/symfonic/capabilities/triage/intent.py
def to_event_payload(self) -> dict[str, object]:
    """Return a minimal payload suitable for an operator event."""
    return {
        "label": self.label,
        "confidence": round(self.confidence, 3),
        "matched_tool_keywords": list(self.matched_tool_keywords),
        "reasoning": self.reasoning,
        "action_score": round(self.action_score, 3),
        "knowledge_score": round(self.knowledge_score, 3),
    }