quantum-framework 0.9.0__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 (262) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/commands/__init__.py +15 -0
  4. quantum/cli/commands/build.py +417 -0
  5. quantum/cli/commands/dev.py +185 -0
  6. quantum/cli/commands/docs.py +329 -0
  7. quantum/cli/commands/lint.py +523 -0
  8. quantum/cli/commands/migrate.py +527 -0
  9. quantum/cli/commands/new.py +622 -0
  10. quantum/cli/commands/serve.py +190 -0
  11. quantum/cli/commands/test.py +193 -0
  12. quantum/cli/deploy.py +810 -0
  13. quantum/cli/hot_reload.py +951 -0
  14. quantum/cli/jobs.py +356 -0
  15. quantum/cli/mq.py +582 -0
  16. quantum/cli/pkg.py +390 -0
  17. quantum/cli/runner.py +547 -0
  18. quantum/cli/server_process.py +159 -0
  19. quantum/cli/utils.py +334 -0
  20. quantum/compiler/__init__.py +30 -0
  21. quantum/compiler/base_generator.py +367 -0
  22. quantum/compiler/cli.py +295 -0
  23. quantum/compiler/expression_transformer.py +444 -0
  24. quantum/compiler/javascript/__init__.py +10 -0
  25. quantum/compiler/javascript/generator.py +659 -0
  26. quantum/compiler/optimizer.py +270 -0
  27. quantum/compiler/python/__init__.py +10 -0
  28. quantum/compiler/python/generator.py +883 -0
  29. quantum/compiler/python/runtime.py +863 -0
  30. quantum/compiler/transpiler.py +330 -0
  31. quantum/core/__init__.py +3 -0
  32. quantum/core/ast_nodes.py +2611 -0
  33. quantum/core/expression_diagnostics.py +87 -0
  34. quantum/core/expression_stdlib.py +148 -0
  35. quantum/core/expressions.py +591 -0
  36. quantum/core/features/agents/src/__init__.py +30 -0
  37. quantum/core/features/agents/src/ast_node.py +540 -0
  38. quantum/core/features/conditionals/src/__init__.py +8 -0
  39. quantum/core/features/conditionals/src/ast_node.py +68 -0
  40. quantum/core/features/data_fetching/src/__init__.py +21 -0
  41. quantum/core/features/data_fetching/src/ast_node.py +312 -0
  42. quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
  43. quantum/core/features/data_fetching/src/html_adapter.py +474 -0
  44. quantum/core/features/data_fetching/src/parser.py +225 -0
  45. quantum/core/features/data_import/src/__init__.py +0 -0
  46. quantum/core/features/data_import/src/ast_node.py +291 -0
  47. quantum/core/features/data_import/src/runtime.py +538 -0
  48. quantum/core/features/dump/src/__init__.py +12 -0
  49. quantum/core/features/dump/src/ast_node.py +106 -0
  50. quantum/core/features/dump/src/parser.py +61 -0
  51. quantum/core/features/dump/src/runtime.py +246 -0
  52. quantum/core/features/functions/src/__init__.py +8 -0
  53. quantum/core/features/functions/src/ast_node.py +149 -0
  54. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  55. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  56. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  57. quantum/core/features/invocation/src/__init__.py +0 -0
  58. quantum/core/features/invocation/src/ast_node.py +146 -0
  59. quantum/core/features/invocation/src/runtime.py +327 -0
  60. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  61. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  62. quantum/core/features/knowledge_base/src/parser.py +82 -0
  63. quantum/core/features/logging/src/__init__.py +12 -0
  64. quantum/core/features/logging/src/ast_node.py +111 -0
  65. quantum/core/features/logging/src/parser.py +50 -0
  66. quantum/core/features/logging/src/runtime.py +190 -0
  67. quantum/core/features/loops/src/__init__.py +8 -0
  68. quantum/core/features/loops/src/ast_node.py +60 -0
  69. quantum/core/features/query/src/__init__.py +0 -0
  70. quantum/core/features/query/src/database_service.py +322 -0
  71. quantum/core/features/query/src/query_validators.py +20 -0
  72. quantum/core/features/state_management/src/__init__.py +11 -0
  73. quantum/core/features/state_management/src/ast_node.py +228 -0
  74. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  75. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  76. quantum/core/features/terminal_engine/src/parser.py +361 -0
  77. quantum/core/features/testing_engine/src/__init__.py +41 -0
  78. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  79. quantum/core/features/testing_engine/src/parser.py +604 -0
  80. quantum/core/features/theming/src/__init__.py +48 -0
  81. quantum/core/features/theming/src/ast_node.py +137 -0
  82. quantum/core/features/theming/src/presets.py +405 -0
  83. quantum/core/features/ui_engine/src/__init__.py +20 -0
  84. quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
  85. quantum/core/features/ui_engine/src/parser.py +1106 -0
  86. quantum/core/features/websocket/src/__init__.py +24 -0
  87. quantum/core/features/websocket/src/ast_node.py +247 -0
  88. quantum/core/html_compat.py +299 -0
  89. quantum/core/parser.py +1235 -0
  90. quantum/core/parser_registry.py +213 -0
  91. quantum/core/parsers/__init__.py +76 -0
  92. quantum/core/parsers/ai/__init__.py +12 -0
  93. quantum/core/parsers/ai/agent_parser.py +106 -0
  94. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  95. quantum/core/parsers/ai/llm_parser.py +78 -0
  96. quantum/core/parsers/ai/team_parser.py +88 -0
  97. quantum/core/parsers/base.py +322 -0
  98. quantum/core/parsers/composition/__init__.py +10 -0
  99. quantum/core/parsers/composition/import_parser.py +50 -0
  100. quantum/core/parsers/composition/slot_parser.py +48 -0
  101. quantum/core/parsers/control_flow/__init__.py +11 -0
  102. quantum/core/parsers/control_flow/if_parser.py +68 -0
  103. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  104. quantum/core/parsers/control_flow/set_parser.py +89 -0
  105. quantum/core/parsers/data/__init__.py +12 -0
  106. quantum/core/parsers/data/data_parser.py +217 -0
  107. quantum/core/parsers/data/invoke_parser.py +116 -0
  108. quantum/core/parsers/data/query_parser.py +188 -0
  109. quantum/core/parsers/data/transaction_parser.py +77 -0
  110. quantum/core/parsers/events/__init__.py +9 -0
  111. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  112. quantum/core/parsers/forms/__init__.py +11 -0
  113. quantum/core/parsers/forms/action_parser.py +54 -0
  114. quantum/core/parsers/forms/flash_parser.py +34 -0
  115. quantum/core/parsers/forms/redirect_parser.py +33 -0
  116. quantum/core/parsers/functions/__init__.py +11 -0
  117. quantum/core/parsers/functions/function_parser.py +137 -0
  118. quantum/core/parsers/functions/param_parser.py +66 -0
  119. quantum/core/parsers/functions/return_parser.py +31 -0
  120. quantum/core/parsers/html/__init__.py +10 -0
  121. quantum/core/parsers/html/component_call_parser.py +130 -0
  122. quantum/core/parsers/html/html_parser.py +115 -0
  123. quantum/core/parsers/jobs/__init__.py +11 -0
  124. quantum/core/parsers/jobs/job_parser.py +71 -0
  125. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  126. quantum/core/parsers/jobs/thread_parser.py +57 -0
  127. quantum/core/parsers/messaging/__init__.py +17 -0
  128. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  130. quantum/core/parsers/messaging/message_parser.py +114 -0
  131. quantum/core/parsers/messaging/queue_parser.py +61 -0
  132. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  133. quantum/core/parsers/persistence/__init__.py +9 -0
  134. quantum/core/parsers/persistence/persist_parser.py +64 -0
  135. quantum/core/parsers/routing/__init__.py +9 -0
  136. quantum/core/parsers/routing/route_parser.py +41 -0
  137. quantum/core/parsers/scripting/__init__.py +12 -0
  138. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  139. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  140. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  141. quantum/core/parsers/scripting/python_parser.py +49 -0
  142. quantum/core/parsers/services/__init__.py +12 -0
  143. quantum/core/parsers/services/dump_parser.py +52 -0
  144. quantum/core/parsers/services/file_parser.py +48 -0
  145. quantum/core/parsers/services/log_parser.py +46 -0
  146. quantum/core/parsers/services/mail_parser.py +65 -0
  147. quantum/core/tiers.py +82 -0
  148. quantum/packages/__init__.py +28 -0
  149. quantum/packages/manager.py +413 -0
  150. quantum/packages/manifest.py +351 -0
  151. quantum/packages/registry.py +399 -0
  152. quantum/packages/resolver.py +336 -0
  153. quantum/plugins/__init__.py +33 -0
  154. quantum/plugins/hooks.py +329 -0
  155. quantum/plugins/loader.py +479 -0
  156. quantum/plugins/manifest.py +336 -0
  157. quantum/plugins/registry.py +371 -0
  158. quantum/runtime/__init__.py +28 -0
  159. quantum/runtime/action_handler.py +443 -0
  160. quantum/runtime/adapters/__init__.py +88 -0
  161. quantum/runtime/adapters/memory_adapter.py +690 -0
  162. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  163. quantum/runtime/adapters/redis_adapter.py +582 -0
  164. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  165. quantum/runtime/agent_service.py +1133 -0
  166. quantum/runtime/api_server.py +86 -0
  167. quantum/runtime/ast_cache.py +506 -0
  168. quantum/runtime/auth_service.py +267 -0
  169. quantum/runtime/component.py +990 -0
  170. quantum/runtime/component_composer.py +319 -0
  171. quantum/runtime/component_resolver.py +174 -0
  172. quantum/runtime/database_service.py +598 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +286 -0
  176. quantum/runtime/executor_registry.py +171 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +262 -0
  184. quantum/runtime/executors/control_flow/__init__.py +11 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +93 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +307 -0
  187. quantum/runtime/executors/control_flow/set_executor.py +412 -0
  188. quantum/runtime/executors/data/__init__.py +12 -0
  189. quantum/runtime/executors/data/data_executor.py +145 -0
  190. quantum/runtime/executors/data/invoke_executor.py +176 -0
  191. quantum/runtime/executors/data/query_executor.py +256 -0
  192. quantum/runtime/executors/data/transaction_executor.py +91 -0
  193. quantum/runtime/executors/jobs/__init__.py +11 -0
  194. quantum/runtime/executors/jobs/job_executor.py +190 -0
  195. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  196. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  197. quantum/runtime/executors/messaging/__init__.py +17 -0
  198. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  199. quantum/runtime/executors/messaging/message_executor.py +174 -0
  200. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  201. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  202. quantum/runtime/executors/scripting/__init__.py +11 -0
  203. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  204. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  205. quantum/runtime/executors/scripting/python_executor.py +249 -0
  206. quantum/runtime/executors/services/__init__.py +12 -0
  207. quantum/runtime/executors/services/dump_executor.py +72 -0
  208. quantum/runtime/executors/services/file_executor.py +89 -0
  209. quantum/runtime/executors/services/log_executor.py +77 -0
  210. quantum/runtime/executors/services/mail_executor.py +81 -0
  211. quantum/runtime/expression_cache.py +498 -0
  212. quantum/runtime/file_upload_service.py +326 -0
  213. quantum/runtime/function_registry.py +118 -0
  214. quantum/runtime/game_builder.py +166 -0
  215. quantum/runtime/game_code_generator.py +2371 -0
  216. quantum/runtime/game_templates.py +2006 -0
  217. quantum/runtime/godot_code_generator.py +4681 -0
  218. quantum/runtime/godot_templates.py +1449 -0
  219. quantum/runtime/job_executor.py +1599 -0
  220. quantum/runtime/knowledge_service.py +500 -0
  221. quantum/runtime/llm_cache.py +100 -0
  222. quantum/runtime/llm_providers.py +704 -0
  223. quantum/runtime/llm_service.py +287 -0
  224. quantum/runtime/logging_setup.py +140 -0
  225. quantum/runtime/message_broker.py +364 -0
  226. quantum/runtime/message_queue_service.py +571 -0
  227. quantum/runtime/param_validation.py +184 -0
  228. quantum/runtime/pypy_compat.py +315 -0
  229. quantum/runtime/python_bridge.py +698 -0
  230. quantum/runtime/query_validators.py +304 -0
  231. quantum/runtime/renderer.py +733 -0
  232. quantum/runtime/service_container.py +444 -0
  233. quantum/runtime/terminal_builder.py +76 -0
  234. quantum/runtime/terminal_code_generator.py +607 -0
  235. quantum/runtime/terminal_templates.py +243 -0
  236. quantum/runtime/testing_builder.py +77 -0
  237. quantum/runtime/testing_code_generator.py +833 -0
  238. quantum/runtime/testing_templates.py +85 -0
  239. quantum/runtime/ui_builder.py +188 -0
  240. quantum/runtime/ui_desktop_adapter.py +1730 -0
  241. quantum/runtime/ui_desktop_templates.py +307 -0
  242. quantum/runtime/ui_html_adapter.py +2691 -0
  243. quantum/runtime/ui_html_templates.py +2297 -0
  244. quantum/runtime/ui_mobile_adapter.py +1832 -0
  245. quantum/runtime/ui_mobile_templates.py +1003 -0
  246. quantum/runtime/ui_textual_adapter.py +1866 -0
  247. quantum/runtime/ui_textual_templates.py +45 -0
  248. quantum/runtime/ui_tokens.py +465 -0
  249. quantum/runtime/ui_validator.py +365 -0
  250. quantum/runtime/validators.py +256 -0
  251. quantum/runtime/web_server.py +1766 -0
  252. quantum/runtime/websocket_adapter.py +501 -0
  253. quantum/runtime/websocket_service.py +585 -0
  254. quantum/runtime/websocket_transport.py +289 -0
  255. quantum/runtime/wsgi.py +101 -0
  256. quantum/utils/__init__.py +1 -0
  257. quantum_framework-0.9.0.dist-info/METADATA +244 -0
  258. quantum_framework-0.9.0.dist-info/RECORD +262 -0
  259. quantum_framework-0.9.0.dist-info/WHEEL +5 -0
  260. quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
  261. quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
  262. quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1133 @@
1
+ """
2
+ Agent Service - Reasoning Loop and Tool Execution for AI Agents
3
+
4
+ Implements the ReAct (Reason + Act) pattern:
5
+ 1. THINK: LLM analyzes task and decides next action
6
+ 2. ACT: Execute the chosen tool
7
+ 3. OBSERVE: Process tool result
8
+ 4. REPEAT: Until task complete or max_iterations
9
+
10
+ Uses existing LLMService for LLM calls.
11
+
12
+ Multi-Agent Support:
13
+ - AgentRegistry: Central registry for agent discovery
14
+ - AgentTeam: Team of collaborating agents with shared context
15
+ - AgentHandoff: Record of agent-to-agent transfers
16
+ - MultiAgentService: Orchestrates team execution
17
+
18
+ Example:
19
+ from quantum.runtime.agent_service import get_agent_service
20
+
21
+ service = get_agent_service()
22
+ result = service.execute(
23
+ instruction="You are a helpful assistant",
24
+ tools=[{"name": "search", "description": "Search docs", ...}],
25
+ task="Find information about orders",
26
+ model="phi3"
27
+ )
28
+ print(result.result) # Agent's response
29
+ print(result.actions) # Tool calls made
30
+
31
+ Multi-Agent Example:
32
+ from quantum.runtime.agent_service import get_multi_agent_service
33
+
34
+ service = get_multi_agent_service()
35
+ team = service.create_team(
36
+ name="support",
37
+ agents={"router": ..., "billing": ...},
38
+ shared={"customerId": "123"},
39
+ supervisor="router"
40
+ )
41
+ result = team.execute("Help with my bill", entry_agent="router")
42
+ """
43
+
44
+ import json
45
+ import re
46
+ import time
47
+ import logging
48
+ from datetime import datetime
49
+ from typing import Dict, List, Any, Optional, Callable, Set
50
+ from dataclasses import dataclass, field
51
+ from collections import defaultdict
52
+
53
+ # Import multi-provider LLM support
54
+ from quantum.runtime.llm_providers import (
55
+ get_llm_provider, MultiProviderLLMService, LLMProviderError,
56
+ BaseLLMProvider
57
+ )
58
+
59
+ logger = logging.getLogger(__name__)
60
+
61
+
62
+ class AgentError(Exception):
63
+ """Error during agent execution."""
64
+ pass
65
+
66
+
67
+ @dataclass
68
+ class ToolCall:
69
+ """Represents a single tool invocation."""
70
+ tool: str
71
+ args: Dict[str, Any]
72
+ result: Any = None
73
+ error: Optional[str] = None
74
+ duration_ms: float = 0
75
+
76
+ def to_dict(self) -> Dict[str, Any]:
77
+ """Convert to dictionary."""
78
+ return {
79
+ "tool": self.tool,
80
+ "args": self.args,
81
+ "result": self.result,
82
+ "error": self.error,
83
+ "duration_ms": self.duration_ms
84
+ }
85
+
86
+
87
+ @dataclass
88
+ class AgentResult:
89
+ """Result of agent execution."""
90
+ success: bool = False
91
+ result: str = ""
92
+ error: Optional[Dict[str, str]] = None
93
+ execution_time_ms: float = 0
94
+ iterations: int = 0
95
+ action_count: int = 0
96
+ actions: List[ToolCall] = field(default_factory=list)
97
+ token_usage: Dict[str, int] = field(default_factory=lambda: {"prompt": 0, "completion": 0, "total": 0})
98
+
99
+ def to_dict(self) -> Dict[str, Any]:
100
+ """Convert to dictionary for template access."""
101
+ return {
102
+ "success": self.success,
103
+ "result": self.result,
104
+ "error": self.error,
105
+ "executionTime": self.execution_time_ms,
106
+ "iterations": self.iterations,
107
+ "actionCount": self.action_count,
108
+ "actions": [a.to_dict() for a in self.actions],
109
+ "tokenUsage": self.token_usage
110
+ }
111
+
112
+
113
+ class AgentService:
114
+ """
115
+ Executes AI agents with tool use capabilities.
116
+
117
+ Uses LLM to reason about which tools to use and when,
118
+ then executes those tools and observes results.
119
+ """
120
+
121
+ # System prompt template for tool-using agents
122
+ SYSTEM_PROMPT_TEMPLATE = '''You are an AI agent that completes tasks by using tools.
123
+
124
+ AVAILABLE TOOLS:
125
+ {tools_description}
126
+
127
+ INSTRUCTIONS:
128
+ {instruction}
129
+
130
+ RESPONSE FORMAT:
131
+ When you need to use a tool, respond with EXACTLY this JSON format (no other text):
132
+ ```json
133
+ {{"action": "tool_name", "args": {{"param1": "value1", "param2": "value2"}}}}
134
+ ```
135
+
136
+ When you have completed the task and have all the information needed, respond with EXACTLY:
137
+ ```json
138
+ {{"action": "finish", "result": "Your complete and helpful response to the user"}}
139
+ ```
140
+
141
+ RULES:
142
+ 1. ONLY use the tools listed above - no other tools exist
143
+ 2. Use ONE tool at a time, then wait for the result
144
+ 3. After seeing a tool result, decide if you need more information or can finish
145
+ 4. When you have enough information, use "finish" to provide your final answer
146
+ 5. Be concise but complete in your final response
147
+ 6. If a tool returns an error, try a different approach or explain the issue
148
+
149
+ IMPORTANT: Always respond with valid JSON in the format shown above. Nothing else.'''
150
+
151
+ def __init__(self, llm_service=None):
152
+ """
153
+ Initialize agent service.
154
+
155
+ Args:
156
+ llm_service: LLMService instance for LLM calls (auto-created if None)
157
+ """
158
+ self._llm_service = llm_service
159
+ self._multi_llm_service: Optional[MultiProviderLLMService] = None
160
+ self._tool_handlers: Dict[str, Callable] = {}
161
+
162
+ @property
163
+ def llm_service(self):
164
+ """Lazy-load LLM service (backward compatibility)."""
165
+ if self._llm_service is None:
166
+ from quantum.runtime.llm_service import get_llm_service
167
+ self._llm_service = get_llm_service()
168
+ return self._llm_service
169
+
170
+ @property
171
+ def multi_llm_service(self) -> MultiProviderLLMService:
172
+ """Lazy-load multi-provider LLM service."""
173
+ if self._multi_llm_service is None:
174
+ self._multi_llm_service = MultiProviderLLMService()
175
+ return self._multi_llm_service
176
+
177
+ def register_tool_handler(self, name: str, handler: Callable):
178
+ """
179
+ Register a custom handler for a tool.
180
+
181
+ Args:
182
+ name: Tool name
183
+ handler: Function(args: dict) -> Any
184
+ """
185
+ self._tool_handlers[name] = handler
186
+ logger.debug(f"Registered tool handler: {name}")
187
+
188
+ def execute(
189
+ self,
190
+ instruction: str,
191
+ tools: List[Dict[str, Any]],
192
+ task: str,
193
+ context: str = "",
194
+ model: str = "phi3",
195
+ endpoint: str = "",
196
+ provider: str = "auto",
197
+ api_key: str = "",
198
+ max_iterations: int = 10,
199
+ timeout_ms: int = 60000,
200
+ tool_executor: Optional[Callable] = None
201
+ ) -> AgentResult:
202
+ """
203
+ Execute an agent with the given task.
204
+
205
+ Args:
206
+ instruction: System instruction for the agent
207
+ tools: List of tool definitions with name, description, params, body
208
+ task: The task to complete
209
+ context: Additional context
210
+ model: LLM model to use
211
+ endpoint: LLM endpoint (optional, uses default if empty)
212
+ provider: LLM provider (ollama, openai, anthropic, auto)
213
+ api_key: API key for cloud providers
214
+ max_iterations: Maximum tool calls
215
+ timeout_ms: Total timeout in milliseconds
216
+ tool_executor: Optional function to execute tool bodies
217
+
218
+ Returns:
219
+ AgentResult with success, result, actions, etc.
220
+ """
221
+ start_time = time.time()
222
+ result = AgentResult()
223
+
224
+ try:
225
+ # Validate inputs
226
+ if not task:
227
+ raise AgentError("Task is required")
228
+ if not tools:
229
+ raise AgentError("At least one tool is required")
230
+
231
+ # Build tools description for system prompt
232
+ tools_desc = self._build_tools_description(tools)
233
+
234
+ # Build system prompt
235
+ system_prompt = self.SYSTEM_PROMPT_TEMPLATE.format(
236
+ tools_description=tools_desc,
237
+ instruction=instruction or "Complete the user's task accurately and helpfully."
238
+ )
239
+
240
+ # Build initial user message
241
+ user_message = task
242
+ if context:
243
+ user_message = f"{task}\n\nContext: {context}"
244
+
245
+ # Conversation history for multi-turn
246
+ messages = [
247
+ {"role": "system", "content": system_prompt},
248
+ {"role": "user", "content": user_message}
249
+ ]
250
+
251
+ # Tool lookup by name
252
+ tools_by_name = {t["name"]: t for t in tools}
253
+
254
+ # Reasoning loop
255
+ iteration = 0
256
+
257
+ while iteration < max_iterations:
258
+ # Check timeout
259
+ elapsed_ms = (time.time() - start_time) * 1000
260
+ if elapsed_ms > timeout_ms:
261
+ raise AgentError(f"Agent timed out after {elapsed_ms:.0f}ms")
262
+
263
+ iteration += 1
264
+ result.iterations = iteration
265
+
266
+ logger.debug(f"Agent iteration {iteration}/{max_iterations}")
267
+
268
+ # Call LLM
269
+ llm_response = self._call_llm(
270
+ messages=messages,
271
+ model=model,
272
+ endpoint=endpoint,
273
+ provider=provider,
274
+ api_key=api_key
275
+ )
276
+
277
+ # Get assistant message
278
+ assistant_message = llm_response.get("content", "").strip()
279
+
280
+ if not assistant_message:
281
+ logger.warning("LLM returned empty response")
282
+ continue
283
+
284
+ # Add to conversation history
285
+ messages.append({"role": "assistant", "content": assistant_message})
286
+
287
+ # Parse action from response
288
+ action = self._extract_action(assistant_message)
289
+
290
+ if action is None:
291
+ # LLM didn't follow format - try to recover
292
+ logger.warning(f"Could not parse action from: {assistant_message[:100]}...")
293
+
294
+ # If it looks like a final answer, treat it as finish
295
+ if iteration > 1 and not any(t["name"] in assistant_message for t in tools):
296
+ result.success = True
297
+ result.result = assistant_message
298
+ break
299
+
300
+ # Ask LLM to try again
301
+ messages.append({
302
+ "role": "user",
303
+ "content": "Please respond with valid JSON in the format: {\"action\": \"tool_name\", \"args\": {...}} or {\"action\": \"finish\", \"result\": \"...\"}"
304
+ })
305
+ continue
306
+
307
+ action_type = action.get("action", "")
308
+
309
+ # Check for finish
310
+ if action_type == "finish":
311
+ result.success = True
312
+ result.result = action.get("result", "Task completed.")
313
+ break
314
+
315
+ # Execute tool
316
+ tool_name = action_type
317
+ tool_args = action.get("args", {})
318
+
319
+ if tool_name not in tools_by_name:
320
+ # Unknown tool - inform LLM
321
+ messages.append({
322
+ "role": "user",
323
+ "content": f"Error: Unknown tool '{tool_name}'. Available tools: {list(tools_by_name.keys())}"
324
+ })
325
+ continue
326
+
327
+ # Execute the tool
328
+ tool_def = tools_by_name[tool_name]
329
+ tool_call = self._execute_tool(
330
+ tool_name=tool_name,
331
+ tool_args=tool_args,
332
+ tool_def=tool_def,
333
+ tool_executor=tool_executor
334
+ )
335
+
336
+ result.actions.append(tool_call)
337
+ result.action_count += 1
338
+
339
+ # Format tool result for LLM
340
+ if tool_call.error:
341
+ tool_result_msg = f"Tool '{tool_name}' failed with error: {tool_call.error}"
342
+ else:
343
+ # Format result as string
344
+ if isinstance(tool_call.result, (dict, list)):
345
+ tool_result_str = json.dumps(tool_call.result, indent=2, default=str)
346
+ else:
347
+ tool_result_str = str(tool_call.result)
348
+ tool_result_msg = f"Tool '{tool_name}' returned:\n{tool_result_str}"
349
+
350
+ messages.append({"role": "user", "content": tool_result_msg})
351
+
352
+ # Check if we hit max iterations without finishing
353
+ if not result.success and iteration >= max_iterations:
354
+ result.error = {"message": f"Agent reached maximum iterations ({max_iterations}) without completing the task"}
355
+
356
+ # Try to salvage a response from the last assistant message
357
+ if messages and messages[-1].get("role") == "user":
358
+ # Last message was a tool result, look for previous assistant response
359
+ for msg in reversed(messages):
360
+ if msg.get("role") == "assistant":
361
+ result.result = f"[Incomplete] {msg.get('content', '')[:500]}"
362
+ break
363
+
364
+ except AgentError as e:
365
+ logger.error(f"Agent error: {e}")
366
+ result.success = False
367
+ result.error = {"message": str(e)}
368
+
369
+ except Exception as e:
370
+ logger.exception(f"Unexpected error in agent execution: {e}")
371
+ result.success = False
372
+ result.error = {"message": f"Internal error: {str(e)}"}
373
+
374
+ result.execution_time_ms = (time.time() - start_time) * 1000
375
+ logger.info(f"Agent completed in {result.execution_time_ms:.0f}ms, "
376
+ f"iterations={result.iterations}, actions={result.action_count}, "
377
+ f"success={result.success}")
378
+
379
+ return result
380
+
381
+ def _build_tools_description(self, tools: List[Dict[str, Any]]) -> str:
382
+ """Build human-readable tools description for system prompt."""
383
+ lines = []
384
+ for tool in tools:
385
+ name = tool.get("name", "unknown")
386
+ desc = tool.get("description", "No description")
387
+ params = tool.get("params", [])
388
+
389
+ # Build parameter string
390
+ param_parts = []
391
+ for p in params:
392
+ p_name = p.get("name", "")
393
+ p_type = p.get("type", "string")
394
+ p_req = " (required)" if p.get("required") else ""
395
+ p_desc = f" - {p.get('description')}" if p.get("description") else ""
396
+ param_parts.append(f" - {p_name}: {p_type}{p_req}{p_desc}")
397
+
398
+ params_str = "\n".join(param_parts) if param_parts else " (no parameters)"
399
+
400
+ lines.append(f"• {name}: {desc}")
401
+ lines.append(f" Parameters:\n{params_str}")
402
+ lines.append("")
403
+
404
+ return "\n".join(lines)
405
+
406
+ def _call_llm(
407
+ self,
408
+ messages: List[Dict[str, str]],
409
+ model: str,
410
+ endpoint: str,
411
+ provider: str = "auto",
412
+ api_key: str = ""
413
+ ) -> Dict[str, Any]:
414
+ """
415
+ Call LLM with messages using multi-provider support.
416
+
417
+ Args:
418
+ messages: Chat messages
419
+ model: Model name
420
+ endpoint: API endpoint (optional)
421
+ provider: Provider name (ollama, openai, anthropic, auto)
422
+ api_key: API key for cloud providers
423
+
424
+ Returns:
425
+ Dict with 'content' key containing the response
426
+ """
427
+ try:
428
+ # Use multi-provider service
429
+ response = self.multi_llm_service.chat(
430
+ messages=messages,
431
+ model=model,
432
+ provider=provider if provider != "auto" else None,
433
+ endpoint=endpoint if endpoint else None,
434
+ api_key=api_key if api_key else None,
435
+ temperature=0.1, # Low temperature for deterministic tool use
436
+ )
437
+
438
+ # Handle response format from MultiProviderLLMService
439
+ if isinstance(response, dict):
440
+ if "content" in response:
441
+ return response
442
+ elif "data" in response:
443
+ return {"content": response["data"]}
444
+
445
+ return {"content": str(response)}
446
+
447
+ except LLMProviderError as e:
448
+ logger.error(f"LLM provider error: {e}")
449
+ raise AgentError(f"LLM provider error: {e}")
450
+
451
+ except Exception as e:
452
+ logger.error(f"LLM call failed: {e}")
453
+ raise AgentError(f"Failed to call LLM: {e}")
454
+
455
+ def _extract_action(self, response: str) -> Optional[Dict[str, Any]]:
456
+ """Extract action JSON from LLM response."""
457
+ # Try to find JSON in markdown code blocks first
458
+ json_patterns = [
459
+ r'```json\s*(\{.*?\})\s*```', # ```json {...} ```
460
+ r'```\s*(\{.*?\})\s*```', # ``` {...} ```
461
+ r'(\{[^{}]*"action"[^{}]*\})', # Bare JSON with "action"
462
+ ]
463
+
464
+ for pattern in json_patterns:
465
+ match = re.search(pattern, response, re.DOTALL)
466
+ if match:
467
+ try:
468
+ parsed = json.loads(match.group(1))
469
+ if "action" in parsed:
470
+ return parsed
471
+ except json.JSONDecodeError:
472
+ continue
473
+
474
+ # Try parsing the entire response as JSON
475
+ try:
476
+ parsed = json.loads(response.strip())
477
+ if "action" in parsed:
478
+ return parsed
479
+ except json.JSONDecodeError:
480
+ pass
481
+
482
+ return None
483
+
484
+ def _execute_tool(
485
+ self,
486
+ tool_name: str,
487
+ tool_args: Dict[str, Any],
488
+ tool_def: Dict[str, Any],
489
+ tool_executor: Optional[Callable] = None
490
+ ) -> ToolCall:
491
+ """Execute a tool and return the result."""
492
+ start = time.time()
493
+ call = ToolCall(tool=tool_name, args=tool_args)
494
+
495
+ try:
496
+ # Check for registered handler first
497
+ if tool_name in self._tool_handlers:
498
+ handler = self._tool_handlers[tool_name]
499
+ call.result = handler(tool_args)
500
+
501
+ # Use provided executor for AST body execution
502
+ elif tool_executor and "body" in tool_def:
503
+ call.result = tool_executor(tool_name, tool_args, tool_def["body"])
504
+
505
+ # Check for inline function/handler in tool definition
506
+ elif "handler" in tool_def:
507
+ handler = tool_def["handler"]
508
+ if callable(handler):
509
+ call.result = handler(**tool_args)
510
+ else:
511
+ call.error = "Tool handler is not callable"
512
+
513
+ else:
514
+ # No handler - return placeholder
515
+ call.result = f"Tool '{tool_name}' executed with args: {tool_args}"
516
+ logger.warning(f"No handler for tool '{tool_name}', returning placeholder")
517
+
518
+ except Exception as e:
519
+ call.error = str(e)
520
+ logger.error(f"Tool '{tool_name}' execution failed: {e}")
521
+
522
+ call.duration_ms = (time.time() - start) * 1000
523
+ return call
524
+
525
+
526
+ # Global singleton
527
+ _agent_service: Optional[AgentService] = None
528
+
529
+
530
+ def get_agent_service(llm_service=None) -> AgentService:
531
+ """
532
+ Get or create the global AgentService instance.
533
+
534
+ Args:
535
+ llm_service: Optional LLMService to use (uses default if None)
536
+
537
+ Returns:
538
+ AgentService instance
539
+ """
540
+ global _agent_service
541
+ if _agent_service is None:
542
+ _agent_service = AgentService(llm_service)
543
+ return _agent_service
544
+
545
+
546
+ def reset_agent_service():
547
+ """Reset the global agent service (for testing)."""
548
+ global _agent_service
549
+ _agent_service = None
550
+
551
+
552
+ # ============================================
553
+ # MULTI-AGENT SYSTEM
554
+ # ============================================
555
+
556
+ @dataclass
557
+ class AgentHandoff:
558
+ """Record of a handoff between agents."""
559
+ from_agent: str
560
+ to_agent: str
561
+ message: str = ""
562
+ timestamp: datetime = field(default_factory=datetime.now)
563
+
564
+ def to_dict(self) -> Dict[str, Any]:
565
+ """Convert to dictionary."""
566
+ return {
567
+ "fromAgent": self.from_agent,
568
+ "toAgent": self.to_agent,
569
+ "message": self.message,
570
+ "timestamp": self.timestamp.isoformat()
571
+ }
572
+
573
+
574
+ @dataclass
575
+ class TeamResult:
576
+ """Result of multi-agent team execution."""
577
+ success: bool = False
578
+ final_response: str = ""
579
+ final_agent: str = "" # Which agent produced the final answer
580
+ handoffs: List[AgentHandoff] = field(default_factory=list)
581
+ total_iterations: int = 0
582
+ execution_time_ms: float = 0
583
+ agent_results: Dict[str, AgentResult] = field(default_factory=dict)
584
+ shared_context: Dict[str, Any] = field(default_factory=dict)
585
+ error: Optional[Dict[str, str]] = None
586
+
587
+ def to_dict(self) -> Dict[str, Any]:
588
+ """Convert to dictionary for template access."""
589
+ return {
590
+ "success": self.success,
591
+ "finalResponse": self.final_response,
592
+ "finalAgent": self.final_agent,
593
+ "handoffs": [h.to_dict() for h in self.handoffs],
594
+ "totalIterations": self.total_iterations,
595
+ "executionTime": self.execution_time_ms,
596
+ "agentResults": {k: v.to_dict() for k, v in self.agent_results.items()},
597
+ "sharedContext": self.shared_context,
598
+ "error": self.error
599
+ }
600
+
601
+
602
+ # Built-in tool definitions for multi-agent systems
603
+ BUILTIN_TOOLS = {
604
+ "handoff": {
605
+ "name": "handoff",
606
+ "description": "Transfer the current task to another agent in the team. Use this when the task requires expertise from a different specialist.",
607
+ "params": [
608
+ {"name": "agent", "type": "string", "required": True,
609
+ "description": "Name of the agent to hand off to"},
610
+ {"name": "message", "type": "string", "required": False,
611
+ "description": "Context or instructions for the next agent"}
612
+ ]
613
+ },
614
+ "readShared": {
615
+ "name": "readShared",
616
+ "description": "Read a value from the team's shared memory. Use this to access information stored by other agents.",
617
+ "params": [
618
+ {"name": "key", "type": "string", "required": True,
619
+ "description": "The key to read from shared memory"}
620
+ ]
621
+ },
622
+ "writeShared": {
623
+ "name": "writeShared",
624
+ "description": "Write a value to the team's shared memory. Use this to store information for other agents.",
625
+ "params": [
626
+ {"name": "key", "type": "string", "required": True,
627
+ "description": "The key to write to"},
628
+ {"name": "value", "type": "string", "required": True,
629
+ "description": "The value to store"}
630
+ ]
631
+ },
632
+ "listAgents": {
633
+ "name": "listAgents",
634
+ "description": "List all available agents in the team and their roles.",
635
+ "params": []
636
+ }
637
+ }
638
+
639
+
640
+ class AgentRegistry:
641
+ """
642
+ Central registry for agent discovery.
643
+
644
+ Allows agents to be registered and discovered by name,
645
+ enabling dynamic handoffs between agents.
646
+ """
647
+
648
+ def __init__(self):
649
+ self._agents: Dict[str, Dict[str, Any]] = {} # name -> config
650
+ self._teams: Dict[str, 'AgentTeam'] = {}
651
+
652
+ def register(self, name: str, config: Dict[str, Any]):
653
+ """Register an agent configuration."""
654
+ self._agents[name] = config
655
+ logger.debug(f"Registered agent: {name}")
656
+
657
+ def get(self, name: str) -> Optional[Dict[str, Any]]:
658
+ """Get an agent configuration by name."""
659
+ return self._agents.get(name)
660
+
661
+ def list_agents(self) -> List[str]:
662
+ """List all registered agent names."""
663
+ return list(self._agents.keys())
664
+
665
+ def unregister(self, name: str):
666
+ """Unregister an agent."""
667
+ if name in self._agents:
668
+ del self._agents[name]
669
+ logger.debug(f"Unregistered agent: {name}")
670
+
671
+ def register_team(self, name: str, team: 'AgentTeam'):
672
+ """Register a team."""
673
+ self._teams[name] = team
674
+ logger.debug(f"Registered team: {name}")
675
+
676
+ def get_team(self, name: str) -> Optional['AgentTeam']:
677
+ """Get a team by name."""
678
+ return self._teams.get(name)
679
+
680
+ def clear(self):
681
+ """Clear all registrations."""
682
+ self._agents.clear()
683
+ self._teams.clear()
684
+
685
+
686
+ class AgentTeam:
687
+ """
688
+ Team of collaborating agents with shared context.
689
+
690
+ Manages:
691
+ - Agent configurations and discovery
692
+ - Shared memory between agents
693
+ - Handoff execution and logging
694
+ - Safety limits (max handoffs, cycle detection)
695
+ """
696
+
697
+ def __init__(
698
+ self,
699
+ name: str,
700
+ agents: Dict[str, Dict[str, Any]],
701
+ shared_context: Optional[Dict[str, Any]] = None,
702
+ supervisor: str = "",
703
+ max_handoffs: int = 10,
704
+ max_total_iterations: int = 50,
705
+ agent_service: Optional['AgentService'] = None
706
+ ):
707
+ """
708
+ Initialize agent team.
709
+
710
+ Args:
711
+ name: Team name
712
+ agents: Dict of agent name -> agent config
713
+ shared_context: Initial shared state
714
+ supervisor: Default entry agent
715
+ max_handoffs: Maximum handoffs allowed
716
+ max_total_iterations: Max iterations across all agents
717
+ agent_service: AgentService for executing individual agents
718
+ """
719
+ self.name = name
720
+ self.agents = agents
721
+ self.shared_context = shared_context or {}
722
+ self.supervisor = supervisor or (list(agents.keys())[0] if agents else "")
723
+ self.max_handoffs = max_handoffs
724
+ self.max_total_iterations = max_total_iterations
725
+ self.handoff_log: List[AgentHandoff] = []
726
+ self._agent_service = agent_service
727
+
728
+ # Cycle detection: track handoff patterns
729
+ self._handoff_counts: Dict[str, int] = defaultdict(int)
730
+
731
+ @property
732
+ def agent_service(self) -> 'AgentService':
733
+ """Lazy-load agent service."""
734
+ if self._agent_service is None:
735
+ self._agent_service = get_agent_service()
736
+ return self._agent_service
737
+
738
+ def get_shared(self, key: str) -> Any:
739
+ """Read from shared context."""
740
+ return self.shared_context.get(key)
741
+
742
+ def set_shared(self, key: str, value: Any):
743
+ """Write to shared context."""
744
+ self.shared_context[key] = value
745
+ logger.debug(f"Team '{self.name}' shared['{key}'] = {value}")
746
+
747
+ def list_agent_names(self) -> List[str]:
748
+ """List all agent names in the team."""
749
+ return list(self.agents.keys())
750
+
751
+ def get_agent_info(self) -> List[Dict[str, str]]:
752
+ """Get info about all agents for listAgents tool."""
753
+ result = []
754
+ for name, config in self.agents.items():
755
+ info = {"name": name}
756
+ if "instruction" in config:
757
+ # Extract first line as role description
758
+ instruction = config["instruction"]
759
+ if isinstance(instruction, str):
760
+ info["role"] = instruction.split('\n')[0][:100]
761
+ result.append(info)
762
+ return result
763
+
764
+ def _check_handoff_cycle(self, from_agent: str, to_agent: str) -> bool:
765
+ """
766
+ Check if this handoff would create a problematic cycle.
767
+
768
+ Returns True if handoff should be blocked.
769
+ """
770
+ key = f"{from_agent}->{to_agent}"
771
+ self._handoff_counts[key] += 1
772
+
773
+ # Block after 2 cycles of same handoff pattern
774
+ if self._handoff_counts[key] > 2:
775
+ logger.warning(f"Handoff cycle detected: {key} occurred {self._handoff_counts[key]} times")
776
+ return True
777
+ return False
778
+
779
+ def handoff(
780
+ self,
781
+ from_agent: str,
782
+ to_agent: str,
783
+ message: str = ""
784
+ ) -> Optional[str]:
785
+ """
786
+ Execute a handoff from one agent to another.
787
+
788
+ Args:
789
+ from_agent: Source agent name
790
+ to_agent: Target agent name
791
+ message: Context message for target agent
792
+
793
+ Returns:
794
+ Error message if handoff blocked, None if successful
795
+ """
796
+ # Validate target agent exists
797
+ if to_agent not in self.agents:
798
+ return f"Unknown agent: {to_agent}. Available: {list(self.agents.keys())}"
799
+
800
+ # Check handoff limit
801
+ if len(self.handoff_log) >= self.max_handoffs:
802
+ return f"Maximum handoffs ({self.max_handoffs}) reached"
803
+
804
+ # Check for cycles
805
+ if self._check_handoff_cycle(from_agent, to_agent):
806
+ return f"Handoff cycle detected ({from_agent} -> {to_agent})"
807
+
808
+ # Record handoff
809
+ handoff = AgentHandoff(
810
+ from_agent=from_agent,
811
+ to_agent=to_agent,
812
+ message=message
813
+ )
814
+ self.handoff_log.append(handoff)
815
+ logger.info(f"Team '{self.name}': {from_agent} -> {to_agent}: {message}")
816
+
817
+ return None
818
+
819
+ def execute(
820
+ self,
821
+ task: str,
822
+ entry_agent: Optional[str] = None,
823
+ context: str = "",
824
+ tool_executor: Optional[Callable] = None
825
+ ) -> TeamResult:
826
+ """
827
+ Execute the team with a task.
828
+
829
+ Args:
830
+ task: The task to complete
831
+ entry_agent: Starting agent (defaults to supervisor)
832
+ context: Additional context
833
+ tool_executor: Function to execute custom tool bodies
834
+
835
+ Returns:
836
+ TeamResult with final response and execution details
837
+ """
838
+ start_time = time.time()
839
+ result = TeamResult(shared_context=dict(self.shared_context))
840
+
841
+ current_agent = entry_agent or self.supervisor
842
+ current_task = task
843
+ current_context = context
844
+ total_iterations = 0
845
+
846
+ try:
847
+ while True:
848
+ # Check iteration limit
849
+ if total_iterations >= self.max_total_iterations:
850
+ result.error = {
851
+ "message": f"Maximum total iterations ({self.max_total_iterations}) reached"
852
+ }
853
+ break
854
+
855
+ # Check handoff limit
856
+ if len(self.handoff_log) >= self.max_handoffs:
857
+ result.error = {
858
+ "message": f"Maximum handoffs ({self.max_handoffs}) reached"
859
+ }
860
+ break
861
+
862
+ # Get agent config
863
+ agent_config = self.agents.get(current_agent)
864
+ if not agent_config:
865
+ result.error = {"message": f"Unknown agent: {current_agent}"}
866
+ break
867
+
868
+ logger.info(f"Team '{self.name}': Executing agent '{current_agent}'")
869
+
870
+ # Build tools with built-in handlers
871
+ tools = self._build_agent_tools(current_agent, agent_config, tool_executor)
872
+
873
+ # Create tool executor that handles built-in tools
874
+ def team_tool_executor(tool_name: str, tool_args: dict, body: list):
875
+ return self._execute_tool(
876
+ tool_name, tool_args, body,
877
+ current_agent, tool_executor
878
+ )
879
+
880
+ # Execute agent
881
+ agent_result = self.agent_service.execute(
882
+ instruction=agent_config.get("instruction", ""),
883
+ tools=tools,
884
+ task=current_task,
885
+ context=current_context,
886
+ model=agent_config.get("model", "phi3"),
887
+ endpoint=agent_config.get("endpoint", ""),
888
+ provider=agent_config.get("provider", "auto"),
889
+ api_key=agent_config.get("api_key", ""),
890
+ max_iterations=agent_config.get("max_iterations", 10),
891
+ timeout_ms=agent_config.get("timeout", 60000),
892
+ tool_executor=team_tool_executor
893
+ )
894
+
895
+ # Store agent result
896
+ result.agent_results[current_agent] = agent_result
897
+ total_iterations += agent_result.iterations
898
+
899
+ # Check for handoff in actions
900
+ handoff_action = None
901
+ for action in agent_result.actions:
902
+ if action.tool == "handoff" and action.result:
903
+ if isinstance(action.result, dict) and "next_agent" in action.result:
904
+ handoff_action = action.result
905
+ break
906
+
907
+ if handoff_action:
908
+ # Execute handoff
909
+ next_agent = handoff_action["next_agent"]
910
+ handoff_message = handoff_action.get("message", "")
911
+
912
+ error = self.handoff(current_agent, next_agent, handoff_message)
913
+ if error:
914
+ result.error = {"message": error}
915
+ result.final_response = agent_result.result
916
+ result.final_agent = current_agent
917
+ break
918
+
919
+ # Continue with next agent
920
+ current_agent = next_agent
921
+ current_task = f"{task}\n\nHandoff context: {handoff_message}"
922
+ current_context = f"Previous agent ({current_agent}) handed off with message: {handoff_message}"
923
+
924
+ else:
925
+ # No handoff - this is the final response
926
+ result.success = agent_result.success
927
+ result.final_response = agent_result.result
928
+ result.final_agent = current_agent
929
+ if agent_result.error:
930
+ result.error = agent_result.error
931
+ break
932
+
933
+ except Exception as e:
934
+ logger.exception(f"Team execution error: {e}")
935
+ result.error = {"message": str(e)}
936
+
937
+ result.handoffs = self.handoff_log.copy()
938
+ result.total_iterations = total_iterations
939
+ result.execution_time_ms = (time.time() - start_time) * 1000
940
+ result.shared_context = dict(self.shared_context)
941
+
942
+ logger.info(f"Team '{self.name}' completed: success={result.success}, "
943
+ f"handoffs={len(result.handoffs)}, iterations={total_iterations}, "
944
+ f"final_agent={result.final_agent}")
945
+
946
+ return result
947
+
948
+ def _build_agent_tools(
949
+ self,
950
+ agent_name: str,
951
+ agent_config: Dict[str, Any],
952
+ tool_executor: Optional[Callable]
953
+ ) -> List[Dict[str, Any]]:
954
+ """Build tool definitions for an agent, including built-in tools."""
955
+ tools = []
956
+
957
+ # Get tools from agent config
958
+ config_tools = agent_config.get("tools", [])
959
+ for tool in config_tools:
960
+ if tool.get("builtin"):
961
+ # Add built-in tool definition
962
+ builtin_name = tool.get("name", "")
963
+ if builtin_name in BUILTIN_TOOLS:
964
+ builtin_def = BUILTIN_TOOLS[builtin_name].copy()
965
+ tools.append(builtin_def)
966
+ else:
967
+ tools.append(tool)
968
+
969
+ return tools
970
+
971
+ def _execute_tool(
972
+ self,
973
+ tool_name: str,
974
+ tool_args: Dict[str, Any],
975
+ body: list,
976
+ current_agent: str,
977
+ tool_executor: Optional[Callable]
978
+ ) -> Any:
979
+ """Execute a tool, handling built-in tools specially."""
980
+
981
+ # Handle built-in tools
982
+ if tool_name == "handoff":
983
+ target = tool_args.get("agent", "")
984
+ message = tool_args.get("message", "")
985
+
986
+ # Validate target exists
987
+ if target not in self.agents:
988
+ return {"error": f"Unknown agent: {target}. Available: {list(self.agents.keys())}"}
989
+
990
+ # Return handoff instruction (will be processed by execute loop)
991
+ return {"next_agent": target, "message": message}
992
+
993
+ elif tool_name == "readShared":
994
+ key = tool_args.get("key", "")
995
+ value = self.get_shared(key)
996
+ if value is None:
997
+ return f"Key '{key}' not found in shared context"
998
+ return value
999
+
1000
+ elif tool_name == "writeShared":
1001
+ key = tool_args.get("key", "")
1002
+ value = tool_args.get("value", "")
1003
+ self.set_shared(key, value)
1004
+ return f"Stored '{key}' in shared context"
1005
+
1006
+ elif tool_name == "listAgents":
1007
+ agents_info = self.get_agent_info()
1008
+ return agents_info
1009
+
1010
+ # Custom tool - use provided executor
1011
+ elif tool_executor and body:
1012
+ return tool_executor(tool_name, tool_args, body)
1013
+
1014
+ else:
1015
+ return f"Tool '{tool_name}' executed with args: {tool_args}"
1016
+
1017
+
1018
+ class MultiAgentService:
1019
+ """
1020
+ Service for managing multi-agent teams.
1021
+
1022
+ Provides:
1023
+ - Team creation and registration
1024
+ - Team execution
1025
+ - Global agent registry
1026
+ """
1027
+
1028
+ def __init__(self, agent_service: Optional[AgentService] = None):
1029
+ """Initialize multi-agent service."""
1030
+ self._agent_service = agent_service
1031
+ self.registry = AgentRegistry()
1032
+
1033
+ @property
1034
+ def agent_service(self) -> AgentService:
1035
+ """Lazy-load agent service."""
1036
+ if self._agent_service is None:
1037
+ self._agent_service = get_agent_service()
1038
+ return self._agent_service
1039
+
1040
+ def create_team(
1041
+ self,
1042
+ name: str,
1043
+ agents: Dict[str, Dict[str, Any]],
1044
+ shared: Optional[Dict[str, Any]] = None,
1045
+ supervisor: str = "",
1046
+ max_handoffs: int = 10,
1047
+ max_total_iterations: int = 50
1048
+ ) -> AgentTeam:
1049
+ """
1050
+ Create and register a new agent team.
1051
+
1052
+ Args:
1053
+ name: Team name
1054
+ agents: Dict of agent name -> agent config
1055
+ shared: Initial shared context
1056
+ supervisor: Entry agent name
1057
+ max_handoffs: Maximum handoffs allowed
1058
+ max_total_iterations: Max total iterations
1059
+
1060
+ Returns:
1061
+ AgentTeam instance
1062
+ """
1063
+ team = AgentTeam(
1064
+ name=name,
1065
+ agents=agents,
1066
+ shared_context=shared,
1067
+ supervisor=supervisor,
1068
+ max_handoffs=max_handoffs,
1069
+ max_total_iterations=max_total_iterations,
1070
+ agent_service=self.agent_service
1071
+ )
1072
+
1073
+ self.registry.register_team(name, team)
1074
+ return team
1075
+
1076
+ def get_team(self, name: str) -> Optional[AgentTeam]:
1077
+ """Get a registered team by name."""
1078
+ return self.registry.get_team(name)
1079
+
1080
+ def execute_team(
1081
+ self,
1082
+ name: str,
1083
+ task: str,
1084
+ entry_agent: Optional[str] = None,
1085
+ context: str = "",
1086
+ tool_executor: Optional[Callable] = None
1087
+ ) -> TeamResult:
1088
+ """
1089
+ Execute a registered team.
1090
+
1091
+ Args:
1092
+ name: Team name
1093
+ task: Task to complete
1094
+ entry_agent: Starting agent
1095
+ context: Additional context
1096
+ tool_executor: Custom tool executor
1097
+
1098
+ Returns:
1099
+ TeamResult
1100
+ """
1101
+ team = self.get_team(name)
1102
+ if not team:
1103
+ result = TeamResult()
1104
+ result.error = {"message": f"Team '{name}' not found"}
1105
+ return result
1106
+
1107
+ return team.execute(task, entry_agent, context, tool_executor)
1108
+
1109
+
1110
+ # Global singleton for multi-agent service
1111
+ _multi_agent_service: Optional[MultiAgentService] = None
1112
+
1113
+
1114
+ def get_multi_agent_service(agent_service: Optional[AgentService] = None) -> MultiAgentService:
1115
+ """
1116
+ Get or create the global MultiAgentService instance.
1117
+
1118
+ Args:
1119
+ agent_service: Optional AgentService to use
1120
+
1121
+ Returns:
1122
+ MultiAgentService instance
1123
+ """
1124
+ global _multi_agent_service
1125
+ if _multi_agent_service is None:
1126
+ _multi_agent_service = MultiAgentService(agent_service)
1127
+ return _multi_agent_service
1128
+
1129
+
1130
+ def reset_multi_agent_service():
1131
+ """Reset the global multi-agent service (for testing)."""
1132
+ global _multi_agent_service
1133
+ _multi_agent_service = None