Skip to content

symfonic.core.learning.entity_extractor_llm

entity_extractor_llm

LLM-backed entity extractor.

Reuses the agent's bound chat model. Best-effort JSON parsing -- a malformed response yields an empty candidate list, never a raise. Caching per-episodic results via a content-hash key is out of scope for v1 (design §9.3 documents the LLM extractor as non-idempotent).

Design reference: §17.1.4 / §13.4 (prompt template).

LlmEntityExtractor

LlmEntityExtractor(
    chat_model: Any,
    *,
    max_entities: int = 10,
    observability_hook: ObservabilityHook | None = None,
)

Decision 4.5 strategy. Uses the agent's bound chat model.

Source code in src/symfonic/core/learning/entity_extractor_llm.py
def __init__(
    self,
    chat_model: Any,
    *,
    max_entities: int = 10,
    observability_hook: ObservabilityHook | None = None,
) -> None:
    self._chat_model = chat_model
    self._max_entities = max_entities
    # v8.3.2 observability-bug fix (G6): the entity-extractor LLM call
    # emitted ONLY on the merge-dependent callback manager (same bug
    # class as the v8.3.1 critic).  When a hook is supplied it also
    # emits on the always-on ``ObservabilityHook``.  ``None`` resolves
    # to ``NoOpObservabilityHook`` so the default is byte-identical.
    # NOTE: the live caller (background ``SleepConsolidator`` ->
    # ``_phases_entity_helpers``) does NOT yet thread a real hook (it
    # calls ``extract(content)`` with no callback_manager either) --
    # see v8.3.2 report for the plumbing this would need.
    if observability_hook is None:
        from symfonic.core.observability.hooks import NoOpObservabilityHook

        observability_hook = NoOpObservabilityHook()
    self._observability_hook = observability_hook

extract async

extract(
    text: str,
    *,
    callback_manager: CallbackManager | None = None,
    run_id: str = "",
    model_name: str = "entity_extractor_llm",
) -> list[EntityCandidate]

Extract entity candidates from episodic text.

v7.4.3 (Jarvio Ask 5): when callback_manager is provided and the underlying chat model exposes ainvoke, fires on_llm_end(node_name="entity_extractor_llm") so the gated Phase 11 LLM call is visible to token billing / OTel. None preserves byte-identical pre-7.4.3 behaviour.

Source code in src/symfonic/core/learning/entity_extractor_llm.py
async def extract(
    self,
    text: str,
    *,
    callback_manager: CallbackManager | None = None,
    run_id: str = "",
    model_name: str = "entity_extractor_llm",
) -> list[EntityCandidate]:
    """Extract entity candidates from episodic text.

    v7.4.3 (Jarvio Ask 5): when ``callback_manager`` is provided and
    the underlying chat model exposes ``ainvoke``, fires
    ``on_llm_end(node_name="entity_extractor_llm")`` so the gated
    Phase 11 LLM call is visible to token billing / OTel.  ``None``
    preserves byte-identical pre-7.4.3 behaviour.
    """
    if not text or self._chat_model is None:
        return []
    cleaned = strip_phase11_markers(text)
    if not cleaned:
        return []
    prompt = _EXTRACTION_PROMPT.format(
        episodic_content=cleaned[:50_000],
        max_entities=self._max_entities,
    )
    try:
        response = await self._invoke_model(
            prompt,
            callback_manager=callback_manager,
            run_id=run_id,
            model_name=model_name,
        )
    except Exception:
        logger.debug("entity_extractor_llm: model invocation failed",
                     exc_info=True)
        return []
    return self._parse_response(response)