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.
Files changed (61) hide show
  1. kite/__init__.py +46 -0
  2. kite/ab_testing.py +384 -0
  3. kite/agent.py +556 -0
  4. kite/agents/__init__.py +3 -0
  5. kite/agents/plan_execute.py +191 -0
  6. kite/agents/react_agent.py +509 -0
  7. kite/agents/reflective_agent.py +90 -0
  8. kite/agents/rewoo.py +119 -0
  9. kite/agents/tot.py +151 -0
  10. kite/conversation.py +125 -0
  11. kite/core.py +974 -0
  12. kite/data_loaders.py +111 -0
  13. kite/embedding_providers.py +372 -0
  14. kite/llm_providers.py +1278 -0
  15. kite/memory/__init__.py +6 -0
  16. kite/memory/advanced_rag.py +333 -0
  17. kite/memory/graph_rag.py +719 -0
  18. kite/memory/session_memory.py +423 -0
  19. kite/memory/vector_memory.py +579 -0
  20. kite/monitoring.py +611 -0
  21. kite/observers.py +107 -0
  22. kite/optimization/__init__.py +9 -0
  23. kite/optimization/resource_router.py +80 -0
  24. kite/persistence.py +42 -0
  25. kite/pipeline/__init__.py +5 -0
  26. kite/pipeline/deterministic_pipeline.py +323 -0
  27. kite/pipeline/reactive_pipeline.py +171 -0
  28. kite/pipeline_manager.py +15 -0
  29. kite/routing/__init__.py +6 -0
  30. kite/routing/aggregator_router.py +325 -0
  31. kite/routing/llm_router.py +149 -0
  32. kite/routing/semantic_router.py +228 -0
  33. kite/safety/__init__.py +6 -0
  34. kite/safety/circuit_breaker.py +360 -0
  35. kite/safety/guardrails.py +82 -0
  36. kite/safety/idempotency_manager.py +304 -0
  37. kite/safety/kill_switch.py +75 -0
  38. kite/tool.py +183 -0
  39. kite/tool_registry.py +87 -0
  40. kite/tools/__init__.py +21 -0
  41. kite/tools/code_execution.py +53 -0
  42. kite/tools/contrib/__init__.py +19 -0
  43. kite/tools/contrib/calculator.py +26 -0
  44. kite/tools/contrib/datetime_utils.py +20 -0
  45. kite/tools/contrib/linkedin.py +428 -0
  46. kite/tools/contrib/web_search.py +30 -0
  47. kite/tools/mcp/__init__.py +31 -0
  48. kite/tools/mcp/database_mcp.py +267 -0
  49. kite/tools/mcp/gdrive_mcp_server.py +503 -0
  50. kite/tools/mcp/gmail_mcp_server.py +601 -0
  51. kite/tools/mcp/postgres_mcp_server.py +490 -0
  52. kite/tools/mcp/slack_mcp_server.py +538 -0
  53. kite/tools/mcp/stripe_mcp_server.py +219 -0
  54. kite/tools/search.py +90 -0
  55. kite/tools/system_tools.py +54 -0
  56. kite/tools_manager.py +27 -0
  57. kite_agent-0.1.0.dist-info/METADATA +621 -0
  58. kite_agent-0.1.0.dist-info/RECORD +61 -0
  59. kite_agent-0.1.0.dist-info/WHEEL +5 -0
  60. kite_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
  61. 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
+ })