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(
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(
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)
|