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,406 @@
1
+ import asyncio
2
+ import inspect
3
+ from collections.abc import AsyncGenerator, Awaitable, Callable
4
+ from contextlib import asynccontextmanager
5
+ from typing import Any
6
+
7
+ import httpx
8
+ import uvicorn
9
+ from fastapi import FastAPI, Request
10
+ from fastapi.responses import StreamingResponse
11
+ from pydantic import TypeAdapter, ValidationError
12
+
13
+ # from agentex.lib.sdk.fastacp.types import BaseACPConfig
14
+ from agentex.lib.environment_variables import EnvironmentVariables
15
+ from agentex.lib.types.acp import (
16
+ PARAMS_MODEL_BY_METHOD,
17
+ RPC_SYNC_METHODS,
18
+ CancelTaskParams,
19
+ CreateTaskParams,
20
+ RPCMethod,
21
+ SendEventParams,
22
+ SendMessageParams,
23
+ )
24
+ from agentex.lib.types.json_rpc import JSONRPCError, JSONRPCRequest, JSONRPCResponse
25
+ from agentex.lib.types.task_message_updates import StreamTaskMessageFull, TaskMessageUpdate
26
+ from agentex.types.task_message_content import TaskMessageContent
27
+ from agentex.lib.utils.logging import make_logger
28
+ from agentex.lib.utils.model_utils import BaseModel
29
+
30
+ logger = make_logger(__name__)
31
+
32
+ # Create a TypeAdapter for TaskMessageUpdate validation
33
+ task_message_update_adapter = TypeAdapter(TaskMessageUpdate)
34
+
35
+
36
+ class BaseACPServer(FastAPI):
37
+ """
38
+ AsyncAgentACP provides RPC-style hooks for agent events and commands asynchronously.
39
+ All methods follow JSON-RPC 2.0 format.
40
+
41
+ Available methods:
42
+ - event/send → Send a message to a task
43
+ - task/cancel → Cancel a task
44
+ - task/approve → Approve a task
45
+ """
46
+
47
+ def __init__(self):
48
+ super().__init__(lifespan=self.get_lifespan_function())
49
+
50
+ self.get("/healthz")(self._healthz)
51
+ self.post("/api")(self._handle_jsonrpc)
52
+
53
+ # Method handlers
54
+ self._handlers: dict[RPCMethod, Callable] = {}
55
+
56
+ @classmethod
57
+ def create(cls):
58
+ """Create and initialize BaseACPServer instance"""
59
+ instance = cls()
60
+ instance._setup_handlers()
61
+ return instance
62
+
63
+ def _setup_handlers(self):
64
+ """Set up default handlers - override in subclasses"""
65
+ # Base class has no default handlers
66
+ pass
67
+
68
+ def get_lifespan_function(self):
69
+ @asynccontextmanager
70
+ async def lifespan_context(app: FastAPI):
71
+ env_vars = EnvironmentVariables.refresh()
72
+ if env_vars.AGENTEX_BASE_URL:
73
+ await self._register_agent(env_vars)
74
+ else:
75
+ logger.warning("AGENTEX_BASE_URL not set, skipping agent registration")
76
+
77
+ yield
78
+
79
+ return lifespan_context
80
+
81
+ async def _healthz(self):
82
+ """Health check endpoint"""
83
+ return {"status": "healthy"}
84
+
85
+ def _wrap_handler(self, fn: Callable[..., Awaitable[Any]]):
86
+ """Wraps handler functions to provide JSON-RPC 2.0 response format"""
87
+
88
+ async def wrapper(*args, **kwargs) -> Any:
89
+ return await fn(*args, **kwargs)
90
+
91
+ return wrapper
92
+
93
+ async def _handle_jsonrpc(self, request: Request):
94
+ """Main JSON-RPC endpoint handler"""
95
+ rpc_request = None
96
+ try:
97
+ data = await request.json()
98
+ rpc_request = JSONRPCRequest(**data)
99
+
100
+ # Check if method is valid first
101
+ try:
102
+ method = RPCMethod(rpc_request.method)
103
+ except ValueError:
104
+ logger.error(f"Method {rpc_request.method} was invalid")
105
+ return JSONRPCResponse(
106
+ id=rpc_request.id,
107
+ error=JSONRPCError(
108
+ code=-32601, message=f"Method {rpc_request.method} not found"
109
+ ),
110
+ )
111
+
112
+ if method not in self._handlers or self._handlers[method] is None:
113
+ logger.error(f"Method {method} not found on existing ACP server")
114
+ return JSONRPCResponse(
115
+ id=rpc_request.id,
116
+ error=JSONRPCError(
117
+ code=-32601, message=f"Method {method} not found"
118
+ ),
119
+ )
120
+
121
+ # Parse params into appropriate model based on method
122
+ params_model = PARAMS_MODEL_BY_METHOD[method]
123
+ params = params_model.model_validate(rpc_request.params)
124
+
125
+ if method in RPC_SYNC_METHODS:
126
+ handler = self._handlers[method]
127
+ result = await handler(params)
128
+
129
+ if rpc_request.id is None:
130
+ # Seems like you should return None for notifications
131
+ return None
132
+ else:
133
+ # Handle streaming vs non-streaming for MESSAGE_SEND
134
+ if method == RPCMethod.MESSAGE_SEND and isinstance(
135
+ result, AsyncGenerator
136
+ ):
137
+ return await self._handle_streaming_response(
138
+ rpc_request.id, result
139
+ )
140
+ else:
141
+ if isinstance(result, BaseModel):
142
+ result = result.model_dump()
143
+ return JSONRPCResponse(id=rpc_request.id, result=result)
144
+ else:
145
+ # If this is a notification (no request ID), process in background and return immediately
146
+ if rpc_request.id is None:
147
+ asyncio.create_task(self._process_notification(method, params))
148
+ return JSONRPCResponse(id=None)
149
+
150
+ # For regular requests, start processing in background but return immediately
151
+ asyncio.create_task(
152
+ self._process_request(rpc_request.id, method, params)
153
+ )
154
+
155
+ # Return immediate acknowledgment
156
+ return JSONRPCResponse(
157
+ id=rpc_request.id, result={"status": "processing"}
158
+ )
159
+
160
+ except Exception as e:
161
+ logger.error(f"Error handling JSON-RPC request: {e}", exc_info=True)
162
+ request_id = None
163
+ if rpc_request is not None:
164
+ request_id = rpc_request.id
165
+ return JSONRPCResponse(
166
+ id=request_id,
167
+ error=JSONRPCError(code=-32603, message=str(e)).model_dump(),
168
+ )
169
+
170
+ async def _handle_streaming_response(
171
+ self, request_id: int | str, async_gen: AsyncGenerator
172
+ ):
173
+ """Handle streaming response by formatting TaskMessageUpdate objects as JSON-RPC stream"""
174
+
175
+ async def generate_json_rpc_stream():
176
+ try:
177
+ async for chunk in async_gen:
178
+ # Each chunk should be a TaskMessageUpdate object
179
+ # Validate using Pydantic's TypeAdapter to ensure it's a proper TaskMessageUpdate
180
+ try:
181
+ # This will validate that chunk conforms to the TaskMessageUpdate union type
182
+ validated_chunk = task_message_update_adapter.validate_python(
183
+ chunk
184
+ )
185
+ # Use mode="json" to properly serialize datetime objects
186
+ chunk_data = validated_chunk.model_dump(mode="json")
187
+ except ValidationError as e:
188
+ raise TypeError(
189
+ f"Streaming chunks must be TaskMessageUpdate objects. Validation error: {e}"
190
+ ) from e
191
+ except Exception as e:
192
+ raise TypeError(
193
+ f"Streaming chunks must be TaskMessageUpdate objects, got {type(chunk)}: {e}"
194
+ ) from e
195
+
196
+ # Wrap in JSON-RPC response format
197
+ response = JSONRPCResponse(id=request_id, result=chunk_data)
198
+ # Use model_dump_json() which handles datetime serialization automatically
199
+ yield f"{response.model_dump_json()}\n"
200
+
201
+ except Exception as e:
202
+ logger.error(f"Error in streaming response: {e}", exc_info=True)
203
+ error_response = JSONRPCResponse(
204
+ id=request_id,
205
+ error=JSONRPCError(code=-32603, message=str(e)).model_dump(),
206
+ )
207
+ yield f"{error_response.model_dump_json()}\n"
208
+
209
+ return StreamingResponse(
210
+ generate_json_rpc_stream(),
211
+ media_type="application/x-ndjson", # Newline Delimited JSON
212
+ headers={
213
+ "Cache-Control": "no-cache",
214
+ "Connection": "keep-alive",
215
+ "X-Accel-Buffering": "no", # Disable nginx buffering
216
+ },
217
+ )
218
+
219
+ async def _process_notification(self, method: RPCMethod, params: Any):
220
+ """Process a notification (request with no ID) in the background"""
221
+ try:
222
+ handler = self._handlers[method]
223
+ await handler(params)
224
+ except Exception as e:
225
+ logger.error(f"Error processing notification {method}: {e}", exc_info=True)
226
+
227
+ async def _process_request(
228
+ self, request_id: int | str, method: RPCMethod, params: Any
229
+ ):
230
+ """Process a request in the background"""
231
+ try:
232
+ handler = self._handlers[method]
233
+ await handler(params)
234
+ # Note: In a real implementation, you might want to store the result somewhere
235
+ # or notify the client through a different mechanism
236
+ logger.info(
237
+ f"Successfully processed request {request_id} for method {method}"
238
+ )
239
+ except Exception as e:
240
+ logger.error(
241
+ f"Error processing request {request_id} for method {method}: {e}",
242
+ exc_info=True,
243
+ )
244
+
245
+ """
246
+ Define all possible decorators to be overriden and implemented by each ACP implementation
247
+ Then the users can override the default handlers by implementing their own handlers
248
+
249
+ ACP Type: Agentic
250
+ Decorators:
251
+ - on_task_create
252
+ - on_task_event_send
253
+ - on_task_cancel
254
+
255
+ ACP Type: Sync
256
+ Decorators:
257
+ - on_message_send
258
+ """
259
+
260
+ # Type: Agentic
261
+ def on_task_create(self, fn: Callable[[CreateTaskParams], Awaitable[Any]]):
262
+ """Handle task/init method"""
263
+ wrapped = self._wrap_handler(fn)
264
+ self._handlers[RPCMethod.TASK_CREATE] = wrapped
265
+ return fn
266
+
267
+ # Type: Agentic
268
+ def on_task_event_send(self, fn: Callable[[SendEventParams], Awaitable[Any]]):
269
+ """Handle event/send method"""
270
+
271
+ async def wrapped_handler(params: SendEventParams):
272
+ # # # Send message to client first most of the time
273
+ # ## But, sometimes you may want to process the message first
274
+ # ## and then send a message to the client
275
+ # await agentex.interactions.send_messages_to_client(
276
+ # task_id=params.task_id,
277
+ # messages=[params.message]
278
+ # )
279
+ return await fn(params)
280
+
281
+ wrapped = self._wrap_handler(wrapped_handler)
282
+ self._handlers[RPCMethod.EVENT_SEND] = wrapped
283
+ return fn
284
+
285
+ # Type: Agentic
286
+ def on_task_cancel(self, fn: Callable[[CancelTaskParams], Awaitable[Any]]):
287
+ """Handle task/cancel method"""
288
+ wrapped = self._wrap_handler(fn)
289
+ self._handlers[RPCMethod.TASK_CANCEL] = wrapped
290
+ return fn
291
+
292
+ # Type: Sync
293
+ def on_message_send(
294
+ self,
295
+ fn: Callable[
296
+ [SendMessageParams],
297
+ Awaitable[TaskMessageContent | list[TaskMessageContent] | AsyncGenerator[TaskMessageUpdate, None]],
298
+ ],
299
+ ):
300
+ """Handle message/send method - supports both single and streaming responses
301
+
302
+ For non-streaming: return a single TaskMessage
303
+ For streaming: return an AsyncGenerator that yields TaskMessageUpdate objects
304
+ """
305
+
306
+ async def message_send_wrapper(params: SendMessageParams):
307
+ """Special wrapper for message_send that handles both regular async functions and async generators"""
308
+ # Check if the function is an async generator function
309
+
310
+ # Regardless of whether the Agent developer implemented an Async generator or not, we will always turn the function into an async generator and yield SSE events back tot he Agentex server so there is only one way for it to process the response. Then, based on the client's desire to stream or not, the Agentex server will either yield back the async generator objects directly (if streaming) or aggregate the content into a list of TaskMessageContents and to dispatch to the client. This basically gives the Agentex server the flexibility to handle both cases itself.
311
+
312
+ if inspect.isasyncgenfunction(fn):
313
+ # The client wants streaming, an async generator already streams the content, so just return it
314
+ return fn(params)
315
+ else:
316
+ # The client wants streaming, but the function is not an async generator, so we turn it into one and yield each TaskMessageContent as a StreamTaskMessageFull which will be streamed to the client by the Agentex server.
317
+ task_message_content_response = await fn(params)
318
+ if isinstance(task_message_content_response, list):
319
+ task_message_content_list = task_message_content_response
320
+ else:
321
+ task_message_content_list = [task_message_content_response]
322
+
323
+ async def async_generator(task_message_content_list: list[TaskMessageContent]):
324
+ for i, task_message_content in enumerate(task_message_content_list):
325
+ yield StreamTaskMessageFull(index=i, content=task_message_content)
326
+
327
+ return async_generator(task_message_content_list)
328
+
329
+ self._handlers[RPCMethod.MESSAGE_SEND] = message_send_wrapper
330
+ return fn
331
+
332
+ """
333
+ End of Decorators
334
+ """
335
+
336
+ """
337
+ ACP Server Lifecycle Methods
338
+ """
339
+
340
+ def run(self, host: str = "0.0.0.0", port: int = 8000, **kwargs):
341
+ """Start the Uvicorn server for async handlers."""
342
+ uvicorn.run(self, host=host, port=port, **kwargs)
343
+
344
+ async def _register_agent(self, env_vars: EnvironmentVariables):
345
+ """Register this agent with the Agentex server"""
346
+ # Build the agent's own URL
347
+ full_acp_url = f"{env_vars.ACP_URL.rstrip('/')}:{env_vars.ACP_PORT}"
348
+
349
+ description = (
350
+ env_vars.AGENT_DESCRIPTION
351
+ or f"Generic description for agent: {env_vars.AGENT_NAME}"
352
+ )
353
+ # Prepare registration data
354
+ registration_data = {
355
+ "name": env_vars.AGENT_NAME,
356
+ "description": description,
357
+ "acp_url": full_acp_url,
358
+ "acp_type": env_vars.ACP_TYPE,
359
+ }
360
+
361
+ if env_vars.AGENT_ID:
362
+ registration_data["agent_id"] = env_vars.AGENT_ID
363
+
364
+ # Make the registration request
365
+ registration_url = f"{env_vars.AGENTEX_BASE_URL.rstrip('/')}/agents/register"
366
+ # Retry logic with configurable attempts and delay
367
+ max_retries = 3
368
+ base_delay = 5 # seconds
369
+ last_exception = None
370
+
371
+ attempt = 0
372
+ while attempt < max_retries:
373
+ try:
374
+ async with httpx.AsyncClient() as client:
375
+ response = await client.post(
376
+ registration_url, json=registration_data, timeout=30.0
377
+ )
378
+ if response.status_code == 200:
379
+ logger.info(
380
+ f"Successfully registered agent '{env_vars.AGENT_NAME}' with Agentex server with acp_url: {full_acp_url}. Registration data: {registration_data}"
381
+ )
382
+ return # Success, exit the retry loop
383
+ else:
384
+ error_msg = f"Failed to register agent. Status: {response.status_code}, Response: {response.text}"
385
+ logger.error(error_msg)
386
+ last_exception = Exception(
387
+ f"Failed to startup agent: {response.text}"
388
+ )
389
+
390
+ except Exception as e:
391
+ logger.error(
392
+ f"Exception during agent registration attempt {attempt + 1}: {e}"
393
+ )
394
+ last_exception = e
395
+ attempt += 1
396
+ if attempt < max_retries:
397
+ delay = (attempt) * base_delay # 5, 10, 15 seconds
398
+ logger.info(
399
+ f"Retrying in {delay} seconds... (attempt {attempt}/{max_retries})"
400
+ )
401
+ await asyncio.sleep(delay)
402
+
403
+ # If we get here, all retries failed
404
+ raise last_exception or Exception(
405
+ f"Failed to register agent after {max_retries} attempts"
406
+ )
@@ -0,0 +1,74 @@
1
+ from typing import Literal
2
+ from agentex.lib.sdk.fastacp.base.base_acp_server import BaseACPServer
3
+ from agentex.lib.sdk.fastacp.impl.agentic_base_acp import AgenticBaseACP
4
+ from agentex.lib.sdk.fastacp.impl.sync_acp import SyncACP
5
+ from agentex.lib.sdk.fastacp.impl.temporal_acp import TemporalACP
6
+ from agentex.lib.types.fastacp import (
7
+ AgenticACPConfig,
8
+ BaseACPConfig,
9
+ SyncACPConfig,
10
+ )
11
+
12
+ # Add new mappings between ACP types and configs here
13
+ # Add new mappings between ACP types and implementations here
14
+ AGENTIC_ACP_IMPLEMENTATIONS: dict[Literal["temporal", "base"], type[BaseACPServer]] = {
15
+ "temporal": TemporalACP,
16
+ "base": AgenticBaseACP,
17
+ }
18
+
19
+
20
+ class FastACP:
21
+ """Factory for creating FastACP instances
22
+
23
+ Supports two main ACP types:
24
+ - "sync": Simple synchronous ACP implementation
25
+ - "agentic": Advanced ACP with sub-types "base" or "temporal" (requires config)
26
+ """
27
+
28
+ @staticmethod
29
+ # Note: the config is optional and not used right now but is there to be extended in the future
30
+ def create_sync_acp(config: SyncACPConfig | None = None, **kwargs) -> SyncACP:
31
+ """Create a SyncACP instance"""
32
+ return SyncACP.create(**kwargs)
33
+
34
+ @staticmethod
35
+ def create_agentic_acp(config: AgenticACPConfig, **kwargs) -> BaseACPServer:
36
+ """Create an agentic ACP instance (base or temporal)
37
+
38
+ Args:
39
+ config: AgenticACPConfig with type="base" or type="temporal"
40
+ **kwargs: Additional configuration parameters
41
+ """
42
+ # Get implementation class
43
+ implementation_class = AGENTIC_ACP_IMPLEMENTATIONS[config.type]
44
+ # Handle temporal-specific configuration
45
+ if config.type == "temporal":
46
+ # Extract temporal_address from config if it's a TemporalACPConfig
47
+ temporal_config = kwargs.copy()
48
+ if hasattr(config, "temporal_address"):
49
+ temporal_config["temporal_address"] = config.temporal_address
50
+ return implementation_class.create(**temporal_config)
51
+ else:
52
+ return implementation_class.create(**kwargs)
53
+
54
+ @staticmethod
55
+ def create(
56
+ acp_type: Literal["sync", "agentic"], config: BaseACPConfig | None = None, **kwargs
57
+ ) -> BaseACPServer | SyncACP | AgenticBaseACP | TemporalACP:
58
+ """Main factory method to create any ACP type
59
+
60
+ Args:
61
+ acp_type: Type of ACP to create ("sync" or "agentic")
62
+ config: Configuration object. Required for agentic type.
63
+ **kwargs: Additional configuration parameters
64
+ """
65
+
66
+ if acp_type == "sync":
67
+ sync_config = config if isinstance(config, SyncACPConfig) else None
68
+ return FastACP.create_sync_acp(sync_config, **kwargs)
69
+ elif acp_type == "agentic":
70
+ if config is None:
71
+ config = AgenticACPConfig(type="base")
72
+ if not isinstance(config, AgenticACPConfig):
73
+ raise ValueError("AgenticACPConfig is required for agentic ACP type")
74
+ return FastACP.create_agentic_acp(config, **kwargs)
@@ -0,0 +1,72 @@
1
+ from typing import Any
2
+ from typing_extensions import override
3
+ from agentex import AsyncAgentex
4
+ from agentex.lib.sdk.fastacp.base.base_acp_server import BaseACPServer
5
+ from agentex.lib.types.acp import (
6
+ CancelTaskParams,
7
+ CreateTaskParams,
8
+ SendEventParams,
9
+ )
10
+ from agentex.lib.utils.logging import make_logger
11
+
12
+ logger = make_logger(__name__)
13
+
14
+
15
+ class AgenticBaseACP(BaseACPServer):
16
+ """
17
+ AgenticBaseACP implementation - a synchronous ACP that provides basic functionality
18
+ without any special async orchestration like Temporal.
19
+
20
+ This implementation provides simple synchronous processing of tasks
21
+ and is suitable for basic agent implementations.
22
+ """
23
+
24
+ def __init__(self):
25
+ super().__init__()
26
+ self._setup_handlers()
27
+ self._agentex_client = AsyncAgentex()
28
+
29
+ @classmethod
30
+ @override
31
+ def create(cls, **kwargs: Any) -> "AgenticBaseACP":
32
+ """Create and initialize SyncACP instance
33
+
34
+ Args:
35
+ **kwargs: Configuration parameters (unused in sync implementation)
36
+
37
+ Returns:
38
+ Initialized SyncACP instance
39
+ """
40
+ logger.info("Initializing AgenticBaseACP instance")
41
+ instance = cls()
42
+ logger.info("AgenticBaseACP instance initialized with default handlers")
43
+ return instance
44
+
45
+ @override
46
+ def _setup_handlers(self):
47
+ """Set up default handlers for sync operations"""
48
+
49
+ @self.on_task_create
50
+ async def handle_create_task(params: CreateTaskParams) -> None: # type: ignore[unused-function]
51
+ """Default create task handler - logs the task"""
52
+ logger.info(f"AgenticBaseACP creating task {params.task.id}")
53
+
54
+ @self.on_task_event_send
55
+ async def handle_event_send(params: SendEventParams) -> None: # type: ignore[unused-function]
56
+ """Default event handler - logs the event"""
57
+ logger.info(
58
+ f"AgenticBaseACP received event for task {params.task.id}: {params.event.id},"
59
+ f"content: {params.event.content}"
60
+ )
61
+ # TODO: Implement event handling logic here
62
+
63
+ # Implement cursor commit logic here
64
+ await self._agentex_client.tracker.update(
65
+ tracker_id=params.task.id,
66
+ last_processed_event_id=params.event.id,
67
+ )
68
+
69
+ @self.on_task_cancel
70
+ async def handle_cancel(params: CancelTaskParams) -> None: # type: ignore[unused-function]
71
+ """Default cancel handler - logs the cancellation"""
72
+ logger.info(f"AgenticBaseACP canceling task {params.task.id}")
@@ -0,0 +1,109 @@
1
+ from collections.abc import AsyncGenerator
2
+ from typing import Any, override
3
+
4
+ from agentex.lib.sdk.fastacp.base.base_acp_server import BaseACPServer
5
+ from agentex.lib.types.acp import SendMessageParams
6
+ from agentex.lib.types.task_message_updates import (
7
+ StreamTaskMessageDelta,
8
+ StreamTaskMessageFull,
9
+ TaskMessageUpdate,
10
+ TextDelta,
11
+ )
12
+ from agentex.types.task_message_content import TaskMessageContent, TextContent
13
+ from agentex.lib.utils.logging import make_logger
14
+
15
+ logger = make_logger(__name__)
16
+
17
+
18
+ class SyncACP(BaseACPServer):
19
+ """
20
+ SyncACP provides synchronous request-response style communication.
21
+ Handlers execute and return responses immediately.
22
+
23
+ The SyncACP automatically creates input and output messages, so handlers
24
+ don't need to manually create TaskMessage objects via the Agentex API. All that needs
25
+ to be done is return the output message via TaskMessageContent objects.
26
+
27
+ Usage:
28
+ acp = SyncACP()
29
+
30
+ @acp.on_message_send
31
+ async def handle_message(params: SendMessageParams) -> TaskMessageContent:
32
+ # Process message and return response
33
+ pass
34
+
35
+ acp.run()
36
+ """
37
+
38
+ def __init__(self):
39
+ super().__init__()
40
+ self._setup_handlers()
41
+
42
+ @classmethod
43
+ @override
44
+ def create(cls, **kwargs: Any) -> "SyncACP":
45
+ """Create and initialize SyncACP instance
46
+
47
+ Args:
48
+ **kwargs: Configuration parameters (unused in sync implementation)
49
+
50
+ Returns:
51
+ Initialized SyncACP instance
52
+ """
53
+ logger.info("Creating SyncACP instance")
54
+ instance = cls()
55
+ logger.info("SyncACP instance created with default handlers")
56
+ return instance
57
+
58
+ @override
59
+ def _setup_handlers(self):
60
+ """Set up default handlers for sync operations"""
61
+
62
+ @self.on_message_send
63
+ async def handle_message_send( # type: ignore[unused-function]
64
+ params: SendMessageParams
65
+ ) -> TaskMessageContent | AsyncGenerator[TaskMessageUpdate, None]:
66
+ """Default message handler with TaskMessageUpdate streaming support
67
+
68
+ For streaming, the SyncACP server automatically creates the input and output
69
+ messages, so we just return TaskMessageUpdate objects with parent_task_message=None
70
+ """
71
+ logger.info(
72
+ f"SyncACP received message for task {params.task.id}: {params.content}"
73
+ )
74
+
75
+ if params.stream:
76
+ # Return streaming response
77
+ async def stream_response():
78
+ # Example: Stream 3 chunks
79
+ full_message = ""
80
+ for i in range(3):
81
+ data = f"Streaming chunk {i+1}: Processing your request...\n"
82
+ full_message += data
83
+ yield StreamTaskMessageDelta(
84
+ type="delta",
85
+ index=0,
86
+ delta=TextDelta(
87
+ text_delta=f"Streaming chunk {i+1}: Processing your request...\n"
88
+ ),
89
+ )
90
+
91
+ # Final response
92
+ yield StreamTaskMessageFull(
93
+ type="full",
94
+ index=0,
95
+ content=TextContent(
96
+ author="agent",
97
+ content=full_message,
98
+ format="markdown",
99
+ ),
100
+ )
101
+
102
+ return stream_response()
103
+ else:
104
+ # Return single response for non-streaming
105
+ return TextContent(
106
+ author="agent",
107
+ content=f"Processed message for task {params.task.id}",
108
+ format="markdown",
109
+ )