GeezerVison is showing github.com — refactor(cli): migrate existing langgraph/pydantic-ai templates to un… · scaleapi/scale-agentex-python@c9f7ab7
File treeExpand file treeCollapse file tree Open diff view settings default-langgraph/project default-pydantic-ai/project temporal-pydantic-ai/project
Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: src/agentex/lib/adk/__init__.py +4Lines changed: 4 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 10 | 10 | from agentex.lib.adk._modules._langgraph_async import stream_langgraph_events | 11 | 11 | from agentex.lib.adk._modules._langgraph_messages import emit_langgraph_messages | 12 | 12 | from agentex.lib.adk._modules._langgraph_sync import convert_langgraph_to_agentex_events | | 13 | +from agentex.lib.adk._modules._langgraph_turn import LangGraphTurn | 13 | 14 | from agentex.lib.adk._modules._pydantic_ai_async import stream_pydantic_ai_events | 14 | 15 | from agentex.lib.adk._modules._pydantic_ai_sync import convert_pydantic_ai_to_agentex_events | 15 | 16 | from agentex.lib.adk._modules._pydantic_ai_tracing import create_pydantic_ai_tracing_handler | | 17 | +from agentex.lib.adk._modules._pydantic_ai_turn import PydanticAITurn | 16 | 18 | from agentex.lib.adk._modules._claude_code_sync import convert_claude_code_to_agentex_events | 17 | 19 | from agentex.lib.adk._modules._claude_code_turn import ( | 18 | 20 | ClaudeCodeTurn, | | 70 | 72 | "stream_langgraph_events", | 71 | 73 | "emit_langgraph_messages", | 72 | 74 | "convert_langgraph_to_agentex_events", | | 75 | + "LangGraphTurn", | 73 | 76 | # Pydantic AI | 74 | 77 | "stream_pydantic_ai_events", | 75 | 78 | "convert_pydantic_ai_to_agentex_events", | 76 | 79 | "create_pydantic_ai_tracing_handler", | | 80 | + "PydanticAITurn", | 77 | 81 | # Claude Code | 78 | 82 | "convert_claude_code_to_agentex_events", | 79 | 83 | "ClaudeCodeTurn", | |
Collapse file Copy file name to clipboardExpand all lines: src/agentex/lib/cli/templates/default-langgraph/project/acp.py.j2 +12-12Lines changed: 12 additions & 12 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -15,13 +15,14 @@ if _litellm_key: | 15 | 15 | os.environ["OPENAI_API_KEY"] = _litellm_key | 16 | 16 |
| 17 | 17 | import agentex.lib.adk as adk | 18 | | -from agentex.lib.adk import create_langgraph_tracing_handler, stream_langgraph_events | | 18 | +from agentex.lib.core.harness import UnifiedEmitter | 19 | 19 | from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config | 20 | 20 | from agentex.lib.sdk.fastacp.fastacp import FastACP | 21 | 21 | from agentex.protocol.acp import SendEventParams, CancelTaskParams, CreateTaskParams | 22 | 22 | from agentex.lib.types.fastacp import AsyncACPConfig | 23 | 23 | from agentex.lib.types.tracing import SGPTracingProcessorConfig | 24 | 24 | from agentex.lib.utils.logging import make_logger | | 25 | +from agentex.lib.adk import LangGraphTurn | 25 | 26 |
| 26 | 27 | from project.graph import create_graph | 27 | 28 |
| @@ -67,24 +68,23 @@ async def handle_task_event_send(params: SendEventParams): | 67 | 68 | input={"message": user_message}, | 68 | 69 | data={"__span_type__": "AGENT_WORKFLOW"}, | 69 | 70 | ) as turn_span: | 70 | | - callback = create_langgraph_tracing_handler( | 71 | | - trace_id=task_id, | 72 | | - parent_span_id=turn_span.id if turn_span else None, | 73 | | - ) | 74 | | - | 75 | 71 | stream = graph.astream( | 76 | 72 | {"messages": [{"role": "user", "content": user_message}]}, | 77 | | - config={ | 78 | | - "configurable": {"thread_id": task_id}, | 79 | | - "callbacks": [callback], | 80 | | - }, | | 73 | + config={"configurable": {"thread_id": task_id}}, | 81 | 74 | stream_mode=["messages", "updates"], | 82 | 75 | ) | 83 | 76 |
| 84 | | - final_output = await stream_langgraph_events(stream, task_id) | | 77 | + turn = LangGraphTurn(stream, model=None) | | 78 | + emitter = UnifiedEmitter( | | 79 | + task_id=task_id, | | 80 | + trace_id=task_id, | | 81 | + parent_span_id=turn_span.id if turn_span else None, | | 82 | + ) | | 83 | + | | 84 | + result = await emitter.auto_send_turn(turn) | 85 | 85 |
| 86 | 86 | if turn_span: | 87 | | - turn_span.output = {"final_output": final_output} | | 87 | + turn_span.output = {"final_output": result.final_text} | 88 | 88 |
| 89 | 89 |
| 90 | 90 | @acp.on_task_create | |
Collapse file Copy file name to clipboardExpand all lines: src/agentex/lib/cli/templates/default-pydantic-ai/project/acp.py.j2 +11-12Lines changed: 11 additions & 12 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -19,21 +19,19 @@ from dotenv import load_dotenv | 19 | 19 |
| 20 | 20 | load_dotenv() | 21 | 21 |
| 22 | | -from project.agent import create_agent | | 22 | +from project.agent import MODEL_NAME, create_agent | 23 | 23 | from pydantic_ai.run import AgentRunResultEvent | 24 | 24 | from pydantic_ai.messages import ModelMessagesTypeAdapter | 25 | 25 |
| 26 | 26 | import agentex.lib.adk as adk | 27 | | -from agentex.lib.adk import ( | 28 | | - stream_pydantic_ai_events, | 29 | | - create_pydantic_ai_tracing_handler, | 30 | | -) | 31 | 27 | from agentex.protocol.acp import SendEventParams, CancelTaskParams, CreateTaskParams | | 28 | +from agentex.lib.core.harness import UnifiedEmitter | 32 | 29 | from agentex.lib.types.fastacp import AsyncACPConfig | 33 | 30 | from agentex.lib.types.tracing import SGPTracingProcessorConfig | 34 | 31 | from agentex.lib.utils.logging import make_logger | 35 | 32 | from agentex.lib.utils.model_utils import BaseModel | 36 | 33 | from agentex.lib.sdk.fastacp.fastacp import FastACP | | 34 | +from agentex.lib.adk import PydanticAITurn | 37 | 35 | from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config | 38 | 36 |
| 39 | 37 | logger = make_logger(__name__) | @@ -125,15 +123,17 @@ async def handle_task_event_send(params: SendEventParams): | 125 | 123 | input={"message": user_message}, | 126 | 124 | data={"__span_type__": "AGENT_WORKFLOW"}, | 127 | 125 | ) as turn_span: | 128 | | - tracing_handler = create_pydantic_ai_tracing_handler( | | 126 | + # Construct the UnifiedEmitter from the ACP context so tracing is | | 127 | + # automatic and messages are auto-sent to the task stream (Redis). | | 128 | + emitter = UnifiedEmitter( | | 129 | + task_id=task_id, | 129 | 130 | trace_id=task_id, | 130 | 131 | parent_span_id=turn_span.id if turn_span else None, | 131 | | - task_id=task_id, | 132 | 132 | ) | 133 | 133 |
| 134 | 134 | # Wrap the pydantic-ai event stream so we can capture the final | 135 | 135 | # AgentRunResultEvent (which carries the full message list for the | 136 | | - # next turn) without changing the streaming-helper's signature. | | 136 | + # next turn) before forwarding events to the emitter. | 137 | 137 | captured_messages: list[Any] = [] | 138 | 138 |
| 139 | 139 | async def tee_messages(upstream) -> AsyncIterator[Any]: | @@ -143,9 +143,8 @@ async def handle_task_event_send(params: SendEventParams): | 143 | 143 | yield event | 144 | 144 |
| 145 | 145 | async with agent.run_stream_events(user_message, message_history=previous_messages) as stream: | 146 | | - final_output = await stream_pydantic_ai_events( | 147 | | - tee_messages(stream), task_id, tracing_handler=tracing_handler | 148 | | - ) | | 146 | + turn = PydanticAITurn(tee_messages(stream), model=MODEL_NAME) | | 147 | + result = await emitter.auto_send_turn(turn) | 149 | 148 |
| 150 | 149 | # Save the updated message history so the next turn picks up here. | 151 | 150 | if captured_messages: | @@ -158,7 +157,7 @@ async def handle_task_event_send(params: SendEventParams): | 158 | 157 | ) | 159 | 158 |
| 160 | 159 | if turn_span: | 161 | | - turn_span.output = {"final_output": final_output} | | 160 | + turn_span.output = {"final_output": result.final_text} | 162 | 161 |
| 163 | 162 |
| 164 | 163 | @acp.on_task_cancel | |
Collapse file Copy file name to clipboardExpand all lines: src/agentex/lib/cli/templates/sync-langgraph/project/acp.py.j2 +11-11Lines changed: 11 additions & 11 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -8,12 +8,13 @@ tokens and tool calls from the LangGraph graph to the Agentex frontend. | 8 | 8 | from typing import AsyncGenerator | 9 | 9 |
| 10 | 10 | import agentex.lib.adk as adk | 11 | | -from agentex.lib.adk import create_langgraph_tracing_handler, convert_langgraph_to_agentex_events | | 11 | +from agentex.lib.core.harness import UnifiedEmitter | 12 | 12 | from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config | 13 | 13 | from agentex.lib.sdk.fastacp.fastacp import FastACP | 14 | 14 | from agentex.protocol.acp import SendMessageParams | 15 | 15 | from agentex.lib.types.tracing import SGPTracingProcessorConfig | 16 | 16 | from agentex.lib.utils.logging import make_logger | | 17 | +from agentex.lib.adk import LangGraphTurn | 17 | 18 | from agentex.types.task_message_content import TaskMessageContent | 18 | 19 | from agentex.types.task_message_delta import TextDelta | 19 | 20 | from agentex.types.task_message_update import TaskMessageUpdate | @@ -72,22 +73,21 @@ async def handle_message_send( | 72 | 73 | input={"message": user_message}, | 73 | 74 | data={"__span_type__": "AGENT_WORKFLOW"}, | 74 | 75 | ) as turn_span: | 75 | | - callback = create_langgraph_tracing_handler( | 76 | | - trace_id=thread_id, | 77 | | - parent_span_id=turn_span.id if turn_span else None, | 78 | | - ) | 79 | | - | 80 | 76 | stream = graph.astream( | 81 | 77 | {"messages": [{"role": "user", "content": user_message}]}, | 82 | | - config={ | 83 | | - "configurable": {"thread_id": thread_id}, | 84 | | - "callbacks": [callback], | 85 | | - }, | | 78 | + config={"configurable": {"thread_id": thread_id}}, | 86 | 79 | stream_mode=["messages", "updates"], | 87 | 80 | ) | 88 | 81 |
| | 82 | + turn = LangGraphTurn(stream, model=None) | | 83 | + emitter = UnifiedEmitter( | | 84 | + task_id=thread_id, | | 85 | + trace_id=thread_id, | | 86 | + parent_span_id=turn_span.id if turn_span else None, | | 87 | + ) | | 88 | + | 89 | 89 | final_text = "" | 90 | | - async for event in convert_langgraph_to_agentex_events(stream): | | 90 | + async for event in emitter.yield_turn(turn): | 91 | 91 | # Accumulate text deltas for span output | 92 | 92 | delta = getattr(event, "delta", None) | 93 | 93 | if isinstance(delta, TextDelta) and delta.text_delta: | |
Collapse file Copy file name to clipboardExpand all lines: src/agentex/lib/cli/templates/sync-pydantic-ai/project/acp.py.j2 +11-12Lines changed: 11 additions & 12 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -15,19 +15,17 @@ from dotenv import load_dotenv | 15 | 15 |
| 16 | 16 | load_dotenv() | 17 | 17 |
| 18 | | -from project.agent import create_agent | | 18 | +from project.agent import MODEL_NAME, create_agent | 19 | 19 |
| 20 | 20 | import agentex.lib.adk as adk | 21 | | -from agentex.lib.adk import ( | 22 | | - create_pydantic_ai_tracing_handler, | 23 | | - convert_pydantic_ai_to_agentex_events, | 24 | | -) | 25 | 21 | from agentex.protocol.acp import SendMessageParams | | 22 | +from agentex.lib.core.harness import UnifiedEmitter | 26 | 23 | from agentex.lib.types.tracing import SGPTracingProcessorConfig | 27 | 24 | from agentex.lib.utils.logging import make_logger | 28 | 25 | from agentex.lib.sdk.fastacp.fastacp import FastACP | 29 | 26 | from agentex.types.task_message_update import TaskMessageUpdate | 30 | 27 | from agentex.types.task_message_content import TaskMessageContent | | 28 | +from agentex.lib.adk import PydanticAITurn | 31 | 29 | from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config | 32 | 30 |
| 33 | 31 | logger = make_logger(__name__) | @@ -73,21 +71,22 @@ async def handle_message_send( | 73 | 71 | logger.info(f"Processing message for task {task_id}") | 74 | 72 |
| 75 | 73 | # Open a per-message turn span. Tool calls below nest underneath this | 76 | | - # span via the tracing handler's parent_span_id wiring. | | 74 | + # span via the emitter's parent_span_id wiring. | 77 | 75 | async with adk.tracing.span( | 78 | 76 | trace_id=task_id, | 79 | 77 | task_id=task_id, | 80 | 78 | name="message", | 81 | 79 | input={"message": user_message}, | 82 | 80 | data={"__span_type__": "AGENT_WORKFLOW"}, | 83 | 81 | ) as turn_span: | 84 | | - tracing_handler = create_pydantic_ai_tracing_handler( | | 82 | + # Construct the UnifiedEmitter from the ACP/streaming context so tracing | | 83 | + # is automatic: tool spans nest under this turn's span. | | 84 | + emitter = UnifiedEmitter( | | 85 | + task_id=task_id, | 85 | 86 | trace_id=task_id, | 86 | 87 | parent_span_id=turn_span.id if turn_span else None, | 87 | | - task_id=task_id, | 88 | 88 | ) | 89 | 89 | async with agent.run_stream_events(user_message) as stream: | 90 | | - async for event in convert_pydantic_ai_to_agentex_events( | 91 | | - stream, tracing_handler=tracing_handler | 92 | | - ): | 93 | | - yield event | | 90 | + turn = PydanticAITurn(stream, model=MODEL_NAME) | | 91 | + async for ev in emitter.yield_turn(turn): | | 92 | + yield ev |
Collapse file Copy file name to clipboardExpand all lines: src/agentex/lib/cli/templates/temporal-pydantic-ai/project/agent.py.j2 +13-14Lines changed: 13 additions & 14 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -11,9 +11,9 @@ moves into recorded activities. | 11 | 11 |
| 12 | 12 | Streaming back to Agentex happens via ``event_stream_handler``, which | 13 | 13 | receives Pydantic AI ``AgentStreamEvent``s from inside the model activity | 14 | | -and forwards them to Redis using the ``stream_pydantic_ai_events`` helper. | 15 | | -The ``task_id`` and tracing parent span ID are threaded into the handler | 16 | | -via ``deps``. | | 14 | +and forwards them through the unified harness surface | | 15 | +(``UnifiedEmitter.auto_send_turn`` + ``PydanticAITurn``). The ``task_id`` and | | 16 | +tracing parent span ID are threaded into the handler via ``deps``. | 17 | 17 | """ | 18 | 18 |
| 19 | 19 | from __future__ import annotations | @@ -27,10 +27,8 @@ from project.tools import get_weather | 27 | 27 | from pydantic_ai.messages import AgentStreamEvent | 28 | 28 | from pydantic_ai.durable_exec.temporal import TemporalAgent | 29 | 29 |
| 30 | | -from agentex.lib.adk import ( | 31 | | - stream_pydantic_ai_events, | 32 | | - create_pydantic_ai_tracing_handler, | 33 | | -) | | 30 | +from agentex.lib.core.harness import UnifiedEmitter | | 31 | +from agentex.lib.adk import PydanticAITurn | 34 | 32 |
| 35 | 33 | # Swap this for any Pydantic AI-supported model identifier | 36 | 34 | # (e.g. "anthropic:claude-3-5-sonnet-latest", "openai:gpt-4o"). | @@ -92,17 +90,18 @@ async def event_handler( | 92 | 90 | activity (not the workflow), it can freely make non-deterministic Redis | 93 | 91 | writes — including the tracing HTTP calls that record per-tool-call | 94 | 92 | spans under the workflow's per-turn span (when ``parent_span_id`` is set). | | 93 | + | | 94 | + The UnifiedEmitter is constructed from ``deps`` (task_id + parent_span_id), | | 95 | + so tool spans nest under the workflow's per-turn span and messages auto-send | | 96 | + to the task stream. | 95 | 97 | """ | 96 | | - tracing_handler = create_pydantic_ai_tracing_handler( | | 98 | + emitter = UnifiedEmitter( | | 99 | + task_id=run_context.deps.task_id, | 97 | 100 | trace_id=run_context.deps.task_id, | 98 | 101 | parent_span_id=run_context.deps.parent_span_id, | 99 | | - task_id=run_context.deps.task_id, | 100 | | - ) | 101 | | - await stream_pydantic_ai_events( | 102 | | - events, | 103 | | - run_context.deps.task_id, | 104 | | - tracing_handler=tracing_handler, | 105 | 102 | ) | | 103 | + turn = PydanticAITurn(events, model=MODEL_NAME) | | 104 | + await emitter.auto_send_turn(turn) | 106 | 105 |
| 107 | 106 |
| 108 | 107 | # Construct the durable agent at module load time so that the | |
|