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.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +417 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +527 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +547 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2611 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +148 -0
- quantum/core/expressions.py +591 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_fetching/src/__init__.py +21 -0
- quantum/core/features/data_fetching/src/ast_node.py +312 -0
- quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
- quantum/core/features/data_fetching/src/html_adapter.py +474 -0
- quantum/core/features/data_fetching/src/parser.py +225 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +291 -0
- quantum/core/features/data_import/src/runtime.py +538 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +146 -0
- quantum/core/features/invocation/src/runtime.py +327 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/database_service.py +322 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +228 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
- quantum/core/features/ui_engine/src/parser.py +1106 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1235 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +68 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +89 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +217 -0
- quantum/core/parsers/data/invoke_parser.py +116 -0
- quantum/core/parsers/data/query_parser.py +188 -0
- quantum/core/parsers/data/transaction_parser.py +77 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +54 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +137 -0
- quantum/core/parsers/functions/param_parser.py +66 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/persistence/__init__.py +9 -0
- quantum/core/parsers/persistence/persist_parser.py +64 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +82 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +28 -0
- quantum/runtime/action_handler.py +443 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/api_server.py +86 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +990 -0
- quantum/runtime/component_composer.py +319 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/database_service.py +598 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +286 -0
- quantum/runtime/executor_registry.py +171 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +262 -0
- quantum/runtime/executors/control_flow/__init__.py +11 -0
- quantum/runtime/executors/control_flow/if_executor.py +93 -0
- quantum/runtime/executors/control_flow/loop_executor.py +307 -0
- quantum/runtime/executors/control_flow/set_executor.py +412 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +145 -0
- quantum/runtime/executors/data/invoke_executor.py +176 -0
- quantum/runtime/executors/data/query_executor.py +256 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +500 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/logging_setup.py +140 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +184 -0
- quantum/runtime/pypy_compat.py +315 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +733 -0
- quantum/runtime/service_container.py +444 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +188 -0
- quantum/runtime/ui_desktop_adapter.py +1730 -0
- quantum/runtime/ui_desktop_templates.py +307 -0
- quantum/runtime/ui_html_adapter.py +2691 -0
- quantum/runtime/ui_html_templates.py +2297 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +1766 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/utils/__init__.py +1 -0
- quantum_framework-0.9.0.dist-info/METADATA +244 -0
- quantum_framework-0.9.0.dist-info/RECORD +262 -0
- quantum_framework-0.9.0.dist-info/WHEEL +5 -0
- quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
- 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
|