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.
Files changed (289) hide show
  1. agentex/__init__.py +103 -0
  2. agentex/_base_client.py +1992 -0
  3. agentex/_client.py +506 -0
  4. agentex/_compat.py +219 -0
  5. agentex/_constants.py +14 -0
  6. agentex/_exceptions.py +108 -0
  7. agentex/_files.py +123 -0
  8. agentex/_models.py +829 -0
  9. agentex/_qs.py +150 -0
  10. agentex/_resource.py +43 -0
  11. agentex/_response.py +830 -0
  12. agentex/_streaming.py +333 -0
  13. agentex/_types.py +219 -0
  14. agentex/_utils/__init__.py +57 -0
  15. agentex/_utils/_logs.py +25 -0
  16. agentex/_utils/_proxy.py +65 -0
  17. agentex/_utils/_reflection.py +42 -0
  18. agentex/_utils/_resources_proxy.py +24 -0
  19. agentex/_utils/_streams.py +12 -0
  20. agentex/_utils/_sync.py +86 -0
  21. agentex/_utils/_transform.py +447 -0
  22. agentex/_utils/_typing.py +151 -0
  23. agentex/_utils/_utils.py +422 -0
  24. agentex/_version.py +4 -0
  25. agentex/lib/.keep +4 -0
  26. agentex/lib/__init__.py +0 -0
  27. agentex/lib/adk/__init__.py +41 -0
  28. agentex/lib/adk/_modules/__init__.py +0 -0
  29. agentex/lib/adk/_modules/acp.py +247 -0
  30. agentex/lib/adk/_modules/agent_task_tracker.py +176 -0
  31. agentex/lib/adk/_modules/agents.py +77 -0
  32. agentex/lib/adk/_modules/events.py +141 -0
  33. agentex/lib/adk/_modules/messages.py +285 -0
  34. agentex/lib/adk/_modules/state.py +291 -0
  35. agentex/lib/adk/_modules/streaming.py +75 -0
  36. agentex/lib/adk/_modules/tasks.py +124 -0
  37. agentex/lib/adk/_modules/tracing.py +194 -0
  38. agentex/lib/adk/providers/__init__.py +9 -0
  39. agentex/lib/adk/providers/_modules/__init__.py +0 -0
  40. agentex/lib/adk/providers/_modules/litellm.py +232 -0
  41. agentex/lib/adk/providers/_modules/openai.py +416 -0
  42. agentex/lib/adk/providers/_modules/sgp.py +85 -0
  43. agentex/lib/adk/utils/__init__.py +5 -0
  44. agentex/lib/adk/utils/_modules/__init__.py +0 -0
  45. agentex/lib/adk/utils/_modules/templating.py +94 -0
  46. agentex/lib/cli/__init__.py +0 -0
  47. agentex/lib/cli/commands/__init__.py +0 -0
  48. agentex/lib/cli/commands/agents.py +328 -0
  49. agentex/lib/cli/commands/init.py +227 -0
  50. agentex/lib/cli/commands/main.py +33 -0
  51. agentex/lib/cli/commands/secrets.py +169 -0
  52. agentex/lib/cli/commands/tasks.py +118 -0
  53. agentex/lib/cli/commands/uv.py +133 -0
  54. agentex/lib/cli/handlers/__init__.py +0 -0
  55. agentex/lib/cli/handlers/agent_handlers.py +160 -0
  56. agentex/lib/cli/handlers/cleanup_handlers.py +186 -0
  57. agentex/lib/cli/handlers/deploy_handlers.py +351 -0
  58. agentex/lib/cli/handlers/run_handlers.py +452 -0
  59. agentex/lib/cli/handlers/secret_handlers.py +670 -0
  60. agentex/lib/cli/templates/default/.dockerignore.j2 +43 -0
  61. agentex/lib/cli/templates/default/Dockerfile-uv.j2 +42 -0
  62. agentex/lib/cli/templates/default/Dockerfile.j2 +42 -0
  63. agentex/lib/cli/templates/default/README.md.j2 +193 -0
  64. agentex/lib/cli/templates/default/deploy/example.yaml.j2 +55 -0
  65. agentex/lib/cli/templates/default/manifest.yaml.j2 +116 -0
  66. agentex/lib/cli/templates/default/project/acp.py.j2 +29 -0
  67. agentex/lib/cli/templates/default/pyproject.toml.j2 +33 -0
  68. agentex/lib/cli/templates/default/requirements.txt.j2 +5 -0
  69. agentex/lib/cli/templates/deploy/Screenshot 2025-03-19 at 10.36.57/342/200/257AM.png +0 -0
  70. agentex/lib/cli/templates/deploy/example.yaml.j2 +55 -0
  71. agentex/lib/cli/templates/sync/.dockerignore.j2 +43 -0
  72. agentex/lib/cli/templates/sync/Dockerfile-uv.j2 +42 -0
  73. agentex/lib/cli/templates/sync/Dockerfile.j2 +42 -0
  74. agentex/lib/cli/templates/sync/README.md.j2 +293 -0
  75. agentex/lib/cli/templates/sync/deploy/example.yaml.j2 +55 -0
  76. agentex/lib/cli/templates/sync/manifest.yaml.j2 +116 -0
  77. agentex/lib/cli/templates/sync/project/acp.py.j2 +26 -0
  78. agentex/lib/cli/templates/sync/pyproject.toml.j2 +33 -0
  79. agentex/lib/cli/templates/sync/requirements.txt.j2 +5 -0
  80. agentex/lib/cli/templates/temporal/.dockerignore.j2 +43 -0
  81. agentex/lib/cli/templates/temporal/Dockerfile-uv.j2 +48 -0
  82. agentex/lib/cli/templates/temporal/Dockerfile.j2 +48 -0
  83. agentex/lib/cli/templates/temporal/README.md.j2 +316 -0
  84. agentex/lib/cli/templates/temporal/deploy/example.yaml.j2 +55 -0
  85. agentex/lib/cli/templates/temporal/manifest.yaml.j2 +137 -0
  86. agentex/lib/cli/templates/temporal/project/acp.py.j2 +30 -0
  87. agentex/lib/cli/templates/temporal/project/run_worker.py.j2 +33 -0
  88. agentex/lib/cli/templates/temporal/project/workflow.py.j2 +66 -0
  89. agentex/lib/cli/templates/temporal/pyproject.toml.j2 +34 -0
  90. agentex/lib/cli/templates/temporal/requirements.txt.j2 +5 -0
  91. agentex/lib/cli/utils/cli_utils.py +14 -0
  92. agentex/lib/cli/utils/credential_utils.py +103 -0
  93. agentex/lib/cli/utils/exceptions.py +6 -0
  94. agentex/lib/cli/utils/kubectl_utils.py +135 -0
  95. agentex/lib/cli/utils/kubernetes_secrets_utils.py +185 -0
  96. agentex/lib/core/__init__.py +0 -0
  97. agentex/lib/core/adapters/__init__.py +0 -0
  98. agentex/lib/core/adapters/llm/__init__.py +1 -0
  99. agentex/lib/core/adapters/llm/adapter_litellm.py +46 -0
  100. agentex/lib/core/adapters/llm/adapter_sgp.py +55 -0
  101. agentex/lib/core/adapters/llm/port.py +24 -0
  102. agentex/lib/core/adapters/streams/adapter_redis.py +128 -0
  103. agentex/lib/core/adapters/streams/port.py +50 -0
  104. agentex/lib/core/clients/__init__.py +1 -0
  105. agentex/lib/core/clients/temporal/__init__.py +0 -0
  106. agentex/lib/core/clients/temporal/temporal_client.py +181 -0
  107. agentex/lib/core/clients/temporal/types.py +47 -0
  108. agentex/lib/core/clients/temporal/utils.py +56 -0
  109. agentex/lib/core/services/__init__.py +0 -0
  110. agentex/lib/core/services/adk/__init__.py +0 -0
  111. agentex/lib/core/services/adk/acp/__init__.py +0 -0
  112. agentex/lib/core/services/adk/acp/acp.py +210 -0
  113. agentex/lib/core/services/adk/agent_task_tracker.py +85 -0
  114. agentex/lib/core/services/adk/agents.py +43 -0
  115. agentex/lib/core/services/adk/events.py +61 -0
  116. agentex/lib/core/services/adk/messages.py +164 -0
  117. agentex/lib/core/services/adk/providers/__init__.py +0 -0
  118. agentex/lib/core/services/adk/providers/litellm.py +256 -0
  119. agentex/lib/core/services/adk/providers/openai.py +723 -0
  120. agentex/lib/core/services/adk/providers/sgp.py +99 -0
  121. agentex/lib/core/services/adk/state.py +120 -0
  122. agentex/lib/core/services/adk/streaming.py +262 -0
  123. agentex/lib/core/services/adk/tasks.py +69 -0
  124. agentex/lib/core/services/adk/tracing.py +36 -0
  125. agentex/lib/core/services/adk/utils/__init__.py +0 -0
  126. agentex/lib/core/services/adk/utils/templating.py +58 -0
  127. agentex/lib/core/temporal/__init__.py +0 -0
  128. agentex/lib/core/temporal/activities/__init__.py +207 -0
  129. agentex/lib/core/temporal/activities/activity_helpers.py +37 -0
  130. agentex/lib/core/temporal/activities/adk/__init__.py +0 -0
  131. agentex/lib/core/temporal/activities/adk/acp/__init__.py +0 -0
  132. agentex/lib/core/temporal/activities/adk/acp/acp_activities.py +86 -0
  133. agentex/lib/core/temporal/activities/adk/agent_task_tracker_activities.py +76 -0
  134. agentex/lib/core/temporal/activities/adk/agents_activities.py +35 -0
  135. agentex/lib/core/temporal/activities/adk/events_activities.py +50 -0
  136. agentex/lib/core/temporal/activities/adk/messages_activities.py +94 -0
  137. agentex/lib/core/temporal/activities/adk/providers/__init__.py +0 -0
  138. agentex/lib/core/temporal/activities/adk/providers/litellm_activities.py +71 -0
  139. agentex/lib/core/temporal/activities/adk/providers/openai_activities.py +210 -0
  140. agentex/lib/core/temporal/activities/adk/providers/sgp_activities.py +42 -0
  141. agentex/lib/core/temporal/activities/adk/state_activities.py +85 -0
  142. agentex/lib/core/temporal/activities/adk/streaming_activities.py +33 -0
  143. agentex/lib/core/temporal/activities/adk/tasks_activities.py +48 -0
  144. agentex/lib/core/temporal/activities/adk/tracing_activities.py +55 -0
  145. agentex/lib/core/temporal/activities/adk/utils/__init__.py +0 -0
  146. agentex/lib/core/temporal/activities/adk/utils/templating_activities.py +41 -0
  147. agentex/lib/core/temporal/services/__init__.py +0 -0
  148. agentex/lib/core/temporal/services/temporal_task_service.py +69 -0
  149. agentex/lib/core/temporal/types/__init__.py +0 -0
  150. agentex/lib/core/temporal/types/workflow.py +5 -0
  151. agentex/lib/core/temporal/workers/__init__.py +0 -0
  152. agentex/lib/core/temporal/workers/worker.py +162 -0
  153. agentex/lib/core/temporal/workflows/workflow.py +26 -0
  154. agentex/lib/core/tracing/__init__.py +5 -0
  155. agentex/lib/core/tracing/processors/agentex_tracing_processor.py +117 -0
  156. agentex/lib/core/tracing/processors/sgp_tracing_processor.py +119 -0
  157. agentex/lib/core/tracing/processors/tracing_processor_interface.py +40 -0
  158. agentex/lib/core/tracing/trace.py +311 -0
  159. agentex/lib/core/tracing/tracer.py +70 -0
  160. agentex/lib/core/tracing/tracing_processor_manager.py +62 -0
  161. agentex/lib/environment_variables.py +87 -0
  162. agentex/lib/py.typed +0 -0
  163. agentex/lib/sdk/__init__.py +0 -0
  164. agentex/lib/sdk/config/__init__.py +0 -0
  165. agentex/lib/sdk/config/agent_config.py +61 -0
  166. agentex/lib/sdk/config/agent_manifest.py +219 -0
  167. agentex/lib/sdk/config/build_config.py +35 -0
  168. agentex/lib/sdk/config/deployment_config.py +117 -0
  169. agentex/lib/sdk/config/local_development_config.py +56 -0
  170. agentex/lib/sdk/config/project_config.py +103 -0
  171. agentex/lib/sdk/fastacp/__init__.py +3 -0
  172. agentex/lib/sdk/fastacp/base/base_acp_server.py +406 -0
  173. agentex/lib/sdk/fastacp/fastacp.py +74 -0
  174. agentex/lib/sdk/fastacp/impl/agentic_base_acp.py +72 -0
  175. agentex/lib/sdk/fastacp/impl/sync_acp.py +109 -0
  176. agentex/lib/sdk/fastacp/impl/temporal_acp.py +97 -0
  177. agentex/lib/sdk/fastacp/tests/README.md +297 -0
  178. agentex/lib/sdk/fastacp/tests/conftest.py +307 -0
  179. agentex/lib/sdk/fastacp/tests/pytest.ini +10 -0
  180. agentex/lib/sdk/fastacp/tests/run_tests.py +227 -0
  181. agentex/lib/sdk/fastacp/tests/test_base_acp_server.py +450 -0
  182. agentex/lib/sdk/fastacp/tests/test_fastacp_factory.py +344 -0
  183. agentex/lib/sdk/fastacp/tests/test_integration.py +477 -0
  184. agentex/lib/sdk/state_machine/__init__.py +6 -0
  185. agentex/lib/sdk/state_machine/noop_workflow.py +21 -0
  186. agentex/lib/sdk/state_machine/state.py +10 -0
  187. agentex/lib/sdk/state_machine/state_machine.py +189 -0
  188. agentex/lib/sdk/state_machine/state_workflow.py +16 -0
  189. agentex/lib/sdk/utils/__init__.py +0 -0
  190. agentex/lib/sdk/utils/messages.py +223 -0
  191. agentex/lib/types/__init__.py +0 -0
  192. agentex/lib/types/acp.py +94 -0
  193. agentex/lib/types/agent_configs.py +79 -0
  194. agentex/lib/types/agent_results.py +29 -0
  195. agentex/lib/types/credentials.py +34 -0
  196. agentex/lib/types/fastacp.py +61 -0
  197. agentex/lib/types/files.py +13 -0
  198. agentex/lib/types/json_rpc.py +49 -0
  199. agentex/lib/types/llm_messages.py +354 -0
  200. agentex/lib/types/task_message_updates.py +171 -0
  201. agentex/lib/types/tracing.py +34 -0
  202. agentex/lib/utils/__init__.py +0 -0
  203. agentex/lib/utils/completions.py +131 -0
  204. agentex/lib/utils/console.py +14 -0
  205. agentex/lib/utils/io.py +29 -0
  206. agentex/lib/utils/iterables.py +14 -0
  207. agentex/lib/utils/json_schema.py +23 -0
  208. agentex/lib/utils/logging.py +31 -0
  209. agentex/lib/utils/mcp.py +17 -0
  210. agentex/lib/utils/model_utils.py +46 -0
  211. agentex/lib/utils/parsing.py +15 -0
  212. agentex/lib/utils/regex.py +6 -0
  213. agentex/lib/utils/temporal.py +13 -0
  214. agentex/py.typed +0 -0
  215. agentex/resources/__init__.py +103 -0
  216. agentex/resources/agents.py +707 -0
  217. agentex/resources/events.py +294 -0
  218. agentex/resources/messages/__init__.py +33 -0
  219. agentex/resources/messages/batch.py +271 -0
  220. agentex/resources/messages/messages.py +492 -0
  221. agentex/resources/spans.py +557 -0
  222. agentex/resources/states.py +544 -0
  223. agentex/resources/tasks.py +615 -0
  224. agentex/resources/tracker.py +384 -0
  225. agentex/types/__init__.py +56 -0
  226. agentex/types/acp_type.py +7 -0
  227. agentex/types/agent.py +29 -0
  228. agentex/types/agent_list_params.py +13 -0
  229. agentex/types/agent_list_response.py +10 -0
  230. agentex/types/agent_rpc_by_name_params.py +21 -0
  231. agentex/types/agent_rpc_params.py +51 -0
  232. agentex/types/agent_rpc_params1.py +21 -0
  233. agentex/types/agent_rpc_response.py +20 -0
  234. agentex/types/agent_rpc_result.py +90 -0
  235. agentex/types/agent_task_tracker.py +34 -0
  236. agentex/types/data_content.py +30 -0
  237. agentex/types/data_content_param.py +31 -0
  238. agentex/types/data_delta.py +14 -0
  239. agentex/types/event.py +29 -0
  240. agentex/types/event_list_params.py +22 -0
  241. agentex/types/event_list_response.py +10 -0
  242. agentex/types/message_author.py +7 -0
  243. agentex/types/message_create_params.py +18 -0
  244. agentex/types/message_list_params.py +14 -0
  245. agentex/types/message_list_response.py +10 -0
  246. agentex/types/message_style.py +7 -0
  247. agentex/types/message_update_params.py +18 -0
  248. agentex/types/messages/__init__.py +8 -0
  249. agentex/types/messages/batch_create_params.py +16 -0
  250. agentex/types/messages/batch_create_response.py +10 -0
  251. agentex/types/messages/batch_update_params.py +16 -0
  252. agentex/types/messages/batch_update_response.py +10 -0
  253. agentex/types/shared/__init__.py +3 -0
  254. agentex/types/shared/task_message_update.py +83 -0
  255. agentex/types/span.py +36 -0
  256. agentex/types/span_create_params.py +40 -0
  257. agentex/types/span_list_params.py +12 -0
  258. agentex/types/span_list_response.py +10 -0
  259. agentex/types/span_update_params.py +37 -0
  260. agentex/types/state.py +25 -0
  261. agentex/types/state_create_params.py +16 -0
  262. agentex/types/state_list_params.py +16 -0
  263. agentex/types/state_list_response.py +10 -0
  264. agentex/types/state_update_params.py +16 -0
  265. agentex/types/task.py +23 -0
  266. agentex/types/task_delete_by_name_response.py +8 -0
  267. agentex/types/task_delete_response.py +8 -0
  268. agentex/types/task_list_response.py +10 -0
  269. agentex/types/task_message.py +33 -0
  270. agentex/types/task_message_content.py +16 -0
  271. agentex/types/task_message_content_param.py +17 -0
  272. agentex/types/task_message_delta.py +16 -0
  273. agentex/types/text_content.py +53 -0
  274. agentex/types/text_content_param.py +54 -0
  275. agentex/types/text_delta.py +14 -0
  276. agentex/types/tool_request_content.py +36 -0
  277. agentex/types/tool_request_content_param.py +37 -0
  278. agentex/types/tool_request_delta.py +18 -0
  279. agentex/types/tool_response_content.py +36 -0
  280. agentex/types/tool_response_content_param.py +36 -0
  281. agentex/types/tool_response_delta.py +18 -0
  282. agentex/types/tracker_list_params.py +16 -0
  283. agentex/types/tracker_list_response.py +10 -0
  284. agentex/types/tracker_update_params.py +19 -0
  285. agentex_sdk-0.1.0a6.dist-info/METADATA +426 -0
  286. agentex_sdk-0.1.0a6.dist-info/RECORD +289 -0
  287. agentex_sdk-0.1.0a6.dist-info/WHEEL +4 -0
  288. agentex_sdk-0.1.0a6.dist-info/entry_points.txt +2 -0
  289. 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