kite-agent 0.1.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.
- kite/__init__.py +46 -0
- kite/ab_testing.py +384 -0
- kite/agent.py +556 -0
- kite/agents/__init__.py +3 -0
- kite/agents/plan_execute.py +191 -0
- kite/agents/react_agent.py +509 -0
- kite/agents/reflective_agent.py +90 -0
- kite/agents/rewoo.py +119 -0
- kite/agents/tot.py +151 -0
- kite/conversation.py +125 -0
- kite/core.py +974 -0
- kite/data_loaders.py +111 -0
- kite/embedding_providers.py +372 -0
- kite/llm_providers.py +1278 -0
- kite/memory/__init__.py +6 -0
- kite/memory/advanced_rag.py +333 -0
- kite/memory/graph_rag.py +719 -0
- kite/memory/session_memory.py +423 -0
- kite/memory/vector_memory.py +579 -0
- kite/monitoring.py +611 -0
- kite/observers.py +107 -0
- kite/optimization/__init__.py +9 -0
- kite/optimization/resource_router.py +80 -0
- kite/persistence.py +42 -0
- kite/pipeline/__init__.py +5 -0
- kite/pipeline/deterministic_pipeline.py +323 -0
- kite/pipeline/reactive_pipeline.py +171 -0
- kite/pipeline_manager.py +15 -0
- kite/routing/__init__.py +6 -0
- kite/routing/aggregator_router.py +325 -0
- kite/routing/llm_router.py +149 -0
- kite/routing/semantic_router.py +228 -0
- kite/safety/__init__.py +6 -0
- kite/safety/circuit_breaker.py +360 -0
- kite/safety/guardrails.py +82 -0
- kite/safety/idempotency_manager.py +304 -0
- kite/safety/kill_switch.py +75 -0
- kite/tool.py +183 -0
- kite/tool_registry.py +87 -0
- kite/tools/__init__.py +21 -0
- kite/tools/code_execution.py +53 -0
- kite/tools/contrib/__init__.py +19 -0
- kite/tools/contrib/calculator.py +26 -0
- kite/tools/contrib/datetime_utils.py +20 -0
- kite/tools/contrib/linkedin.py +428 -0
- kite/tools/contrib/web_search.py +30 -0
- kite/tools/mcp/__init__.py +31 -0
- kite/tools/mcp/database_mcp.py +267 -0
- kite/tools/mcp/gdrive_mcp_server.py +503 -0
- kite/tools/mcp/gmail_mcp_server.py +601 -0
- kite/tools/mcp/postgres_mcp_server.py +490 -0
- kite/tools/mcp/slack_mcp_server.py +538 -0
- kite/tools/mcp/stripe_mcp_server.py +219 -0
- kite/tools/search.py +90 -0
- kite/tools/system_tools.py +54 -0
- kite/tools_manager.py +27 -0
- kite_agent-0.1.0.dist-info/METADATA +621 -0
- kite_agent-0.1.0.dist-info/RECORD +61 -0
- kite_agent-0.1.0.dist-info/WHEEL +5 -0
- kite_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- kite_agent-0.1.0.dist-info/top_level.txt +1 -0
kite/core.py
ADDED
|
@@ -0,0 +1,974 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kite Framework - Core
|
|
3
|
+
General-purpose framework for ANY agentic AI application.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import logging
|
|
8
|
+
import json
|
|
9
|
+
from typing import Dict, Optional, Any, Callable, List
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from .data_loaders import DocumentLoader
|
|
12
|
+
from .monitoring import get_metrics, get_tracer
|
|
13
|
+
|
|
14
|
+
logging.basicConfig(
|
|
15
|
+
level=logging.INFO,
|
|
16
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
class EventBus:
|
|
20
|
+
"""Simple asynchronous event bus for pub/sub monitoring."""
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self._subscribers = {}
|
|
23
|
+
self._relays = []
|
|
24
|
+
self.logger = logging.getLogger("EventBus")
|
|
25
|
+
|
|
26
|
+
def add_relay(self, url: str):
|
|
27
|
+
if url not in self._relays:
|
|
28
|
+
self._relays.append(url)
|
|
29
|
+
self.logger.info(f"Added event relay to: {url}")
|
|
30
|
+
|
|
31
|
+
def subscribe(self, event_name: str, callback: Callable):
|
|
32
|
+
if event_name not in self._subscribers:
|
|
33
|
+
self._subscribers[event_name] = []
|
|
34
|
+
if callback not in self._subscribers[event_name]:
|
|
35
|
+
self._subscribers[event_name].append(callback)
|
|
36
|
+
self.logger.debug(f"Subscribed to {event_name}")
|
|
37
|
+
|
|
38
|
+
def unsubscribe(self, event_name: str, callback: Callable):
|
|
39
|
+
"""Remove a subscription."""
|
|
40
|
+
if event_name in self._subscribers:
|
|
41
|
+
try:
|
|
42
|
+
self._subscribers[event_name].remove(callback)
|
|
43
|
+
self.logger.debug(f"Unsubscribed from {event_name}")
|
|
44
|
+
except ValueError:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
def emit(self, event_name: str, data: Any):
|
|
48
|
+
self.logger.debug(f"Emitting {event_name}")
|
|
49
|
+
callbacks = self._subscribers.get(event_name, [])
|
|
50
|
+
# Also support catch-all "*" subscribers
|
|
51
|
+
callbacks += self._subscribers.get("*", [])
|
|
52
|
+
|
|
53
|
+
for cb in callbacks:
|
|
54
|
+
try:
|
|
55
|
+
import asyncio
|
|
56
|
+
if asyncio.iscoroutinefunction(cb):
|
|
57
|
+
asyncio.create_task(cb(event_name, data))
|
|
58
|
+
else:
|
|
59
|
+
cb(event_name, data)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
self.logger.error(f"Error in event callback for {event_name}: {e}")
|
|
62
|
+
|
|
63
|
+
# Also emit to external relays
|
|
64
|
+
if self._relays:
|
|
65
|
+
for relay_url in self._relays:
|
|
66
|
+
try:
|
|
67
|
+
import asyncio
|
|
68
|
+
asyncio.create_task(self._relay_event(relay_url, event_name, data))
|
|
69
|
+
except: pass
|
|
70
|
+
|
|
71
|
+
async def _relay_event(self, url: str, event: str, data: Any):
|
|
72
|
+
try:
|
|
73
|
+
import httpx
|
|
74
|
+
async with httpx.AsyncClient() as client:
|
|
75
|
+
resp = await client.post(url, json={"event": event, "data": data, "timestamp": datetime.now().isoformat()}, timeout=2.0)
|
|
76
|
+
if resp.status_code != 200:
|
|
77
|
+
self.logger.debug(f"Relay {url} returned {resp.status_code}")
|
|
78
|
+
except Exception as e:
|
|
79
|
+
# Silence relay errors to avoid console noise when dashboard is down
|
|
80
|
+
self.logger.debug(f"Failed to relay event {event} to {url}: {e}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class KnowledgeStore:
|
|
84
|
+
"""Lightweight knowledge manager for query templates and context."""
|
|
85
|
+
def __init__(self, knowledge_dir: str = "knowledge"):
|
|
86
|
+
self.knowledge_dir = knowledge_dir
|
|
87
|
+
self.data = {}
|
|
88
|
+
self.load()
|
|
89
|
+
|
|
90
|
+
def load(self):
|
|
91
|
+
if not os.path.exists(self.knowledge_dir):
|
|
92
|
+
os.makedirs(self.knowledge_dir)
|
|
93
|
+
return
|
|
94
|
+
for filename in os.listdir(self.knowledge_dir):
|
|
95
|
+
if filename.endswith(".json"):
|
|
96
|
+
path = os.path.join(self.knowledge_dir, filename)
|
|
97
|
+
try:
|
|
98
|
+
with open(path, "r") as f:
|
|
99
|
+
name = filename.replace(".json", "")
|
|
100
|
+
self.data[name] = json.load(f)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
print(f"Error loading knowledge {filename}: {e}")
|
|
103
|
+
|
|
104
|
+
def get(self, key_path: str, default: Any = None) -> Any:
|
|
105
|
+
# Support dot notation: "linkedin_queries.b2b_software"
|
|
106
|
+
parts = key_path.split(".")
|
|
107
|
+
current = self.data
|
|
108
|
+
for p in parts:
|
|
109
|
+
if isinstance(current, dict) and p in current:
|
|
110
|
+
current = current[p]
|
|
111
|
+
else:
|
|
112
|
+
return default
|
|
113
|
+
return current
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class Kite:
|
|
117
|
+
"""
|
|
118
|
+
General-Purpose Agentic AI Framework (Kite)
|
|
119
|
+
|
|
120
|
+
Build ANY agentic application - not limited to customer support.
|
|
121
|
+
|
|
122
|
+
Foundation components:
|
|
123
|
+
- ai.llm # LLM provider
|
|
124
|
+
- ai.embeddings # Embedding provider
|
|
125
|
+
- ai.circuit_breaker # Safety
|
|
126
|
+
- ai.idempotency # Safety
|
|
127
|
+
- ai.vector_memory # Memory
|
|
128
|
+
- ai.session_memory # Memory
|
|
129
|
+
- ai.graph_rag # Memory
|
|
130
|
+
- ai.semantic_router # Routing
|
|
131
|
+
- ai.tools # Tool registry
|
|
132
|
+
- ai.slm # SLM specialists
|
|
133
|
+
- ai.pipeline # Workflow system
|
|
134
|
+
- ai.chat(messages)
|
|
135
|
+
- ai.complete(prompt)
|
|
136
|
+
- ai.embed(text)
|
|
137
|
+
- ai.create_agent(...)
|
|
138
|
+
- ai.create_tool(...)
|
|
139
|
+
- ai.create_workflow(...)
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(self, config: Optional[Dict] = None):
|
|
143
|
+
# Initialize logger first for debug output
|
|
144
|
+
self.logger = logging.getLogger("Kite")
|
|
145
|
+
|
|
146
|
+
# Always load environment defaults first
|
|
147
|
+
self.config = self._load_config()
|
|
148
|
+
# Merge with user-provided config if any
|
|
149
|
+
if config:
|
|
150
|
+
self.config.update(config)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# Lazy storage
|
|
154
|
+
self._llm = None
|
|
155
|
+
self._embeddings = None
|
|
156
|
+
self._vector_memory = None
|
|
157
|
+
self._session_memory = None
|
|
158
|
+
self._graph_rag = None
|
|
159
|
+
self._semantic_router = None
|
|
160
|
+
self._aggregator_router = None
|
|
161
|
+
self._tools = None
|
|
162
|
+
self._pipeline = None
|
|
163
|
+
self._advanced_rag = None
|
|
164
|
+
self._cache = None
|
|
165
|
+
self._db_mcp = None
|
|
166
|
+
|
|
167
|
+
# Monitoring & Observability
|
|
168
|
+
self.metrics = get_metrics()
|
|
169
|
+
self.tracer = get_tracer()
|
|
170
|
+
|
|
171
|
+
# New Event System for Pub/Sub monitoring
|
|
172
|
+
self.event_bus = EventBus()
|
|
173
|
+
self.knowledge = KnowledgeStore()
|
|
174
|
+
|
|
175
|
+
self.data_loader = DocumentLoader()
|
|
176
|
+
|
|
177
|
+
# State management for transient run data
|
|
178
|
+
self.state = {}
|
|
179
|
+
|
|
180
|
+
# Only initialize core safety eagerly
|
|
181
|
+
self._init_safety()
|
|
182
|
+
|
|
183
|
+
self.logger.info("[OK] Kite initialized (lazy-loading enabled)")
|
|
184
|
+
|
|
185
|
+
def enable_tracing(self, filename: str = "process_trace.json"):
|
|
186
|
+
"""Enable native JSON file tracing."""
|
|
187
|
+
from .observers import EventFileLogger
|
|
188
|
+
logger = EventFileLogger(filename)
|
|
189
|
+
self.event_bus.subscribe("*", logger.on_event)
|
|
190
|
+
self.logger.info(f"Native tracing enabled: {filename}")
|
|
191
|
+
return logger
|
|
192
|
+
|
|
193
|
+
def enable_state_tracking(self, session_file: str = "session.json", event_map: Dict[str, str] = None):
|
|
194
|
+
"""Enable native state tracking for the run."""
|
|
195
|
+
from .observers import StateTracker
|
|
196
|
+
tracker = StateTracker(session_file, event_map)
|
|
197
|
+
self.event_bus.subscribe("*", tracker.on_event)
|
|
198
|
+
return tracker
|
|
199
|
+
|
|
200
|
+
def add_knowledge_source(self, source_type: str, path: str, name: str, use_vector: bool = True):
|
|
201
|
+
"""Explicitly register and index a knowledge source."""
|
|
202
|
+
self.logger.info(f"Adding knowledge source: {name} (Type: {source_type}, Vector: {use_vector})")
|
|
203
|
+
|
|
204
|
+
if source_type == "local_json":
|
|
205
|
+
if not os.path.exists(path):
|
|
206
|
+
self.logger.error(f"Knowledge file not found: {path}")
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
with open(path, 'r') as f:
|
|
210
|
+
data = json.load(f)
|
|
211
|
+
# Store in KnowledgeStore for structured access
|
|
212
|
+
self.knowledge.data[name] = data
|
|
213
|
+
|
|
214
|
+
# Also index into VectorMemory for semantic retrieval
|
|
215
|
+
if use_vector:
|
|
216
|
+
for key, val in data.items():
|
|
217
|
+
doc_id = f"k_{name}_{key}"
|
|
218
|
+
text = f"Expert info for {key}: {val}"
|
|
219
|
+
self.vector_memory.add_document(doc_id, text, metadata={"source": name, "key": key})
|
|
220
|
+
|
|
221
|
+
elif source_type == "vector_db":
|
|
222
|
+
self.logger.info(f"Connected to external vector source: {path}")
|
|
223
|
+
# Logic for external DB connection
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
elif source_type == "mcp_resource":
|
|
227
|
+
self.logger.info(f"Registering MCP Resource: {path}")
|
|
228
|
+
# Logic for MCP integration
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
def add_event_relay(self, url: str):
|
|
232
|
+
"""Add an external HTTP endpoint to relay all events to."""
|
|
233
|
+
if hasattr(self, 'event_bus'):
|
|
234
|
+
self.event_bus.add_relay(url)
|
|
235
|
+
self.logger.info(f"Added event relay to: {url}")
|
|
236
|
+
|
|
237
|
+
def load_document(self, path: str, doc_id: Optional[str] = None):
|
|
238
|
+
"""Load and store document(s) using DocumentLoader."""
|
|
239
|
+
if os.path.isdir(path):
|
|
240
|
+
self.logger.info(f"Loading directory: {path}")
|
|
241
|
+
data = self.data_loader.load_directory(path)
|
|
242
|
+
if not data:
|
|
243
|
+
self.logger.warning(f" No supported files found in {path}")
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
for filename, text in data.items():
|
|
247
|
+
if text.startswith("Error"):
|
|
248
|
+
self.logger.warning(f" Skipping {filename}: {text}")
|
|
249
|
+
continue
|
|
250
|
+
if text.startswith("Unsupported"):
|
|
251
|
+
self.logger.warning(f" Skipping {filename} (Unsupported extension)")
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
did = f"{doc_id}_{filename}" if doc_id else filename
|
|
255
|
+
self.logger.info(f" Adding document: {did}")
|
|
256
|
+
self.vector_memory.add_document(did, text)
|
|
257
|
+
return True
|
|
258
|
+
else:
|
|
259
|
+
self.logger.info(f"Loading file: {path}")
|
|
260
|
+
text = self.data_loader.load_any(path)
|
|
261
|
+
if text.startswith("Error") or text.startswith("Unsupported"):
|
|
262
|
+
self.logger.error(text)
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
did = doc_id or os.path.basename(path)
|
|
266
|
+
self.vector_memory.add_document(did, text)
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
def _load_config(self) -> Dict:
|
|
270
|
+
from dotenv import load_dotenv
|
|
271
|
+
import os
|
|
272
|
+
from pathlib import Path
|
|
273
|
+
|
|
274
|
+
# Find .env file explicitly
|
|
275
|
+
env_path = Path.cwd() / '.env'
|
|
276
|
+
if env_path.exists():
|
|
277
|
+
self.logger.debug(f"Found .env at: {env_path}")
|
|
278
|
+
# Load with explicit path
|
|
279
|
+
loaded = load_dotenv(dotenv_path=str(env_path), override=True)
|
|
280
|
+
self.logger.debug(f"load_dotenv(path={env_path}) returned: {loaded}")
|
|
281
|
+
else:
|
|
282
|
+
self.logger.debug(f"No .env found at: {env_path}, trying default load")
|
|
283
|
+
loaded = load_dotenv(override=True)
|
|
284
|
+
self.logger.debug(f"load_dotenv() returned: {loaded}")
|
|
285
|
+
|
|
286
|
+
# Check what was loaded
|
|
287
|
+
provider_from_env = os.getenv('LLM_PROVIDER')
|
|
288
|
+
self.logger.debug(f"After load_dotenv: LLM_PROVIDER={provider_from_env}")
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
'llm_provider': os.getenv('LLM_PROVIDER', 'ollama'),
|
|
292
|
+
'llm_model': os.getenv('LLM_MODEL'),
|
|
293
|
+
'openai_api_key': os.getenv('OPENAI_API_KEY'),
|
|
294
|
+
'groq_api_key': os.getenv('GROQ_API_KEY'),
|
|
295
|
+
'vector_backend': os.getenv('VECTOR_BACKEND', 'memory'), # Default to memory if not set
|
|
296
|
+
'circuit_breaker_threshold': int(os.getenv('CIRCUIT_BREAKER_FAILURE_THRESHOLD', 3)),
|
|
297
|
+
'circuit_breaker_timeout': int(os.getenv('CIRCUIT_BREAKER_TIMEOUT_SECONDS', 60)),
|
|
298
|
+
'llm_timeout': float(os.getenv('LLM_TIMEOUT', 600.0)),
|
|
299
|
+
'semantic_router_threshold': float(os.getenv('SEMANTIC_ROUTER_THRESHOLD', 0.3)),
|
|
300
|
+
'llm_timeout': float(os.getenv('LLM_TIMEOUT', 600.0)),
|
|
301
|
+
'max_iterations': int(os.getenv('MAX_ITERATIONS', 10)),
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
@property
|
|
305
|
+
def llm(self):
|
|
306
|
+
if self._llm is None:
|
|
307
|
+
from .llm_providers import LLMFactory
|
|
308
|
+
try:
|
|
309
|
+
# Priority 1: Check config
|
|
310
|
+
llm_provider = self.config.get('llm_provider')
|
|
311
|
+
llm_model = self.config.get('llm_model')
|
|
312
|
+
|
|
313
|
+
# Priority 2: Check environment variables if not in config
|
|
314
|
+
if not llm_provider:
|
|
315
|
+
llm_provider = os.getenv('LLM_PROVIDER')
|
|
316
|
+
if not llm_model:
|
|
317
|
+
llm_model = os.getenv('LLM_MODEL')
|
|
318
|
+
|
|
319
|
+
if llm_provider:
|
|
320
|
+
api_key = self.config.get(f'{llm_provider}_api_key') or os.getenv(f'{llm_provider.upper()}_API_KEY')
|
|
321
|
+
self._llm = LLMFactory.create(
|
|
322
|
+
llm_provider,
|
|
323
|
+
llm_model,
|
|
324
|
+
api_key=api_key,
|
|
325
|
+
timeout=self.config.get('llm_timeout', 600.0)
|
|
326
|
+
)
|
|
327
|
+
self.logger.info(f"[OK] Using {llm_provider}/{llm_model or 'default'} from config/env")
|
|
328
|
+
else:
|
|
329
|
+
# Priority 3: Auto-detect
|
|
330
|
+
self._llm = LLMFactory.auto_detect(timeout=self.config.get('llm_timeout', 600.0))
|
|
331
|
+
except Exception as e:
|
|
332
|
+
self.logger.warning(f" LLM initialization failed: {e}")
|
|
333
|
+
self._llm = LLMFactory.auto_detect()
|
|
334
|
+
return self._llm
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def embeddings(self):
|
|
338
|
+
if self._embeddings is None:
|
|
339
|
+
from .embedding_providers import EmbeddingFactory
|
|
340
|
+
try:
|
|
341
|
+
emb_provider = self.config.get('embedding_provider')
|
|
342
|
+
if emb_provider:
|
|
343
|
+
self._embeddings = EmbeddingFactory.create(
|
|
344
|
+
emb_provider,
|
|
345
|
+
self.config.get('embedding_model')
|
|
346
|
+
)
|
|
347
|
+
else:
|
|
348
|
+
self._embeddings = EmbeddingFactory.auto_detect()
|
|
349
|
+
except Exception as e:
|
|
350
|
+
self.logger.warning(f" Embedding initialization failed: {e}")
|
|
351
|
+
self._embeddings = EmbeddingFactory.auto_detect()
|
|
352
|
+
return self._embeddings
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def vector_memory(self):
|
|
356
|
+
if self._vector_memory is None:
|
|
357
|
+
from .memory.vector_memory import VectorMemory
|
|
358
|
+
backend = self.config.get('vector_backend', 'memory')
|
|
359
|
+
self._vector_memory = VectorMemory(
|
|
360
|
+
backend=backend,
|
|
361
|
+
embedding_provider=self.embeddings
|
|
362
|
+
)
|
|
363
|
+
self.logger.info(f" [OK] Vector Memory initialized (Backend: {backend})")
|
|
364
|
+
return self._vector_memory
|
|
365
|
+
|
|
366
|
+
@property
|
|
367
|
+
def session_memory(self):
|
|
368
|
+
if self._session_memory is None:
|
|
369
|
+
from .memory.session_memory import SessionMemory
|
|
370
|
+
self._session_memory = SessionMemory()
|
|
371
|
+
return self._session_memory
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def graph_rag(self):
|
|
375
|
+
if self._graph_rag is None:
|
|
376
|
+
from .memory.graph_rag import GraphRAG
|
|
377
|
+
self._graph_rag = GraphRAG(llm=self.llm)
|
|
378
|
+
return self._graph_rag
|
|
379
|
+
|
|
380
|
+
@property
|
|
381
|
+
def semantic_router(self):
|
|
382
|
+
if self._semantic_router is None:
|
|
383
|
+
# Default to LLM router, config overrides allowed
|
|
384
|
+
router_type = self.config.get('router_type', 'llm')
|
|
385
|
+
|
|
386
|
+
if router_type == 'llm':
|
|
387
|
+
from .routing import LLMRouter
|
|
388
|
+
from .llm_providers import LLMFactory
|
|
389
|
+
|
|
390
|
+
# Determine Router LLM
|
|
391
|
+
router_llm = self.llm # Default to main LLM
|
|
392
|
+
|
|
393
|
+
# OPTIMIZATION: Use FAST_LLM_MODEL for routing if available and no specific router is set
|
|
394
|
+
fast_model = self.config.get('fast_model') or os.getenv('FAST_LLM_MODEL')
|
|
395
|
+
r_provider = self.config.get('router_llm_provider')
|
|
396
|
+
r_model = self.config.get('router_llm_model')
|
|
397
|
+
|
|
398
|
+
# Logic: Explicit Router Config > Fast Model Config > Main LLM
|
|
399
|
+
target_provider = r_provider
|
|
400
|
+
target_model = r_model
|
|
401
|
+
|
|
402
|
+
if not target_provider and fast_model:
|
|
403
|
+
# Auto-detect provider/model from string like "groq/llama-3.1-8b"
|
|
404
|
+
if "/" in fast_model:
|
|
405
|
+
parts = fast_model.split("/", 1)
|
|
406
|
+
target_provider = parts[0]
|
|
407
|
+
target_model = parts[1]
|
|
408
|
+
self.logger.info(f" [Router] Opting for FAST model: {target_provider}/{target_model}")
|
|
409
|
+
|
|
410
|
+
if target_provider:
|
|
411
|
+
try:
|
|
412
|
+
r_api_key = self.config.get(f'{target_provider}_api_key') or os.getenv(f'{target_provider.upper()}_API_KEY')
|
|
413
|
+
router_llm = LLMFactory.create(
|
|
414
|
+
target_provider,
|
|
415
|
+
target_model,
|
|
416
|
+
api_key=r_api_key,
|
|
417
|
+
timeout=self.config.get('llm_timeout', 60.0)
|
|
418
|
+
)
|
|
419
|
+
self.logger.info(f" [OK] Router using dedicated LLM: {target_provider}/{target_model}")
|
|
420
|
+
except Exception as e:
|
|
421
|
+
self.logger.warning(f"Failed to init Router LLM ({target_provider}): {e}. Fallback to default.")
|
|
422
|
+
|
|
423
|
+
self._semantic_router = LLMRouter(llm=router_llm)
|
|
424
|
+
else:
|
|
425
|
+
from .routing import SemanticRouter
|
|
426
|
+
self._semantic_router = SemanticRouter(
|
|
427
|
+
confidence_threshold=self.config.get('semantic_router_threshold', 0.3),
|
|
428
|
+
embedding_provider=self.embeddings
|
|
429
|
+
)
|
|
430
|
+
return self._semantic_router
|
|
431
|
+
|
|
432
|
+
@property
|
|
433
|
+
def aggregator_router(self):
|
|
434
|
+
if self._aggregator_router is None:
|
|
435
|
+
from .routing import AggregatorRouter
|
|
436
|
+
self._aggregator_router = AggregatorRouter(llm=self.llm)
|
|
437
|
+
return self._aggregator_router
|
|
438
|
+
|
|
439
|
+
@property
|
|
440
|
+
def tools(self):
|
|
441
|
+
if self._tools is None:
|
|
442
|
+
from .tool_registry import ToolRegistry
|
|
443
|
+
self._tools = ToolRegistry(self.config, self.logger)
|
|
444
|
+
# Centralized registration of official contrib tools
|
|
445
|
+
self._tools.load_standard_tools(self)
|
|
446
|
+
return self._tools
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def pipeline(self):
|
|
450
|
+
if self._pipeline is None:
|
|
451
|
+
from .pipeline_manager import PipelineManager
|
|
452
|
+
from .pipeline import DeterministicPipeline
|
|
453
|
+
self._pipeline = PipelineManager(DeterministicPipeline, self.logger)
|
|
454
|
+
return self._pipeline
|
|
455
|
+
|
|
456
|
+
@property
|
|
457
|
+
def advanced_rag(self):
|
|
458
|
+
if self._advanced_rag is None:
|
|
459
|
+
from .memory.advanced_rag import AdvancedRAG
|
|
460
|
+
self._advanced_rag = AdvancedRAG(self.vector_memory, self.llm)
|
|
461
|
+
return self._advanced_rag
|
|
462
|
+
|
|
463
|
+
@property
|
|
464
|
+
def cache(self):
|
|
465
|
+
if self._cache is None:
|
|
466
|
+
from .caching.cache_manager import CacheManager
|
|
467
|
+
self._cache = CacheManager(self.vector_memory)
|
|
468
|
+
return self._cache
|
|
469
|
+
|
|
470
|
+
@property
|
|
471
|
+
def db_mcp(self):
|
|
472
|
+
if self._db_mcp is None:
|
|
473
|
+
from .tools.mcp.database_mcp import DatabaseMCP
|
|
474
|
+
self._db_mcp = DatabaseMCP()
|
|
475
|
+
return self._db_mcp
|
|
476
|
+
|
|
477
|
+
def _init_safety(self):
|
|
478
|
+
"""Initialize safety patterns."""
|
|
479
|
+
from .safety import CircuitBreaker, IdempotencyManager, CircuitBreakerConfig, IdempotencyConfig
|
|
480
|
+
|
|
481
|
+
self.circuit_breaker = CircuitBreaker(
|
|
482
|
+
name="system",
|
|
483
|
+
config=CircuitBreakerConfig(
|
|
484
|
+
failure_threshold=self.config.get('circuit_breaker_threshold', 5),
|
|
485
|
+
timeout_seconds=self.config.get('circuit_breaker_timeout', 60)
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
self.idempotency = IdempotencyManager(
|
|
490
|
+
config=IdempotencyConfig(storage_backend='memory')
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
self.logger.info(" [OK] Safety")
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
# ==========================================================================
|
|
497
|
+
# GENERAL-PURPOSE METHODS
|
|
498
|
+
# ==========================================================================
|
|
499
|
+
|
|
500
|
+
def chat(self, messages: List[Dict], **kwargs) -> str:
|
|
501
|
+
"""Chat with LLM."""
|
|
502
|
+
return self.llm.chat(messages, **kwargs)
|
|
503
|
+
|
|
504
|
+
def complete(self, prompt: str, **kwargs) -> str:
|
|
505
|
+
"""Complete prompt."""
|
|
506
|
+
return self.llm.complete(prompt, **kwargs)
|
|
507
|
+
|
|
508
|
+
def embed(self, text: str) -> List[float]:
|
|
509
|
+
"""Generate embedding."""
|
|
510
|
+
return self.embeddings.embed(text)
|
|
511
|
+
|
|
512
|
+
def embed_batch(self, texts: List[str]) -> List[List[float]]:
|
|
513
|
+
"""Generate embeddings for multiple texts."""
|
|
514
|
+
return self.embeddings.embed_batch(texts)
|
|
515
|
+
|
|
516
|
+
def create_tool(self, name: str, func: Callable, description: str = None) -> Any:
|
|
517
|
+
"""Create and register a tool."""
|
|
518
|
+
from .tool import Tool
|
|
519
|
+
|
|
520
|
+
tool_desc = description or func.__doc__ or "No description"
|
|
521
|
+
tool = Tool(
|
|
522
|
+
name=name,
|
|
523
|
+
func=func,
|
|
524
|
+
description=tool_desc
|
|
525
|
+
)
|
|
526
|
+
self.tools.register(name, tool)
|
|
527
|
+
return tool
|
|
528
|
+
|
|
529
|
+
def create_agent(self,
|
|
530
|
+
name: str,
|
|
531
|
+
system_prompt: str,
|
|
532
|
+
tools: List = None,
|
|
533
|
+
llm_provider: str = None,
|
|
534
|
+
llm_model: str = None,
|
|
535
|
+
model: str = None, # Alias for llm_model
|
|
536
|
+
agent_type: str = "base",
|
|
537
|
+
knowledge_sources: List[str] = None,
|
|
538
|
+
verbose: bool = False):
|
|
539
|
+
"""
|
|
540
|
+
Create custom agent with optional specific AI configuration.
|
|
541
|
+
Supported types: 'simple' (one-shot), 'react' (looping), 'plan_execute', 'rewoo', 'tot'
|
|
542
|
+
"""
|
|
543
|
+
from .agent import Agent
|
|
544
|
+
from .agents.react_agent import ReActAgent
|
|
545
|
+
from .agents.plan_execute import PlanExecuteAgent
|
|
546
|
+
from .agents.rewoo import ReWOOAgent
|
|
547
|
+
from .agents.tot import TreeOfThoughtsAgent
|
|
548
|
+
from .llm_providers import LLMFactory
|
|
549
|
+
|
|
550
|
+
# 1. Normalize Model/Provider
|
|
551
|
+
target_model = llm_model or model
|
|
552
|
+
target_provider = llm_provider
|
|
553
|
+
|
|
554
|
+
if target_model and "/" in target_model and not target_provider:
|
|
555
|
+
parts = target_model.split("/", 1)
|
|
556
|
+
target_provider = parts[0]
|
|
557
|
+
target_model = parts[1]
|
|
558
|
+
|
|
559
|
+
# Determine LLM
|
|
560
|
+
agent_llm = self.llm
|
|
561
|
+
if target_provider:
|
|
562
|
+
try:
|
|
563
|
+
agent_llm = LLMFactory.create(target_provider, target_model)
|
|
564
|
+
except Exception as e:
|
|
565
|
+
self.logger.warning(f"Failed to create specific LLM for {name}: {e}. Using default.")
|
|
566
|
+
|
|
567
|
+
tools_list = tools or []
|
|
568
|
+
|
|
569
|
+
max_iter = self.config.get('max_iterations', 10)
|
|
570
|
+
|
|
571
|
+
if agent_type == "react":
|
|
572
|
+
return ReActAgent(name, system_prompt, tools=tools_list, framework=self, llm=agent_llm, max_iterations=max_iter, knowledge_sources=knowledge_sources, verbose=verbose)
|
|
573
|
+
elif agent_type == "plan_execute":
|
|
574
|
+
return PlanExecuteAgent(name, system_prompt, tools=tools_list, framework=self, llm=agent_llm, max_iterations=max_iter, verbose=verbose)
|
|
575
|
+
elif agent_type == "rewoo":
|
|
576
|
+
return ReWOOAgent(name, system_prompt, tools=tools_list, framework=self, llm=agent_llm, max_iterations=max_iter, verbose=verbose)
|
|
577
|
+
elif agent_type == "tot":
|
|
578
|
+
return TreeOfThoughtsAgent(name, system_prompt, tools=tools_list, framework=self, llm=agent_llm, max_iterations=max_iter, verbose=verbose)
|
|
579
|
+
elif agent_type == "reflective":
|
|
580
|
+
from .agents.reflective_agent import ReflectiveAgent
|
|
581
|
+
return ReflectiveAgent(name, system_prompt, tools=tools_list, framework=self, llm=agent_llm, verbose=verbose)
|
|
582
|
+
|
|
583
|
+
# Default to simple Agent
|
|
584
|
+
mode = "react" if agent_type == "react" else "simple"
|
|
585
|
+
return Agent(name, system_prompt, tools=tools_list, framework=self, llm=agent_llm, max_iterations=max_iter, knowledge_sources=knowledge_sources, verbose=verbose, agent_type=mode)
|
|
586
|
+
|
|
587
|
+
def create_react_agent(self,
|
|
588
|
+
name: str,
|
|
589
|
+
system_prompt: str,
|
|
590
|
+
tools: List = None,
|
|
591
|
+
kill_switch = None,
|
|
592
|
+
**kwargs):
|
|
593
|
+
"""
|
|
594
|
+
Create a ReActAgent for autonomous tasks.
|
|
595
|
+
"""
|
|
596
|
+
from .agents.react_agent import ReActAgent
|
|
597
|
+
from .safety.kill_switch import KillSwitch
|
|
598
|
+
|
|
599
|
+
# Use provided or default kill switch
|
|
600
|
+
ks = kill_switch or KillSwitch()
|
|
601
|
+
|
|
602
|
+
# We can reuse the same LLM/SLM logic from create_agent if needed,
|
|
603
|
+
# but for now let's keep it simple and use the base agent creation.
|
|
604
|
+
base_agent = self.create_agent(name, system_prompt, tools, **kwargs)
|
|
605
|
+
|
|
606
|
+
return ReActAgent(
|
|
607
|
+
name=base_agent.name,
|
|
608
|
+
system_prompt=base_agent.system_prompt,
|
|
609
|
+
llm=base_agent.llm,
|
|
610
|
+
tools=list(base_agent.tools.values()),
|
|
611
|
+
framework=self,
|
|
612
|
+
kill_switch=ks
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
def create_plan_execute_agent(self,
|
|
616
|
+
name: str = "PlanExecuteAgent",
|
|
617
|
+
system_prompt: str = "You are a master planner. Decompose the following goal into a sequence of actionable steps.",
|
|
618
|
+
tools: List = None,
|
|
619
|
+
max_iterations: int = 10,
|
|
620
|
+
**kwargs):
|
|
621
|
+
"""Create a Plan-and-Execute agent."""
|
|
622
|
+
from .agents.plan_execute import PlanExecuteAgent
|
|
623
|
+
base = self.create_agent(name, system_prompt, tools, **kwargs)
|
|
624
|
+
return PlanExecuteAgent(
|
|
625
|
+
name=base.name,
|
|
626
|
+
system_prompt=base.system_prompt,
|
|
627
|
+
llm=base.llm,
|
|
628
|
+
tools=list(base.tools.values()),
|
|
629
|
+
framework=self,
|
|
630
|
+
max_iterations=max_iterations
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
def create_rewoo_agent(self,
|
|
634
|
+
name: str = "ReWOOAgent",
|
|
635
|
+
system_prompt: str = "You are a ReWOO planner. Create a plan to achieve the goal using available tools.",
|
|
636
|
+
tools: List = None,
|
|
637
|
+
max_iterations: int = 10,
|
|
638
|
+
**kwargs):
|
|
639
|
+
"""Create a ReWOO (Reasoning WithOut Observation) agent."""
|
|
640
|
+
from .agents.rewoo import ReWOOAgent
|
|
641
|
+
base = self.create_agent(name, system_prompt, tools, **kwargs)
|
|
642
|
+
return ReWOOAgent(
|
|
643
|
+
name=base.name,
|
|
644
|
+
system_prompt=base.system_prompt,
|
|
645
|
+
llm=base.llm,
|
|
646
|
+
tools=list(base.tools.values()),
|
|
647
|
+
framework=self,
|
|
648
|
+
max_iterations=max_iterations
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
def create_tot_agent(self,
|
|
652
|
+
name: str = "TreeOfThoughtsAgent",
|
|
653
|
+
system_prompt: str = "You are a deliberate thinker. Generate distinct, creative next steps or reasoning thoughts to achieve the goal.",
|
|
654
|
+
max_iterations: int = 3,
|
|
655
|
+
**kwargs):
|
|
656
|
+
"""Create a Tree-of-Thoughts agent."""
|
|
657
|
+
from .agents.tot import TreeOfThoughtsAgent
|
|
658
|
+
# ToT doesn't necessarily need tools in the same way, but we'll allow them
|
|
659
|
+
base = self.create_agent(name, system_prompt, **kwargs)
|
|
660
|
+
return TreeOfThoughtsAgent(
|
|
661
|
+
name=base.name,
|
|
662
|
+
system_prompt=base.system_prompt,
|
|
663
|
+
llm=base.llm,
|
|
664
|
+
tools=list(base.tools.values()),
|
|
665
|
+
framework=self,
|
|
666
|
+
max_iterations=max_iterations
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
def create_planning_agent(self,
|
|
670
|
+
strategy: str = "plan-and-execute",
|
|
671
|
+
name: Optional[str] = None,
|
|
672
|
+
system_prompt: Optional[str] = None,
|
|
673
|
+
tools: List = None,
|
|
674
|
+
max_iterations: int = 10,
|
|
675
|
+
**kwargs):
|
|
676
|
+
"""
|
|
677
|
+
Unified factory for planning agents.
|
|
678
|
+
|
|
679
|
+
Strategies:
|
|
680
|
+
- "plan-and-execute": Step-by-step planning.
|
|
681
|
+
- "rewoo": Reasoning WithOut Observation (Parallel).
|
|
682
|
+
- "tot": Tree-of-Thoughts (Multi-path reasoning).
|
|
683
|
+
"""
|
|
684
|
+
strategy = strategy.lower()
|
|
685
|
+
|
|
686
|
+
# Set default name/prompt if not provided
|
|
687
|
+
if not name:
|
|
688
|
+
name = f"{strategy.replace('-', ' ').title().replace(' ', '')}Agent"
|
|
689
|
+
if not system_prompt:
|
|
690
|
+
if strategy == "plan-and-execute":
|
|
691
|
+
system_prompt = "You are a master planner. Decompose the following goal into a sequence of actionable steps."
|
|
692
|
+
elif strategy == "rewoo":
|
|
693
|
+
system_prompt = "You are a ReWOO planner. Create a plan to achieve the goal using available tools."
|
|
694
|
+
elif strategy == "tot":
|
|
695
|
+
system_prompt = "You are a deliberate thinker. Generate distinct, creative next steps or reasoning thoughts to achieve the goal."
|
|
696
|
+
|
|
697
|
+
if strategy == "plan-and-execute":
|
|
698
|
+
return self.create_plan_execute_agent(name, system_prompt, tools, max_iterations=max_iterations, **kwargs)
|
|
699
|
+
elif strategy == "rewoo":
|
|
700
|
+
return self.create_rewoo_agent(name, system_prompt, tools, max_iterations=max_iterations, **kwargs)
|
|
701
|
+
elif strategy == "tot":
|
|
702
|
+
return self.create_tot_agent(name, system_prompt, max_iterations=max_iterations, **kwargs)
|
|
703
|
+
else:
|
|
704
|
+
raise ValueError(f"Unknown strategy: {strategy}")
|
|
705
|
+
|
|
706
|
+
def create_reflective_agent(self,
|
|
707
|
+
name: str,
|
|
708
|
+
system_prompt: str,
|
|
709
|
+
critic_prompt: str = None,
|
|
710
|
+
max_reflections: int = 2,
|
|
711
|
+
tools: List = None,
|
|
712
|
+
**kwargs):
|
|
713
|
+
"""
|
|
714
|
+
Create a Self-Reflecting Agent that critiques and improves its own output.
|
|
715
|
+
"""
|
|
716
|
+
from .agents.reflective_agent import ReflectiveAgent
|
|
717
|
+
|
|
718
|
+
# Base agent creation logic to get LLM
|
|
719
|
+
base = self.create_agent(name, system_prompt, tools, **kwargs)
|
|
720
|
+
|
|
721
|
+
return ReflectiveAgent(
|
|
722
|
+
name=base.name,
|
|
723
|
+
system_prompt=base.system_prompt,
|
|
724
|
+
llm=base.llm,
|
|
725
|
+
tools=list(base.tools.values()),
|
|
726
|
+
framework=self,
|
|
727
|
+
critic_prompt=critic_prompt,
|
|
728
|
+
max_reflections=max_reflections
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
def create_conversation(self,
|
|
732
|
+
agents: List["Agent"],
|
|
733
|
+
max_turns: int = 10,
|
|
734
|
+
min_turns: int = 3,
|
|
735
|
+
termination_condition: str = "consensus"):
|
|
736
|
+
"""
|
|
737
|
+
Create a multi-agent conversation manager.
|
|
738
|
+
"""
|
|
739
|
+
from .conversation import ConversationManager
|
|
740
|
+
return ConversationManager(
|
|
741
|
+
agents=agents,
|
|
742
|
+
framework=self,
|
|
743
|
+
max_turns=max_turns,
|
|
744
|
+
min_turns=min_turns,
|
|
745
|
+
termination_condition=termination_condition
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
async def process(self, tasks: List[Dict]) -> List[Dict]:
|
|
749
|
+
"""
|
|
750
|
+
Run multiple agent tasks in parallel.
|
|
751
|
+
|
|
752
|
+
Args:
|
|
753
|
+
tasks: List of dicts, each containing:
|
|
754
|
+
- agent: The agent instance or name
|
|
755
|
+
- input: The user input
|
|
756
|
+
- context: Optional context
|
|
757
|
+
|
|
758
|
+
Returns:
|
|
759
|
+
List of results.
|
|
760
|
+
"""
|
|
761
|
+
import asyncio
|
|
762
|
+
|
|
763
|
+
async_tasks = []
|
|
764
|
+
for task in tasks:
|
|
765
|
+
agent = task['agent']
|
|
766
|
+
async_tasks.append(agent.run(task['input'], task.get('context')))
|
|
767
|
+
|
|
768
|
+
return await asyncio.gather(*async_tasks)
|
|
769
|
+
|
|
770
|
+
def process_sync(self, tasks: List[Dict]) -> List[Dict]:
|
|
771
|
+
"""
|
|
772
|
+
Synchronous wrapper for process.
|
|
773
|
+
Runs multiple agent tasks in parallel without requiring async/await in the caller.
|
|
774
|
+
"""
|
|
775
|
+
import asyncio
|
|
776
|
+
try:
|
|
777
|
+
loop = asyncio.get_event_loop()
|
|
778
|
+
except RuntimeError:
|
|
779
|
+
loop = asyncio.new_event_loop()
|
|
780
|
+
asyncio.set_event_loop(loop)
|
|
781
|
+
|
|
782
|
+
if loop.is_running():
|
|
783
|
+
import nest_asyncio
|
|
784
|
+
nest_asyncio.apply()
|
|
785
|
+
return loop.run_until_complete(self.process(tasks))
|
|
786
|
+
else:
|
|
787
|
+
return asyncio.run(self.process(tasks))
|
|
788
|
+
|
|
789
|
+
def get_metrics(self) -> Dict:
|
|
790
|
+
base_metrics = {
|
|
791
|
+
'circuit_breaker': getattr(self.circuit_breaker, 'get_stats', lambda: {})(),
|
|
792
|
+
'idempotency': getattr(self.idempotency, 'get_metrics', lambda: {})(),
|
|
793
|
+
'vector_memory': getattr(self.vector_memory, 'get_metrics', lambda: {})(),
|
|
794
|
+
'session_memory': getattr(self.session_memory, 'get_metrics', lambda: {})(),
|
|
795
|
+
}
|
|
796
|
+
# Merge with global metrics collector data
|
|
797
|
+
base_metrics.update(self.metrics.get_metrics())
|
|
798
|
+
return base_metrics
|
|
799
|
+
|
|
800
|
+
def print_summary(self):
|
|
801
|
+
"""Print the detailed framework summary report."""
|
|
802
|
+
print(self.metrics.get_detailed_report())
|
|
803
|
+
|
|
804
|
+
def enable_verbose_monitoring(self):
|
|
805
|
+
"""Enable standardized console feedback for all events."""
|
|
806
|
+
def default_on_event(event, data):
|
|
807
|
+
agent = data.get('agent') or data.get('pipeline') or 'System'
|
|
808
|
+
|
|
809
|
+
if "thought" in event:
|
|
810
|
+
print(f" [{agent}] Thinking: {data.get('reasoning', '')[:100]}...")
|
|
811
|
+
elif "action" in event:
|
|
812
|
+
print(f" [{agent}] Action: {data.get('tool')}({data.get('args', {})})")
|
|
813
|
+
elif "observation" in event:
|
|
814
|
+
obs = str(data.get('observation', ''))
|
|
815
|
+
print(f" [{agent}] Observation: {obs[:500]}..." if len(obs) > 500 else f" [{agent}] Observation: {obs}")
|
|
816
|
+
elif "tool:log" in event:
|
|
817
|
+
print(f" [{agent}] Tool Debug: {data.get('message', '')}")
|
|
818
|
+
elif "complete" in event:
|
|
819
|
+
print(f" [{agent}] Task Completed.")
|
|
820
|
+
elif "error" in event:
|
|
821
|
+
print(f" [{agent}] ERROR: {data.get('error')}")
|
|
822
|
+
elif "pipeline:step_start" in event:
|
|
823
|
+
print(f" [Pipeline] Step '{data.get('step')}' started.")
|
|
824
|
+
elif "pipeline:step_finish" in event or "pipeline:step_complete" in event:
|
|
825
|
+
print(f" [Pipeline] Step '{data.get('step')}' finished.")
|
|
826
|
+
elif "supervisor" in event:
|
|
827
|
+
print(f" [Supervisor] {data.get('message')}")
|
|
828
|
+
|
|
829
|
+
def tool(self, func: Optional[Callable] = None, *, name: str = None, description: str = None):
|
|
830
|
+
"""
|
|
831
|
+
Decorator to register a tool.
|
|
832
|
+
|
|
833
|
+
Usage:
|
|
834
|
+
@app.tool
|
|
835
|
+
def my_tool(arg: str): ...
|
|
836
|
+
|
|
837
|
+
@app.tool(description="Custom desc")
|
|
838
|
+
def my_tool(arg: str): ...
|
|
839
|
+
"""
|
|
840
|
+
def decorator(f):
|
|
841
|
+
tool_name = name or f.__name__
|
|
842
|
+
tool_desc = description or f.__doc__ or "No description"
|
|
843
|
+
self.create_tool(tool_name, f, tool_desc)
|
|
844
|
+
return f
|
|
845
|
+
|
|
846
|
+
if func is None:
|
|
847
|
+
return decorator
|
|
848
|
+
return decorator(func)
|
|
849
|
+
|
|
850
|
+
def agent(self,
|
|
851
|
+
func: Optional[Callable] = None,
|
|
852
|
+
*,
|
|
853
|
+
model: str = None,
|
|
854
|
+
provider: str = None,
|
|
855
|
+
tools: List = None,
|
|
856
|
+
routes: List[str] = None,
|
|
857
|
+
system_prompt: str = None):
|
|
858
|
+
"""
|
|
859
|
+
Decorator to register an agent and optionally route to it.
|
|
860
|
+
|
|
861
|
+
Usage:
|
|
862
|
+
@app.agent(routes=["How do I..."])
|
|
863
|
+
def support_agent(context):
|
|
864
|
+
return "System Prompt..."
|
|
865
|
+
"""
|
|
866
|
+
def decorator(f):
|
|
867
|
+
# 1. Determine System Prompt
|
|
868
|
+
nonlocal system_prompt
|
|
869
|
+
if not system_prompt:
|
|
870
|
+
try:
|
|
871
|
+
# Try calling with empty context to get dynamic prompt
|
|
872
|
+
res = f({})
|
|
873
|
+
if isinstance(res, str) and res:
|
|
874
|
+
system_prompt = res
|
|
875
|
+
except Exception:
|
|
876
|
+
pass
|
|
877
|
+
|
|
878
|
+
# Fallback to docstring if dynamic prompt failed or returned None
|
|
879
|
+
if not system_prompt:
|
|
880
|
+
system_prompt = f.__doc__ or "You are a helpful assistant."
|
|
881
|
+
|
|
882
|
+
agent_name = f.__name__
|
|
883
|
+
|
|
884
|
+
# 1.5 Parse Model/Provider from string if detected
|
|
885
|
+
# e.g. model="groq/llama3" -> provider="groq", model="llama3"
|
|
886
|
+
nonlocal provider, model
|
|
887
|
+
if model and "/" in model and not provider:
|
|
888
|
+
parts = model.split("/", 1)
|
|
889
|
+
provider = parts[0]
|
|
890
|
+
model = parts[1]
|
|
891
|
+
|
|
892
|
+
# 2. Get Tools (resolve functions to registered tools)
|
|
893
|
+
resolved_tools = []
|
|
894
|
+
if tools:
|
|
895
|
+
for t in tools:
|
|
896
|
+
if hasattr(t, '__name__'):
|
|
897
|
+
# Find the registered tool by name
|
|
898
|
+
found = self.tools.get(t.__name__)
|
|
899
|
+
if found: resolved_tools.append(found)
|
|
900
|
+
|
|
901
|
+
# 3. Create Agent
|
|
902
|
+
new_agent = self.create_agent(
|
|
903
|
+
name=agent_name,
|
|
904
|
+
system_prompt=system_prompt,
|
|
905
|
+
tools=resolved_tools,
|
|
906
|
+
llm_provider=provider,
|
|
907
|
+
llm_model=model
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
# 4. Register Routes
|
|
911
|
+
if routes and self.semantic_router:
|
|
912
|
+
for route_query in routes:
|
|
913
|
+
self.semantic_router.add_route(
|
|
914
|
+
name=agent_name,
|
|
915
|
+
samples=[route_query],
|
|
916
|
+
handler=lambda q, c=None: new_agent.run(q, context=c)
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
return new_agent
|
|
920
|
+
|
|
921
|
+
if func is None:
|
|
922
|
+
return decorator
|
|
923
|
+
return decorator(func)
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
class MasterAgent:
|
|
927
|
+
"""High-level supervisor for goal-driven autonomous operations."""
|
|
928
|
+
def __init__(self, name: str, framework):
|
|
929
|
+
self.name = name
|
|
930
|
+
self.framework = framework
|
|
931
|
+
self.state = {"goal_reached": False, "count": 0, "results": []}
|
|
932
|
+
|
|
933
|
+
async def run_until(self, goal_description: str, check_fn: Callable, max_iterations: int = 5):
|
|
934
|
+
"""
|
|
935
|
+
Execute a goal-driven loop.
|
|
936
|
+
The caller (orchestrator script) should yield the results back to update state.
|
|
937
|
+
"""
|
|
938
|
+
self.framework.event_bus.emit("supervisor:goal_set", {
|
|
939
|
+
"master": self.name,
|
|
940
|
+
"goal": goal_description,
|
|
941
|
+
"message": f"Global Mission Started: {goal_description}"
|
|
942
|
+
})
|
|
943
|
+
|
|
944
|
+
for i in range(max_iterations):
|
|
945
|
+
self.framework.event_bus.emit("supervisor:iteration_start", {
|
|
946
|
+
"master": self.name,
|
|
947
|
+
"iteration": i + 1,
|
|
948
|
+
"message": f"Planning Iteration {i+1} to reach goal..."
|
|
949
|
+
})
|
|
950
|
+
|
|
951
|
+
# The implementation script will handle the actual agent calls
|
|
952
|
+
# and update supervisor.state["count"] and supervisor.state["results"]
|
|
953
|
+
yield i + 1
|
|
954
|
+
|
|
955
|
+
# Check if goal is met
|
|
956
|
+
if check_fn(self.state):
|
|
957
|
+
self.framework.event_bus.emit("supervisor:goal_reached", {
|
|
958
|
+
"master": self.name,
|
|
959
|
+
"count": self.state["count"],
|
|
960
|
+
"message": f"MISSION SUCCESS: Goal reached in {i+1} iterations!"
|
|
961
|
+
})
|
|
962
|
+
return
|
|
963
|
+
|
|
964
|
+
self.framework.event_bus.emit("supervisor:goal_check", {
|
|
965
|
+
"master": self.name,
|
|
966
|
+
"current": self.state["count"],
|
|
967
|
+
"status": "Goal incomplete. Continuing...",
|
|
968
|
+
"message": f"Currently at {self.state['count']} items. Need more."
|
|
969
|
+
})
|
|
970
|
+
|
|
971
|
+
self.framework.event_bus.emit("supervisor:max_iterations", {
|
|
972
|
+
"master": self.name,
|
|
973
|
+
"error": "Failed to meet goal within iteration limit."
|
|
974
|
+
})
|