agentex-sdk 0.1.0a6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentex/__init__.py +103 -0
- agentex/_base_client.py +1992 -0
- agentex/_client.py +506 -0
- agentex/_compat.py +219 -0
- agentex/_constants.py +14 -0
- agentex/_exceptions.py +108 -0
- agentex/_files.py +123 -0
- agentex/_models.py +829 -0
- agentex/_qs.py +150 -0
- agentex/_resource.py +43 -0
- agentex/_response.py +830 -0
- agentex/_streaming.py +333 -0
- agentex/_types.py +219 -0
- agentex/_utils/__init__.py +57 -0
- agentex/_utils/_logs.py +25 -0
- agentex/_utils/_proxy.py +65 -0
- agentex/_utils/_reflection.py +42 -0
- agentex/_utils/_resources_proxy.py +24 -0
- agentex/_utils/_streams.py +12 -0
- agentex/_utils/_sync.py +86 -0
- agentex/_utils/_transform.py +447 -0
- agentex/_utils/_typing.py +151 -0
- agentex/_utils/_utils.py +422 -0
- agentex/_version.py +4 -0
- agentex/lib/.keep +4 -0
- agentex/lib/__init__.py +0 -0
- agentex/lib/adk/__init__.py +41 -0
- agentex/lib/adk/_modules/__init__.py +0 -0
- agentex/lib/adk/_modules/acp.py +247 -0
- agentex/lib/adk/_modules/agent_task_tracker.py +176 -0
- agentex/lib/adk/_modules/agents.py +77 -0
- agentex/lib/adk/_modules/events.py +141 -0
- agentex/lib/adk/_modules/messages.py +285 -0
- agentex/lib/adk/_modules/state.py +291 -0
- agentex/lib/adk/_modules/streaming.py +75 -0
- agentex/lib/adk/_modules/tasks.py +124 -0
- agentex/lib/adk/_modules/tracing.py +194 -0
- agentex/lib/adk/providers/__init__.py +9 -0
- agentex/lib/adk/providers/_modules/__init__.py +0 -0
- agentex/lib/adk/providers/_modules/litellm.py +232 -0
- agentex/lib/adk/providers/_modules/openai.py +416 -0
- agentex/lib/adk/providers/_modules/sgp.py +85 -0
- agentex/lib/adk/utils/__init__.py +5 -0
- agentex/lib/adk/utils/_modules/__init__.py +0 -0
- agentex/lib/adk/utils/_modules/templating.py +94 -0
- agentex/lib/cli/__init__.py +0 -0
- agentex/lib/cli/commands/__init__.py +0 -0
- agentex/lib/cli/commands/agents.py +328 -0
- agentex/lib/cli/commands/init.py +227 -0
- agentex/lib/cli/commands/main.py +33 -0
- agentex/lib/cli/commands/secrets.py +169 -0
- agentex/lib/cli/commands/tasks.py +118 -0
- agentex/lib/cli/commands/uv.py +133 -0
- agentex/lib/cli/handlers/__init__.py +0 -0
- agentex/lib/cli/handlers/agent_handlers.py +160 -0
- agentex/lib/cli/handlers/cleanup_handlers.py +186 -0
- agentex/lib/cli/handlers/deploy_handlers.py +351 -0
- agentex/lib/cli/handlers/run_handlers.py +452 -0
- agentex/lib/cli/handlers/secret_handlers.py +670 -0
- agentex/lib/cli/templates/default/.dockerignore.j2 +43 -0
- agentex/lib/cli/templates/default/Dockerfile-uv.j2 +42 -0
- agentex/lib/cli/templates/default/Dockerfile.j2 +42 -0
- agentex/lib/cli/templates/default/README.md.j2 +193 -0
- agentex/lib/cli/templates/default/deploy/example.yaml.j2 +55 -0
- agentex/lib/cli/templates/default/manifest.yaml.j2 +116 -0
- agentex/lib/cli/templates/default/project/acp.py.j2 +29 -0
- agentex/lib/cli/templates/default/pyproject.toml.j2 +33 -0
- agentex/lib/cli/templates/default/requirements.txt.j2 +5 -0
- agentex/lib/cli/templates/deploy/Screenshot 2025-03-19 at 10.36.57/342/200/257AM.png +0 -0
- agentex/lib/cli/templates/deploy/example.yaml.j2 +55 -0
- agentex/lib/cli/templates/sync/.dockerignore.j2 +43 -0
- agentex/lib/cli/templates/sync/Dockerfile-uv.j2 +42 -0
- agentex/lib/cli/templates/sync/Dockerfile.j2 +42 -0
- agentex/lib/cli/templates/sync/README.md.j2 +293 -0
- agentex/lib/cli/templates/sync/deploy/example.yaml.j2 +55 -0
- agentex/lib/cli/templates/sync/manifest.yaml.j2 +116 -0
- agentex/lib/cli/templates/sync/project/acp.py.j2 +26 -0
- agentex/lib/cli/templates/sync/pyproject.toml.j2 +33 -0
- agentex/lib/cli/templates/sync/requirements.txt.j2 +5 -0
- agentex/lib/cli/templates/temporal/.dockerignore.j2 +43 -0
- agentex/lib/cli/templates/temporal/Dockerfile-uv.j2 +48 -0
- agentex/lib/cli/templates/temporal/Dockerfile.j2 +48 -0
- agentex/lib/cli/templates/temporal/README.md.j2 +316 -0
- agentex/lib/cli/templates/temporal/deploy/example.yaml.j2 +55 -0
- agentex/lib/cli/templates/temporal/manifest.yaml.j2 +137 -0
- agentex/lib/cli/templates/temporal/project/acp.py.j2 +30 -0
- agentex/lib/cli/templates/temporal/project/run_worker.py.j2 +33 -0
- agentex/lib/cli/templates/temporal/project/workflow.py.j2 +66 -0
- agentex/lib/cli/templates/temporal/pyproject.toml.j2 +34 -0
- agentex/lib/cli/templates/temporal/requirements.txt.j2 +5 -0
- agentex/lib/cli/utils/cli_utils.py +14 -0
- agentex/lib/cli/utils/credential_utils.py +103 -0
- agentex/lib/cli/utils/exceptions.py +6 -0
- agentex/lib/cli/utils/kubectl_utils.py +135 -0
- agentex/lib/cli/utils/kubernetes_secrets_utils.py +185 -0
- agentex/lib/core/__init__.py +0 -0
- agentex/lib/core/adapters/__init__.py +0 -0
- agentex/lib/core/adapters/llm/__init__.py +1 -0
- agentex/lib/core/adapters/llm/adapter_litellm.py +46 -0
- agentex/lib/core/adapters/llm/adapter_sgp.py +55 -0
- agentex/lib/core/adapters/llm/port.py +24 -0
- agentex/lib/core/adapters/streams/adapter_redis.py +128 -0
- agentex/lib/core/adapters/streams/port.py +50 -0
- agentex/lib/core/clients/__init__.py +1 -0
- agentex/lib/core/clients/temporal/__init__.py +0 -0
- agentex/lib/core/clients/temporal/temporal_client.py +181 -0
- agentex/lib/core/clients/temporal/types.py +47 -0
- agentex/lib/core/clients/temporal/utils.py +56 -0
- agentex/lib/core/services/__init__.py +0 -0
- agentex/lib/core/services/adk/__init__.py +0 -0
- agentex/lib/core/services/adk/acp/__init__.py +0 -0
- agentex/lib/core/services/adk/acp/acp.py +210 -0
- agentex/lib/core/services/adk/agent_task_tracker.py +85 -0
- agentex/lib/core/services/adk/agents.py +43 -0
- agentex/lib/core/services/adk/events.py +61 -0
- agentex/lib/core/services/adk/messages.py +164 -0
- agentex/lib/core/services/adk/providers/__init__.py +0 -0
- agentex/lib/core/services/adk/providers/litellm.py +256 -0
- agentex/lib/core/services/adk/providers/openai.py +723 -0
- agentex/lib/core/services/adk/providers/sgp.py +99 -0
- agentex/lib/core/services/adk/state.py +120 -0
- agentex/lib/core/services/adk/streaming.py +262 -0
- agentex/lib/core/services/adk/tasks.py +69 -0
- agentex/lib/core/services/adk/tracing.py +36 -0
- agentex/lib/core/services/adk/utils/__init__.py +0 -0
- agentex/lib/core/services/adk/utils/templating.py +58 -0
- agentex/lib/core/temporal/__init__.py +0 -0
- agentex/lib/core/temporal/activities/__init__.py +207 -0
- agentex/lib/core/temporal/activities/activity_helpers.py +37 -0
- agentex/lib/core/temporal/activities/adk/__init__.py +0 -0
- agentex/lib/core/temporal/activities/adk/acp/__init__.py +0 -0
- agentex/lib/core/temporal/activities/adk/acp/acp_activities.py +86 -0
- agentex/lib/core/temporal/activities/adk/agent_task_tracker_activities.py +76 -0
- agentex/lib/core/temporal/activities/adk/agents_activities.py +35 -0
- agentex/lib/core/temporal/activities/adk/events_activities.py +50 -0
- agentex/lib/core/temporal/activities/adk/messages_activities.py +94 -0
- agentex/lib/core/temporal/activities/adk/providers/__init__.py +0 -0
- agentex/lib/core/temporal/activities/adk/providers/litellm_activities.py +71 -0
- agentex/lib/core/temporal/activities/adk/providers/openai_activities.py +210 -0
- agentex/lib/core/temporal/activities/adk/providers/sgp_activities.py +42 -0
- agentex/lib/core/temporal/activities/adk/state_activities.py +85 -0
- agentex/lib/core/temporal/activities/adk/streaming_activities.py +33 -0
- agentex/lib/core/temporal/activities/adk/tasks_activities.py +48 -0
- agentex/lib/core/temporal/activities/adk/tracing_activities.py +55 -0
- agentex/lib/core/temporal/activities/adk/utils/__init__.py +0 -0
- agentex/lib/core/temporal/activities/adk/utils/templating_activities.py +41 -0
- agentex/lib/core/temporal/services/__init__.py +0 -0
- agentex/lib/core/temporal/services/temporal_task_service.py +69 -0
- agentex/lib/core/temporal/types/__init__.py +0 -0
- agentex/lib/core/temporal/types/workflow.py +5 -0
- agentex/lib/core/temporal/workers/__init__.py +0 -0
- agentex/lib/core/temporal/workers/worker.py +162 -0
- agentex/lib/core/temporal/workflows/workflow.py +26 -0
- agentex/lib/core/tracing/__init__.py +5 -0
- agentex/lib/core/tracing/processors/agentex_tracing_processor.py +117 -0
- agentex/lib/core/tracing/processors/sgp_tracing_processor.py +119 -0
- agentex/lib/core/tracing/processors/tracing_processor_interface.py +40 -0
- agentex/lib/core/tracing/trace.py +311 -0
- agentex/lib/core/tracing/tracer.py +70 -0
- agentex/lib/core/tracing/tracing_processor_manager.py +62 -0
- agentex/lib/environment_variables.py +87 -0
- agentex/lib/py.typed +0 -0
- agentex/lib/sdk/__init__.py +0 -0
- agentex/lib/sdk/config/__init__.py +0 -0
- agentex/lib/sdk/config/agent_config.py +61 -0
- agentex/lib/sdk/config/agent_manifest.py +219 -0
- agentex/lib/sdk/config/build_config.py +35 -0
- agentex/lib/sdk/config/deployment_config.py +117 -0
- agentex/lib/sdk/config/local_development_config.py +56 -0
- agentex/lib/sdk/config/project_config.py +103 -0
- agentex/lib/sdk/fastacp/__init__.py +3 -0
- agentex/lib/sdk/fastacp/base/base_acp_server.py +406 -0
- agentex/lib/sdk/fastacp/fastacp.py +74 -0
- agentex/lib/sdk/fastacp/impl/agentic_base_acp.py +72 -0
- agentex/lib/sdk/fastacp/impl/sync_acp.py +109 -0
- agentex/lib/sdk/fastacp/impl/temporal_acp.py +97 -0
- agentex/lib/sdk/fastacp/tests/README.md +297 -0
- agentex/lib/sdk/fastacp/tests/conftest.py +307 -0
- agentex/lib/sdk/fastacp/tests/pytest.ini +10 -0
- agentex/lib/sdk/fastacp/tests/run_tests.py +227 -0
- agentex/lib/sdk/fastacp/tests/test_base_acp_server.py +450 -0
- agentex/lib/sdk/fastacp/tests/test_fastacp_factory.py +344 -0
- agentex/lib/sdk/fastacp/tests/test_integration.py +477 -0
- agentex/lib/sdk/state_machine/__init__.py +6 -0
- agentex/lib/sdk/state_machine/noop_workflow.py +21 -0
- agentex/lib/sdk/state_machine/state.py +10 -0
- agentex/lib/sdk/state_machine/state_machine.py +189 -0
- agentex/lib/sdk/state_machine/state_workflow.py +16 -0
- agentex/lib/sdk/utils/__init__.py +0 -0
- agentex/lib/sdk/utils/messages.py +223 -0
- agentex/lib/types/__init__.py +0 -0
- agentex/lib/types/acp.py +94 -0
- agentex/lib/types/agent_configs.py +79 -0
- agentex/lib/types/agent_results.py +29 -0
- agentex/lib/types/credentials.py +34 -0
- agentex/lib/types/fastacp.py +61 -0
- agentex/lib/types/files.py +13 -0
- agentex/lib/types/json_rpc.py +49 -0
- agentex/lib/types/llm_messages.py +354 -0
- agentex/lib/types/task_message_updates.py +171 -0
- agentex/lib/types/tracing.py +34 -0
- agentex/lib/utils/__init__.py +0 -0
- agentex/lib/utils/completions.py +131 -0
- agentex/lib/utils/console.py +14 -0
- agentex/lib/utils/io.py +29 -0
- agentex/lib/utils/iterables.py +14 -0
- agentex/lib/utils/json_schema.py +23 -0
- agentex/lib/utils/logging.py +31 -0
- agentex/lib/utils/mcp.py +17 -0
- agentex/lib/utils/model_utils.py +46 -0
- agentex/lib/utils/parsing.py +15 -0
- agentex/lib/utils/regex.py +6 -0
- agentex/lib/utils/temporal.py +13 -0
- agentex/py.typed +0 -0
- agentex/resources/__init__.py +103 -0
- agentex/resources/agents.py +707 -0
- agentex/resources/events.py +294 -0
- agentex/resources/messages/__init__.py +33 -0
- agentex/resources/messages/batch.py +271 -0
- agentex/resources/messages/messages.py +492 -0
- agentex/resources/spans.py +557 -0
- agentex/resources/states.py +544 -0
- agentex/resources/tasks.py +615 -0
- agentex/resources/tracker.py +384 -0
- agentex/types/__init__.py +56 -0
- agentex/types/acp_type.py +7 -0
- agentex/types/agent.py +29 -0
- agentex/types/agent_list_params.py +13 -0
- agentex/types/agent_list_response.py +10 -0
- agentex/types/agent_rpc_by_name_params.py +21 -0
- agentex/types/agent_rpc_params.py +51 -0
- agentex/types/agent_rpc_params1.py +21 -0
- agentex/types/agent_rpc_response.py +20 -0
- agentex/types/agent_rpc_result.py +90 -0
- agentex/types/agent_task_tracker.py +34 -0
- agentex/types/data_content.py +30 -0
- agentex/types/data_content_param.py +31 -0
- agentex/types/data_delta.py +14 -0
- agentex/types/event.py +29 -0
- agentex/types/event_list_params.py +22 -0
- agentex/types/event_list_response.py +10 -0
- agentex/types/message_author.py +7 -0
- agentex/types/message_create_params.py +18 -0
- agentex/types/message_list_params.py +14 -0
- agentex/types/message_list_response.py +10 -0
- agentex/types/message_style.py +7 -0
- agentex/types/message_update_params.py +18 -0
- agentex/types/messages/__init__.py +8 -0
- agentex/types/messages/batch_create_params.py +16 -0
- agentex/types/messages/batch_create_response.py +10 -0
- agentex/types/messages/batch_update_params.py +16 -0
- agentex/types/messages/batch_update_response.py +10 -0
- agentex/types/shared/__init__.py +3 -0
- agentex/types/shared/task_message_update.py +83 -0
- agentex/types/span.py +36 -0
- agentex/types/span_create_params.py +40 -0
- agentex/types/span_list_params.py +12 -0
- agentex/types/span_list_response.py +10 -0
- agentex/types/span_update_params.py +37 -0
- agentex/types/state.py +25 -0
- agentex/types/state_create_params.py +16 -0
- agentex/types/state_list_params.py +16 -0
- agentex/types/state_list_response.py +10 -0
- agentex/types/state_update_params.py +16 -0
- agentex/types/task.py +23 -0
- agentex/types/task_delete_by_name_response.py +8 -0
- agentex/types/task_delete_response.py +8 -0
- agentex/types/task_list_response.py +10 -0
- agentex/types/task_message.py +33 -0
- agentex/types/task_message_content.py +16 -0
- agentex/types/task_message_content_param.py +17 -0
- agentex/types/task_message_delta.py +16 -0
- agentex/types/text_content.py +53 -0
- agentex/types/text_content_param.py +54 -0
- agentex/types/text_delta.py +14 -0
- agentex/types/tool_request_content.py +36 -0
- agentex/types/tool_request_content_param.py +37 -0
- agentex/types/tool_request_delta.py +18 -0
- agentex/types/tool_response_content.py +36 -0
- agentex/types/tool_response_content_param.py +36 -0
- agentex/types/tool_response_delta.py +18 -0
- agentex/types/tracker_list_params.py +16 -0
- agentex/types/tracker_list_response.py +10 -0
- agentex/types/tracker_update_params.py +19 -0
- agentex_sdk-0.1.0a6.dist-info/METADATA +426 -0
- agentex_sdk-0.1.0a6.dist-info/RECORD +289 -0
- agentex_sdk-0.1.0a6.dist-info/WHEEL +4 -0
- agentex_sdk-0.1.0a6.dist-info/entry_points.txt +2 -0
- agentex_sdk-0.1.0a6.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,128 @@
|
|
1
|
+
import asyncio
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
from collections.abc import AsyncIterator
|
5
|
+
from typing import Annotated, Any
|
6
|
+
|
7
|
+
import redis.asyncio as redis
|
8
|
+
from fastapi import Depends
|
9
|
+
|
10
|
+
from agentex.lib.core.adapters.streams.port import EventStreamRepository
|
11
|
+
from agentex.lib.utils.logging import make_logger
|
12
|
+
|
13
|
+
logger = make_logger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
class RedisEventStreamRepository(EventStreamRepository):
|
17
|
+
"""
|
18
|
+
A simplified Redis implementation of the EventStreamRepository interface.
|
19
|
+
Optimized for text/JSON streaming with SSE.
|
20
|
+
"""
|
21
|
+
|
22
|
+
def __init__(self, redis_url: str | None = None):
|
23
|
+
# Get Redis URL from environment if not provided
|
24
|
+
self.redis_url = redis_url or os.environ.get(
|
25
|
+
"REDIS_URL", "redis://localhost:6379"
|
26
|
+
)
|
27
|
+
self.redis = redis.from_url(self.redis_url)
|
28
|
+
|
29
|
+
async def send_event(self, topic: str, event: dict[str, Any]) -> str:
|
30
|
+
"""
|
31
|
+
Send an event to a Redis stream.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
topic: The stream topic/name
|
35
|
+
event: The event data (will be JSON serialized)
|
36
|
+
|
37
|
+
Returns:
|
38
|
+
The message ID from Redis
|
39
|
+
"""
|
40
|
+
try:
|
41
|
+
# Simple JSON serialization
|
42
|
+
event_json = json.dumps(event)
|
43
|
+
|
44
|
+
# # Uncomment to debug
|
45
|
+
# logger.info(f"Sending event to Redis stream {topic}: {event_json}")
|
46
|
+
|
47
|
+
# Add to Redis stream with a reasonable max length
|
48
|
+
message_id = await self.redis.xadd(
|
49
|
+
name=topic,
|
50
|
+
fields={"data": event_json},
|
51
|
+
)
|
52
|
+
|
53
|
+
return message_id
|
54
|
+
except Exception as e:
|
55
|
+
logger.error(f"Error publishing to Redis stream {topic}: {e}")
|
56
|
+
raise
|
57
|
+
|
58
|
+
async def subscribe(
|
59
|
+
self, topic: str, last_id: str = "$"
|
60
|
+
) -> AsyncIterator[dict[str, Any]]:
|
61
|
+
"""
|
62
|
+
Subscribe to a Redis stream and yield events as they come in.
|
63
|
+
|
64
|
+
Args:
|
65
|
+
topic: The stream topic to subscribe to
|
66
|
+
last_id: Where to start reading from:
|
67
|
+
"$" = only new messages (default)
|
68
|
+
"0" = all messages from the beginning
|
69
|
+
"<id>" = messages after the specified ID
|
70
|
+
|
71
|
+
Yields:
|
72
|
+
Parsed event data
|
73
|
+
"""
|
74
|
+
|
75
|
+
current_id = last_id
|
76
|
+
|
77
|
+
while True:
|
78
|
+
try:
|
79
|
+
# Read new messages with a reasonable block time
|
80
|
+
streams = {topic: current_id}
|
81
|
+
response = await self.redis.xread(
|
82
|
+
streams=streams,
|
83
|
+
count=10, # Get up to 10 messages at a time (reduces overprocessing)
|
84
|
+
block=2000, # Wait up to 2 seconds for new messages
|
85
|
+
)
|
86
|
+
|
87
|
+
if response:
|
88
|
+
for _, messages in response:
|
89
|
+
for message_id, fields in messages:
|
90
|
+
# Update the last_id for next iteration
|
91
|
+
current_id = message_id
|
92
|
+
|
93
|
+
# Extract and parse the JSON data
|
94
|
+
if b"data" in fields:
|
95
|
+
try:
|
96
|
+
data_str = fields[b"data"].decode("utf-8")
|
97
|
+
event = json.loads(data_str)
|
98
|
+
yield event
|
99
|
+
except Exception as e:
|
100
|
+
logger.warning(
|
101
|
+
f"Failed to parse event from Redis stream: {e}"
|
102
|
+
)
|
103
|
+
|
104
|
+
# Small sleep to prevent tight loops
|
105
|
+
await asyncio.sleep(0.01)
|
106
|
+
|
107
|
+
except Exception as e:
|
108
|
+
logger.error(f"Error reading from Redis stream: {e}")
|
109
|
+
await asyncio.sleep(1) # Back off on errors
|
110
|
+
|
111
|
+
async def cleanup_stream(self, topic: str) -> None:
|
112
|
+
"""
|
113
|
+
Clean up a Redis stream.
|
114
|
+
|
115
|
+
Args:
|
116
|
+
topic: The stream topic to clean up
|
117
|
+
"""
|
118
|
+
try:
|
119
|
+
await self.redis.delete(topic)
|
120
|
+
logger.info(f"Cleaned up Redis stream: {topic}")
|
121
|
+
except Exception as e:
|
122
|
+
logger.error(f"Error cleaning up Redis stream {topic}: {e}")
|
123
|
+
raise
|
124
|
+
|
125
|
+
|
126
|
+
DRedisEventStreamRepository = Annotated[
|
127
|
+
RedisEventStreamRepository | None, Depends(RedisEventStreamRepository)
|
128
|
+
]
|
@@ -0,0 +1,50 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from collections.abc import AsyncIterator
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
|
6
|
+
class EventStreamRepository(ABC):
|
7
|
+
"""
|
8
|
+
Interface for event streaming repositories.
|
9
|
+
Used to publish and subscribe to event streams.
|
10
|
+
"""
|
11
|
+
|
12
|
+
@abstractmethod
|
13
|
+
async def send_event(self, topic: str, event: dict[str, Any]) -> str:
|
14
|
+
"""
|
15
|
+
Send an event to a stream.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
topic: The stream topic/name
|
19
|
+
event: The event data
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
The message ID or other identifier
|
23
|
+
"""
|
24
|
+
raise NotImplementedError
|
25
|
+
|
26
|
+
@abstractmethod
|
27
|
+
async def subscribe(
|
28
|
+
self, topic: str, last_id: str = "$"
|
29
|
+
) -> AsyncIterator[dict[str, Any]]:
|
30
|
+
"""
|
31
|
+
Subscribe to a stream and yield events as they come in.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
topic: The stream topic to subscribe to
|
35
|
+
last_id: Where to start reading from
|
36
|
+
|
37
|
+
Yields:
|
38
|
+
Event data
|
39
|
+
"""
|
40
|
+
raise NotImplementedError
|
41
|
+
|
42
|
+
@abstractmethod
|
43
|
+
async def cleanup_stream(self, topic: str) -> None:
|
44
|
+
"""
|
45
|
+
Clean up a stream.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
topic: The stream topic to clean up
|
49
|
+
"""
|
50
|
+
raise NotImplementedError
|
@@ -0,0 +1 @@
|
|
1
|
+
|
File without changes
|
@@ -0,0 +1,181 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
from datetime import timedelta
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
from temporalio.client import Client, WorkflowExecutionStatus
|
6
|
+
from temporalio.common import RetryPolicy as TemporalRetryPolicy
|
7
|
+
from temporalio.common import WorkflowIDReusePolicy
|
8
|
+
from temporalio.service import RPCError, RPCStatusCode
|
9
|
+
|
10
|
+
from agentex.lib.core.clients.temporal.types import (
|
11
|
+
DuplicateWorkflowPolicy,
|
12
|
+
RetryPolicy,
|
13
|
+
TaskStatus,
|
14
|
+
WorkflowState,
|
15
|
+
)
|
16
|
+
from agentex.lib.core.clients.temporal.utils import get_temporal_client
|
17
|
+
from agentex.lib.utils.logging import make_logger
|
18
|
+
from agentex.lib.utils.model_utils import BaseModel
|
19
|
+
|
20
|
+
logger = make_logger(__name__)
|
21
|
+
|
22
|
+
DEFAULT_RETRY_POLICY = RetryPolicy(maximum_attempts=1)
|
23
|
+
|
24
|
+
|
25
|
+
TEMPORAL_STATUS_TO_UPLOAD_STATUS_AND_REASON = {
|
26
|
+
# TODO: Support canceled status
|
27
|
+
WorkflowExecutionStatus.CANCELED: WorkflowState(
|
28
|
+
status=TaskStatus.CANCELED,
|
29
|
+
reason="Task canceled by the user.",
|
30
|
+
is_terminal=True,
|
31
|
+
),
|
32
|
+
WorkflowExecutionStatus.COMPLETED: WorkflowState(
|
33
|
+
status=TaskStatus.COMPLETED,
|
34
|
+
reason="Task completed successfully.",
|
35
|
+
is_terminal=True,
|
36
|
+
),
|
37
|
+
WorkflowExecutionStatus.FAILED: WorkflowState(
|
38
|
+
status=TaskStatus.FAILED,
|
39
|
+
reason="Task encountered terminal failure. "
|
40
|
+
"Please contact support if retrying does not resolve the issue.",
|
41
|
+
is_terminal=True,
|
42
|
+
),
|
43
|
+
WorkflowExecutionStatus.RUNNING: WorkflowState(
|
44
|
+
status=TaskStatus.RUNNING,
|
45
|
+
reason="Task is running.",
|
46
|
+
is_terminal=False,
|
47
|
+
),
|
48
|
+
WorkflowExecutionStatus.TERMINATED: WorkflowState(
|
49
|
+
status=TaskStatus.CANCELED,
|
50
|
+
reason="Task canceled by the user.",
|
51
|
+
is_terminal=True,
|
52
|
+
),
|
53
|
+
WorkflowExecutionStatus.TIMED_OUT: WorkflowState(
|
54
|
+
status=TaskStatus.FAILED,
|
55
|
+
reason="Task timed out. Please contact support if retrying does not resolve the issue",
|
56
|
+
is_terminal=True,
|
57
|
+
),
|
58
|
+
WorkflowExecutionStatus.CONTINUED_AS_NEW: WorkflowState(
|
59
|
+
status=TaskStatus.RUNNING,
|
60
|
+
reason="Task is running.",
|
61
|
+
is_terminal=False,
|
62
|
+
),
|
63
|
+
}
|
64
|
+
|
65
|
+
DUPLICATE_POLICY_TO_ID_REUSE_POLICY = {
|
66
|
+
DuplicateWorkflowPolicy.ALLOW_DUPLICATE: WorkflowIDReusePolicy.ALLOW_DUPLICATE,
|
67
|
+
DuplicateWorkflowPolicy.ALLOW_DUPLICATE_FAILED_ONLY: WorkflowIDReusePolicy.ALLOW_DUPLICATE_FAILED_ONLY,
|
68
|
+
DuplicateWorkflowPolicy.REJECT_DUPLICATE: WorkflowIDReusePolicy.REJECT_DUPLICATE,
|
69
|
+
DuplicateWorkflowPolicy.TERMINATE_IF_RUNNING: WorkflowIDReusePolicy.TERMINATE_IF_RUNNING,
|
70
|
+
}
|
71
|
+
|
72
|
+
|
73
|
+
class TemporalClient:
|
74
|
+
def __init__(self, temporal_client: Client | None = None):
|
75
|
+
self._client: Client = temporal_client
|
76
|
+
|
77
|
+
@classmethod
|
78
|
+
async def create(cls, temporal_address: str):
|
79
|
+
if temporal_address in [
|
80
|
+
"false",
|
81
|
+
"False",
|
82
|
+
"null",
|
83
|
+
"None",
|
84
|
+
"",
|
85
|
+
"undefined",
|
86
|
+
False,
|
87
|
+
None,
|
88
|
+
]:
|
89
|
+
_client = None
|
90
|
+
else:
|
91
|
+
_client = await get_temporal_client(temporal_address)
|
92
|
+
return cls(_client)
|
93
|
+
|
94
|
+
async def setup(self, temporal_address: str):
|
95
|
+
self._client = await self._get_temporal_client(
|
96
|
+
temporal_address=temporal_address
|
97
|
+
)
|
98
|
+
|
99
|
+
async def _get_temporal_client(self, temporal_address: str) -> Client:
|
100
|
+
if temporal_address in [
|
101
|
+
"false",
|
102
|
+
"False",
|
103
|
+
"null",
|
104
|
+
"None",
|
105
|
+
"",
|
106
|
+
"undefined",
|
107
|
+
False,
|
108
|
+
None,
|
109
|
+
]:
|
110
|
+
return None
|
111
|
+
else:
|
112
|
+
return await get_temporal_client(temporal_address)
|
113
|
+
|
114
|
+
async def start_workflow(
|
115
|
+
self,
|
116
|
+
*args: Any,
|
117
|
+
duplicate_policy: DuplicateWorkflowPolicy = DuplicateWorkflowPolicy.ALLOW_DUPLICATE,
|
118
|
+
retry_policy: RetryPolicy = DEFAULT_RETRY_POLICY,
|
119
|
+
task_timeout: timedelta = timedelta(seconds=10),
|
120
|
+
execution_timeout: timedelta = timedelta(seconds=86400),
|
121
|
+
**kwargs: Any,
|
122
|
+
) -> str:
|
123
|
+
temporal_retry_policy = TemporalRetryPolicy(
|
124
|
+
**retry_policy.model_dump(exclude_unset=True)
|
125
|
+
)
|
126
|
+
workflow_handle = await self._client.start_workflow(
|
127
|
+
*args,
|
128
|
+
retry_policy=temporal_retry_policy,
|
129
|
+
task_timeout=task_timeout,
|
130
|
+
execution_timeout=execution_timeout,
|
131
|
+
id_reuse_policy=DUPLICATE_POLICY_TO_ID_REUSE_POLICY[duplicate_policy],
|
132
|
+
**kwargs,
|
133
|
+
)
|
134
|
+
return workflow_handle.id
|
135
|
+
|
136
|
+
async def send_signal(
|
137
|
+
self,
|
138
|
+
workflow_id: str,
|
139
|
+
signal: str | Callable[[dict[str, Any] | list[Any] | str | int | float | bool | BaseModel], Any],
|
140
|
+
payload: dict[str, Any] | list[Any] | str | int | float | bool | BaseModel,
|
141
|
+
) -> None:
|
142
|
+
handle = self._client.get_workflow_handle(workflow_id=workflow_id)
|
143
|
+
await handle.signal(signal, payload)
|
144
|
+
|
145
|
+
async def query_workflow(
|
146
|
+
self,
|
147
|
+
workflow_id: str,
|
148
|
+
query: str | Callable[[dict[str, Any] | list[Any] | str | int | float | bool | BaseModel], Any],
|
149
|
+
) -> Any:
|
150
|
+
"""
|
151
|
+
Submit a query to a workflow by name and return the results.
|
152
|
+
|
153
|
+
Args:
|
154
|
+
workflow_id: The ID of the workflow to query
|
155
|
+
query: The name of the query or a callable query function
|
156
|
+
|
157
|
+
Returns:
|
158
|
+
The result of the query
|
159
|
+
"""
|
160
|
+
handle = self._client.get_workflow_handle(workflow_id=workflow_id)
|
161
|
+
return await handle.query(query)
|
162
|
+
|
163
|
+
async def get_workflow_status(self, workflow_id: str) -> WorkflowState:
|
164
|
+
try:
|
165
|
+
handle = self._client.get_workflow_handle(workflow_id=workflow_id)
|
166
|
+
description = await handle.describe()
|
167
|
+
return TEMPORAL_STATUS_TO_UPLOAD_STATUS_AND_REASON[description.status]
|
168
|
+
except RPCError as e:
|
169
|
+
if e.status == RPCStatusCode.NOT_FOUND:
|
170
|
+
return WorkflowState(
|
171
|
+
status="NOT_FOUND",
|
172
|
+
reason="Workflow not found",
|
173
|
+
is_terminal=True,
|
174
|
+
)
|
175
|
+
raise
|
176
|
+
|
177
|
+
async def terminate_workflow(self, workflow_id: str) -> None:
|
178
|
+
return await self._client.get_workflow_handle(workflow_id).terminate()
|
179
|
+
|
180
|
+
async def cancel_workflow(self, workflow_id: str) -> None:
|
181
|
+
return await self._client.get_workflow_handle(workflow_id).cancel()
|
@@ -0,0 +1,47 @@
|
|
1
|
+
from datetime import timedelta
|
2
|
+
from enum import Enum
|
3
|
+
|
4
|
+
from pydantic import Field
|
5
|
+
|
6
|
+
from agentex.lib.utils.model_utils import BaseModel
|
7
|
+
|
8
|
+
|
9
|
+
class WorkflowState(BaseModel):
|
10
|
+
status: str
|
11
|
+
is_terminal: bool
|
12
|
+
reason: str | None = None
|
13
|
+
|
14
|
+
|
15
|
+
class RetryPolicy(BaseModel):
|
16
|
+
initial_interval: timedelta = Field(
|
17
|
+
timedelta(seconds=1),
|
18
|
+
description="Backoff interval for the first retry. Default 1s.",
|
19
|
+
)
|
20
|
+
backoff_coefficient: float = Field(
|
21
|
+
2.0,
|
22
|
+
description="Coefficient to multiply previous backoff interval by to get new interval. Default 2.0.",
|
23
|
+
)
|
24
|
+
maximum_interval: timedelta | None = Field(
|
25
|
+
None,
|
26
|
+
description="Maximum backoff interval between retries. Default 100x :py:attr:`initial_interval`.",
|
27
|
+
)
|
28
|
+
maximum_attempts: int = Field(
|
29
|
+
0,
|
30
|
+
description="Maximum number of attempts. If 0, the default, there is no maximum.",
|
31
|
+
)
|
32
|
+
|
33
|
+
|
34
|
+
class DuplicateWorkflowPolicy(str, Enum):
|
35
|
+
ALLOW_DUPLICATE = "ALLOW_DUPLICATE"
|
36
|
+
ALLOW_DUPLICATE_FAILED_ONLY = "ALLOW_DUPLICATE_FAILED_ONLY"
|
37
|
+
REJECT_DUPLICATE = "REJECT_DUPLICATE"
|
38
|
+
TERMINATE_IF_RUNNING = "TERMINATE_IF_RUNNING"
|
39
|
+
|
40
|
+
|
41
|
+
class TaskStatus(str, Enum):
|
42
|
+
CANCELED = "CANCELED"
|
43
|
+
COMPLETED = "COMPLETED"
|
44
|
+
FAILED = "FAILED"
|
45
|
+
RUNNING = "RUNNING"
|
46
|
+
TERMINATED = "TERMINATED"
|
47
|
+
TIMED_OUT = "TIMED_OUT"
|
@@ -0,0 +1,56 @@
|
|
1
|
+
from temporalio.client import Client
|
2
|
+
from temporalio.contrib.pydantic import pydantic_data_converter
|
3
|
+
from temporalio.runtime import OpenTelemetryConfig, Runtime, TelemetryConfig
|
4
|
+
|
5
|
+
# class DateTimeJSONEncoder(AdvancedJSONEncoder):
|
6
|
+
# def default(self, o: Any) -> Any:
|
7
|
+
# if isinstance(o, datetime.datetime):
|
8
|
+
# return o.isoformat()
|
9
|
+
# return super().default(o)
|
10
|
+
|
11
|
+
|
12
|
+
# class DateTimeJSONTypeConverter(JSONTypeConverter):
|
13
|
+
# def to_typed_value(
|
14
|
+
# self, hint: Type, value: Any
|
15
|
+
# ) -> Union[Optional[Any], _JSONTypeConverterUnhandled]:
|
16
|
+
# if hint == datetime.datetime:
|
17
|
+
# return datetime.datetime.fromisoformat(value)
|
18
|
+
# return JSONTypeConverter.Unhandled
|
19
|
+
|
20
|
+
|
21
|
+
# class DateTimePayloadConverter(CompositePayloadConverter):
|
22
|
+
# def __init__(self) -> None:
|
23
|
+
# json_converter = JSONPlainPayloadConverter(
|
24
|
+
# encoder=DateTimeJSONEncoder,
|
25
|
+
# custom_type_converters=[DateTimeJSONTypeConverter()],
|
26
|
+
# )
|
27
|
+
# super().__init__(
|
28
|
+
# *[
|
29
|
+
# c if not isinstance(c, JSONPlainPayloadConverter) else json_converter
|
30
|
+
# for c in DefaultPayloadConverter.default_encoding_payload_converters
|
31
|
+
# ]
|
32
|
+
# )
|
33
|
+
|
34
|
+
|
35
|
+
# custom_data_converter = dataclasses.replace(
|
36
|
+
# DataConverter.default,
|
37
|
+
# payload_converter_class=DateTimePayloadConverter,
|
38
|
+
# )
|
39
|
+
|
40
|
+
|
41
|
+
async def get_temporal_client(temporal_address: str, metrics_url: str = None) -> Client:
|
42
|
+
if not metrics_url:
|
43
|
+
client = await Client.connect(
|
44
|
+
target_host=temporal_address,
|
45
|
+
# data_converter=custom_data_converter,
|
46
|
+
data_converter=pydantic_data_converter,
|
47
|
+
)
|
48
|
+
else:
|
49
|
+
runtime = Runtime(telemetry=TelemetryConfig(metrics=OpenTelemetryConfig(url=metrics_url)))
|
50
|
+
client = await Client.connect(
|
51
|
+
target_host=temporal_address,
|
52
|
+
# data_converter=custom_data_converter,
|
53
|
+
data_converter=pydantic_data_converter,
|
54
|
+
runtime=runtime,
|
55
|
+
)
|
56
|
+
return client
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,210 @@
|
|
1
|
+
from typing import Any, cast
|
2
|
+
|
3
|
+
from agentex import AsyncAgentex
|
4
|
+
from agentex.lib.core.tracing.tracer import AsyncTracer
|
5
|
+
from agentex.lib.utils.logging import make_logger
|
6
|
+
from agentex.lib.utils.temporal import heartbeat_if_in_workflow
|
7
|
+
from agentex.types.event import Event
|
8
|
+
from agentex.types.task import Task
|
9
|
+
from agentex.types.task_message import TaskMessage
|
10
|
+
from agentex.types.task_message_content import TaskMessageContent
|
11
|
+
from agentex.types.task_message_content_param import TaskMessageContentParam
|
12
|
+
|
13
|
+
logger = make_logger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
class ACPService:
|
17
|
+
def __init__(
|
18
|
+
self,
|
19
|
+
agentex_client: AsyncAgentex,
|
20
|
+
tracer: AsyncTracer,
|
21
|
+
):
|
22
|
+
self._agentex_client = agentex_client
|
23
|
+
self._tracer = tracer
|
24
|
+
|
25
|
+
async def task_create(
|
26
|
+
self,
|
27
|
+
name: str | None = None,
|
28
|
+
agent_id: str | None = None,
|
29
|
+
agent_name: str | None = None,
|
30
|
+
params: dict[str, Any] | None = None,
|
31
|
+
trace_id: str | None = None,
|
32
|
+
parent_span_id: str | None = None,
|
33
|
+
) -> Task:
|
34
|
+
trace = self._tracer.trace(trace_id=trace_id)
|
35
|
+
async with trace.span(
|
36
|
+
parent_id=parent_span_id,
|
37
|
+
name="task_create",
|
38
|
+
input={
|
39
|
+
"name": name,
|
40
|
+
"agent_id": agent_id,
|
41
|
+
"agent_name": agent_name,
|
42
|
+
"params": params,
|
43
|
+
},
|
44
|
+
) as span:
|
45
|
+
heartbeat_if_in_workflow("task create")
|
46
|
+
if agent_name:
|
47
|
+
json_rpc_response = await self._agentex_client.agents.rpc_by_name(
|
48
|
+
agent_name=agent_name,
|
49
|
+
method="task/create",
|
50
|
+
params={
|
51
|
+
"name": name,
|
52
|
+
"params": params,
|
53
|
+
},
|
54
|
+
)
|
55
|
+
elif agent_id:
|
56
|
+
json_rpc_response = await self._agentex_client.agents.rpc(
|
57
|
+
agent_id=agent_id,
|
58
|
+
method="task/create",
|
59
|
+
params={
|
60
|
+
"name": name,
|
61
|
+
"params": params,
|
62
|
+
},
|
63
|
+
)
|
64
|
+
else:
|
65
|
+
raise ValueError("Either agent_name or agent_id must be provided")
|
66
|
+
|
67
|
+
task_entry = Task.model_validate(json_rpc_response["result"])
|
68
|
+
if span:
|
69
|
+
span.output = task_entry.model_dump()
|
70
|
+
return task_entry
|
71
|
+
|
72
|
+
async def message_send(
|
73
|
+
self,
|
74
|
+
content: TaskMessageContent,
|
75
|
+
agent_id: str | None = None,
|
76
|
+
agent_name: str | None = None,
|
77
|
+
task_id: str | None = None,
|
78
|
+
task_name: str | None = None,
|
79
|
+
trace_id: str | None = None,
|
80
|
+
parent_span_id: str | None = None,
|
81
|
+
) -> TaskMessage:
|
82
|
+
trace = self._tracer.trace(trace_id=trace_id)
|
83
|
+
async with trace.span(
|
84
|
+
parent_id=parent_span_id,
|
85
|
+
name="message_send",
|
86
|
+
input={
|
87
|
+
"agent_id": agent_id,
|
88
|
+
"agent_name": agent_name,
|
89
|
+
"task_id": task_id,
|
90
|
+
"task_name": task_name,
|
91
|
+
"message": content,
|
92
|
+
},
|
93
|
+
) as span:
|
94
|
+
heartbeat_if_in_workflow("message send")
|
95
|
+
if agent_name:
|
96
|
+
json_rpc_response = await self._agentex_client.agents.rpc_by_name(
|
97
|
+
agent_name=agent_name,
|
98
|
+
method="message/send",
|
99
|
+
params={
|
100
|
+
"task_id": task_id,
|
101
|
+
"content": cast(TaskMessageContentParam, content.model_dump()),
|
102
|
+
"stream": False,
|
103
|
+
},
|
104
|
+
)
|
105
|
+
elif agent_id:
|
106
|
+
json_rpc_response = await self._agentex_client.agents.rpc(
|
107
|
+
agent_id=agent_id,
|
108
|
+
method="message/send",
|
109
|
+
params={
|
110
|
+
"task_id": task_id,
|
111
|
+
"content": cast(TaskMessageContentParam, content.model_dump()),
|
112
|
+
"stream": False,
|
113
|
+
},
|
114
|
+
)
|
115
|
+
else:
|
116
|
+
raise ValueError("Either agent_name or agent_id must be provided")
|
117
|
+
|
118
|
+
task_message = TaskMessage.model_validate(json_rpc_response["result"])
|
119
|
+
if span:
|
120
|
+
span.output = task_message.model_dump()
|
121
|
+
return task_message
|
122
|
+
|
123
|
+
async def event_send(
|
124
|
+
self,
|
125
|
+
content: TaskMessageContent,
|
126
|
+
agent_id: str | None = None,
|
127
|
+
agent_name: str | None = None,
|
128
|
+
task_id: str | None = None,
|
129
|
+
task_name: str | None = None,
|
130
|
+
trace_id: str | None = None,
|
131
|
+
parent_span_id: str | None = None,
|
132
|
+
) -> Event:
|
133
|
+
trace = self._tracer.trace(trace_id=trace_id)
|
134
|
+
async with trace.span(
|
135
|
+
parent_id=parent_span_id,
|
136
|
+
name="event_send",
|
137
|
+
input={
|
138
|
+
"agent_id": agent_id,
|
139
|
+
"agent_name": agent_name,
|
140
|
+
"task_id": task_id,
|
141
|
+
"content": content,
|
142
|
+
},
|
143
|
+
) as span:
|
144
|
+
heartbeat_if_in_workflow("event send")
|
145
|
+
if agent_name:
|
146
|
+
json_rpc_response = await self._agentex_client.agents.rpc_by_name(
|
147
|
+
agent_name=agent_name,
|
148
|
+
method="event/send",
|
149
|
+
params={
|
150
|
+
"task_name": task_name,
|
151
|
+
"content": cast(TaskMessageContentParam, content.model_dump()),
|
152
|
+
},
|
153
|
+
)
|
154
|
+
elif agent_id:
|
155
|
+
json_rpc_response = await self._agentex_client.agents.rpc(
|
156
|
+
agent_id=agent_id,
|
157
|
+
method="event/send",
|
158
|
+
params={
|
159
|
+
"task_id": task_id,
|
160
|
+
"content": cast(TaskMessageContentParam, content.model_dump()),
|
161
|
+
},
|
162
|
+
)
|
163
|
+
else:
|
164
|
+
raise ValueError("Either agent_name or agent_id must be provided")
|
165
|
+
|
166
|
+
event_entry = Event.model_validate(json_rpc_response["result"])
|
167
|
+
if span:
|
168
|
+
span.output = event_entry.model_dump()
|
169
|
+
return event_entry
|
170
|
+
|
171
|
+
async def task_cancel(
|
172
|
+
self,
|
173
|
+
task_id: str | None = None,
|
174
|
+
task_name: str | None = None,
|
175
|
+
trace_id: str | None = None,
|
176
|
+
parent_span_id: str | None = None,
|
177
|
+
) -> Task:
|
178
|
+
trace = self._tracer.trace(trace_id=trace_id)
|
179
|
+
async with trace.span(
|
180
|
+
parent_id=parent_span_id,
|
181
|
+
name="task_cancel",
|
182
|
+
input={
|
183
|
+
"task_id": task_id,
|
184
|
+
"task_name": task_name,
|
185
|
+
},
|
186
|
+
) as span:
|
187
|
+
heartbeat_if_in_workflow("task cancel")
|
188
|
+
if task_name:
|
189
|
+
json_rpc_response = await self._agentex_client.agents.rpc_by_name(
|
190
|
+
agent_name=task_name,
|
191
|
+
method="task/cancel",
|
192
|
+
params={
|
193
|
+
"task_name": task_name,
|
194
|
+
},
|
195
|
+
)
|
196
|
+
elif task_id:
|
197
|
+
json_rpc_response = await self._agentex_client.agents.rpc(
|
198
|
+
agent_id=task_id,
|
199
|
+
method="task/cancel",
|
200
|
+
params={
|
201
|
+
"task_id": task_id,
|
202
|
+
},
|
203
|
+
)
|
204
|
+
else:
|
205
|
+
raise ValueError("Either task_name or task_id must be provided")
|
206
|
+
|
207
|
+
task_entry = Task.model_validate(json_rpc_response["result"])
|
208
|
+
if span:
|
209
|
+
span.output = task_entry.model_dump()
|
210
|
+
return task_entry
|