|
26 | 26 | ) |
27 | 27 |
|
28 | 28 |
|
| 29 | +class _ContextToActionBridge: |
| 30 | + """ |
| 31 | + Adapter that implements ``ContextTraceSinkProtocol`` and forwards |
| 32 | + ``ContextEvent``s to a ``LangextractSink`` as equivalent ``ActionEvent``s. |
| 33 | +
|
| 34 | + The base agent runtime (``chat_mixin``, ``tool_execution``, |
| 35 | + ``unified_execution_mixin``) emits lifecycle events via |
| 36 | + ``ContextTraceEmitter`` only. This bridge lets the langextract sink |
| 37 | + observe those events without touching the core SDK. |
| 38 | + """ |
| 39 | + |
| 40 | + __slots__ = ("_sink",) |
| 41 | + |
| 42 | + # Subset of ContextEventType values we care about (strings to avoid |
| 43 | + # importing ContextEventType at module load time). |
| 44 | + _CTX_AGENT_START = "agent_start" |
| 45 | + _CTX_AGENT_END = "agent_end" |
| 46 | + _CTX_TOOL_START = "tool_call_start" |
| 47 | + _CTX_TOOL_END = "tool_call_end" |
| 48 | + _CTX_LLM_RESPONSE = "llm_response" |
| 49 | + |
| 50 | + def __init__(self, sink: "LangextractSink") -> None: |
| 51 | + self._sink = sink |
| 52 | + |
| 53 | + def emit(self, event: Any) -> None: # ContextEvent duck-typed |
| 54 | + et = getattr(event, "event_type", None) |
| 55 | + et_value = et.value if hasattr(et, "value") else et |
| 56 | + data = getattr(event, "data", {}) or {} |
| 57 | + ts = getattr(event, "timestamp", 0.0) |
| 58 | + agent = getattr(event, "agent_name", None) |
| 59 | + |
| 60 | + if et_value == self._CTX_AGENT_START: |
| 61 | + self._sink.emit(ActionEvent( |
| 62 | + event_type=ActionEventType.AGENT_START.value, |
| 63 | + timestamp=ts, |
| 64 | + agent_name=agent, |
| 65 | + metadata={"input": data.get("input") or data.get("goal") or ""}, |
| 66 | + )) |
| 67 | + elif et_value == self._CTX_AGENT_END: |
| 68 | + self._sink.emit(ActionEvent( |
| 69 | + event_type=ActionEventType.AGENT_END.value, |
| 70 | + timestamp=ts, |
| 71 | + agent_name=agent, |
| 72 | + status="ok", |
| 73 | + )) |
| 74 | + elif et_value == self._CTX_TOOL_START: |
| 75 | + self._sink.emit(ActionEvent( |
| 76 | + event_type=ActionEventType.TOOL_START.value, |
| 77 | + timestamp=ts, |
| 78 | + agent_name=agent, |
| 79 | + tool_name=data.get("tool_name"), |
| 80 | + tool_args=data.get("arguments"), |
| 81 | + )) |
| 82 | + elif et_value == self._CTX_TOOL_END: |
| 83 | + self._sink.emit(ActionEvent( |
| 84 | + event_type=ActionEventType.TOOL_END.value, |
| 85 | + timestamp=ts, |
| 86 | + agent_name=agent, |
| 87 | + tool_name=data.get("tool_name"), |
| 88 | + duration_ms=(data.get("duration_ms") or 0.0), |
| 89 | + status=data.get("status") or "ok", |
| 90 | + tool_result_summary=str(data.get("result"))[:500] if data.get("result") is not None else None, |
| 91 | + )) |
| 92 | + elif et_value == self._CTX_LLM_RESPONSE: |
| 93 | + # Treat LLM response as an OUTPUT event so the final text shows |
| 94 | + # up in the rendered HTML. |
| 95 | + content = data.get("response_content") or data.get("content") or "" |
| 96 | + self._sink.emit(ActionEvent( |
| 97 | + event_type=ActionEventType.OUTPUT.value, |
| 98 | + timestamp=ts, |
| 99 | + agent_name=agent, |
| 100 | + tool_result_summary=content, |
| 101 | + )) |
| 102 | + |
| 103 | + def flush(self) -> None: |
| 104 | + pass |
| 105 | + |
| 106 | + def close(self) -> None: |
| 107 | + # The owning sink handles render/close — nothing to do here. |
| 108 | + pass |
| 109 | + |
| 110 | + |
29 | 111 | @dataclass |
30 | 112 | class LangextractSinkConfig: |
31 | 113 | """Configuration for the langextract trace sink.""" |
@@ -61,6 +143,17 @@ def __init__(self, config: Optional[LangextractSinkConfig] = None) -> None: |
61 | 143 | self._source_text: Optional[str] = None |
62 | 144 | self._closed = False |
63 | 145 |
|
| 146 | + # ---- Context-emitter bridge ------------------------------------------- |
| 147 | + |
| 148 | + def context_sink(self) -> "_ContextToActionBridge": |
| 149 | + """ |
| 150 | + Return a ``ContextTraceSinkProtocol`` adapter that forwards core |
| 151 | + ``ContextEvent``s into this sink as ``ActionEvent``s. Use with |
| 152 | + ``praisonaiagents.trace.context_events.set_context_emitter`` (or |
| 153 | + ``trace_context``) to capture real agent runtime events. |
| 154 | + """ |
| 155 | + return _ContextToActionBridge(self) |
| 156 | + |
64 | 157 | # ---- TraceSinkProtocol ------------------------------------------------- |
65 | 158 |
|
66 | 159 | def emit(self, event: ActionEvent) -> None: |
|
0 commit comments