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
@@ -0,0 +1,304 @@
1
+ """
2
+ Idempotency Manager for AI Agent Operations
3
+
4
+ Ensures that operations are executed exactly once, even if called multiple times.
5
+ Critical for preventing duplicate actions like refunds, emails, database writes.
6
+
7
+ Author: Agentic AI Systems
8
+ License: MIT
9
+ """
10
+
11
+ import hashlib
12
+ import json
13
+ import time
14
+ from typing import Dict, Any, Optional
15
+ from dataclasses import dataclass
16
+ from datetime import datetime, timedelta
17
+ import logging
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @dataclass
23
+ class IdempotencyConfig:
24
+ """Configuration for idempotency behavior."""
25
+ ttl_seconds: int = 3600 # How long to remember operations (1 hour default)
26
+ storage_backend: str = "memory" # Options: memory, redis, database
27
+
28
+
29
+ class IdempotencyManager:
30
+ """
31
+ Manages idempotency for agent operations.
32
+
33
+ Use this to ensure operations like refunds, emails, or database
34
+ writes only execute once, even if the agent retries.
35
+
36
+ Example:
37
+ manager = IdempotencyManager()
38
+
39
+ # Generate idempotency key
40
+ key = manager.generate_id(
41
+ operation="refund",
42
+ params={"order_id": "123", "amount": 99.99}
43
+ )
44
+
45
+ # Check if already executed
46
+ if manager.is_duplicate(key):
47
+ return manager.get_result(key)
48
+
49
+ # Execute operation
50
+ result = process_refund(...)
51
+ manager.store_result(key, result)
52
+ """
53
+
54
+ def __init__(self, config: Optional[IdempotencyConfig] = None):
55
+ self.config = config or IdempotencyConfig()
56
+ self._storage: Dict[str, Dict[str, Any]] = {}
57
+ logger.info(f"Idempotency manager initialized: {self.config}")
58
+
59
+ def generate_id(self, operation: str, params: Dict[str, Any]) -> str:
60
+ """
61
+ Generate deterministic idempotency key.
62
+
63
+ The same operation + params will always produce the same key.
64
+ This is critical for detecting duplicates.
65
+
66
+ Args:
67
+ operation: Name of the operation (e.g., "refund", "send_email")
68
+ params: Parameters that uniquely identify this operation
69
+
70
+ Returns:
71
+ Deterministic hash string
72
+ """
73
+ # Sort params for deterministic hashing
74
+ sorted_params = json.dumps(params, sort_keys=True)
75
+ unique_string = f"{operation}:{sorted_params}"
76
+
77
+ # Generate hash
78
+ return hashlib.sha256(unique_string.encode()).hexdigest()
79
+
80
+ def is_duplicate(self, idempotency_key: str) -> bool:
81
+ """
82
+ Check if this operation has already been executed.
83
+
84
+ Args:
85
+ idempotency_key: The idempotency key from generate_id()
86
+
87
+ Returns:
88
+ True if operation already executed, False otherwise
89
+ """
90
+ if idempotency_key not in self._storage:
91
+ return False
92
+
93
+ # Check if TTL expired
94
+ entry = self._storage[idempotency_key]
95
+ expiry = entry["expires_at"]
96
+
97
+ if datetime.now() > expiry:
98
+ # Expired, remove and return False
99
+ del self._storage[idempotency_key]
100
+ logger.info(f"Idempotency key expired and removed: {idempotency_key}")
101
+ return False
102
+
103
+ logger.warning(f"Duplicate operation detected: {idempotency_key}")
104
+ return True
105
+
106
+ def get_result(self, idempotency_key: str) -> Optional[Any]:
107
+ """
108
+ Get the cached result of a previous execution.
109
+
110
+ Args:
111
+ idempotency_key: The idempotency key
112
+
113
+ Returns:
114
+ Cached result or None if not found
115
+ """
116
+ if idempotency_key not in self._storage:
117
+ return None
118
+
119
+ entry = self._storage[idempotency_key]
120
+
121
+ # Check expiry
122
+ if datetime.now() > entry["expires_at"]:
123
+ del self._storage[idempotency_key]
124
+ return None
125
+
126
+ logger.info(f"Returning cached result for key: {idempotency_key}")
127
+ return entry["result"]
128
+
129
+ def store_result(
130
+ self,
131
+ idempotency_key: str,
132
+ result: Any,
133
+ ttl_seconds: Optional[int] = None
134
+ ):
135
+ """
136
+ Store the result of an operation.
137
+
138
+ Args:
139
+ idempotency_key: The idempotency key
140
+ result: The result to cache
141
+ ttl_seconds: Override default TTL
142
+ """
143
+ ttl = ttl_seconds or self.config.ttl_seconds
144
+ expires_at = datetime.now() + timedelta(seconds=ttl)
145
+
146
+ self._storage[idempotency_key] = {
147
+ "result": result,
148
+ "created_at": datetime.now(),
149
+ "expires_at": expires_at
150
+ }
151
+
152
+ logger.info(
153
+ f"Stored result for key: {idempotency_key}, "
154
+ f"expires: {expires_at.isoformat()}"
155
+ )
156
+
157
+ def clear_expired(self):
158
+ """Remove all expired entries (cleanup)."""
159
+ now = datetime.now()
160
+ expired_keys = [
161
+ key for key, entry in self._storage.items()
162
+ if entry["expires_at"] < now
163
+ ]
164
+
165
+ for key in expired_keys:
166
+ del self._storage[key]
167
+
168
+ if expired_keys:
169
+ logger.info(f"Cleared {len(expired_keys)} expired entries")
170
+
171
+ def clear_all(self):
172
+ """Clear all cached results."""
173
+ self._storage.clear()
174
+ logger.info("All idempotency cache cleared")
175
+
176
+ def get_stats(self) -> Dict[str, Any]:
177
+ """Get statistics about cached operations."""
178
+ now = datetime.now()
179
+ active = sum(
180
+ 1 for entry in self._storage.values()
181
+ if entry["expires_at"] > now
182
+ )
183
+
184
+ return {
185
+ "total_cached": len(self._storage),
186
+ "active": active,
187
+ "expired": len(self._storage) - active
188
+ }
189
+
190
+
191
+ # Decorator for easy use
192
+ def idempotent(manager: IdempotencyManager, operation_name: str):
193
+ """
194
+ Decorator to make a function idempotent.
195
+
196
+ Example:
197
+ manager = IdempotencyManager()
198
+
199
+ @idempotent(manager, "process_refund")
200
+ def process_refund(order_id: str, amount: float):
201
+ # This will only execute once for the same order_id + amount
202
+ return stripe.Refund.create(...)
203
+ """
204
+ def decorator(func):
205
+ def wrapper(*args, **kwargs):
206
+ # Generate idempotency key from function args
207
+ params = {
208
+ "args": str(args),
209
+ "kwargs": str(kwargs)
210
+ }
211
+ key = manager.generate_id(operation_name, params)
212
+
213
+ # Check if already executed
214
+ if manager.is_duplicate(key):
215
+ logger.info(f"Returning cached result for {operation_name}")
216
+ return manager.get_result(key)
217
+
218
+ # Execute function
219
+ result = func(*args, **kwargs)
220
+
221
+ # Cache result
222
+ manager.store_result(key, result)
223
+
224
+ return result
225
+
226
+ return wrapper
227
+ return decorator
228
+
229
+
230
+ if __name__ == "__main__":
231
+ # Example usage
232
+ logging.basicConfig(level=logging.INFO)
233
+
234
+ print("=== Idempotency Manager Demo ===\n")
235
+
236
+ manager = IdempotencyManager(
237
+ config=IdempotencyConfig(ttl_seconds=60)
238
+ )
239
+
240
+ # Simulate a refund operation
241
+ def process_refund(order_id: str, amount: float):
242
+ """Simulate processing a refund."""
243
+ print(f" Processing refund: ${amount} for order {order_id}")
244
+ time.sleep(0.5) # Simulate API call
245
+ return {
246
+ "success": True,
247
+ "refund_id": f"ref_{int(time.time())}",
248
+ "amount": amount
249
+ }
250
+
251
+ # Test idempotency
252
+ print("1. First refund request:")
253
+ key1 = manager.generate_id(
254
+ "refund",
255
+ {"order_id": "12345", "amount": 299.99}
256
+ )
257
+
258
+ if not manager.is_duplicate(key1):
259
+ result1 = process_refund("12345", 299.99)
260
+ manager.store_result(key1, result1)
261
+ print(f" [OK] Result: {result1}\n")
262
+
263
+ print("2. Duplicate refund request (should use cache):")
264
+ key2 = manager.generate_id(
265
+ "refund",
266
+ {"order_id": "12345", "amount": 299.99}
267
+ )
268
+
269
+ if manager.is_duplicate(key2):
270
+ cached = manager.get_result(key2)
271
+ print(f" Cached result: {cached}\n")
272
+
273
+ print("3. Different refund (should execute):")
274
+ key3 = manager.generate_id(
275
+ "refund",
276
+ {"order_id": "67890", "amount": 199.99}
277
+ )
278
+
279
+ if not manager.is_duplicate(key3):
280
+ result3 = process_refund("67890", 199.99)
281
+ manager.store_result(key3, result3)
282
+ print(f" [OK] Result: {result3}\n")
283
+
284
+ # Stats
285
+ print("Statistics:")
286
+ print(f" {manager.get_stats()}\n")
287
+
288
+ # Decorator example
289
+ print("\n=== Decorator Example ===\n")
290
+
291
+ @idempotent(manager, "send_email")
292
+ def send_email(to: str, subject: str):
293
+ print(f" Sending email to {to}: {subject}")
294
+ return {"sent": True, "message_id": f"msg_{int(time.time())}"}
295
+
296
+ # First call - executes
297
+ print("1. First email:")
298
+ result = send_email("user@example.com", "Welcome!")
299
+ print(f" Result: {result}\n")
300
+
301
+ # Duplicate call - cached
302
+ print("2. Duplicate email (cached):")
303
+ result = send_email("user@example.com", "Welcome!")
304
+ print(f" Result: {result}\n")
@@ -0,0 +1,75 @@
1
+ """
2
+ KillSwitch - Safety limits for autonomous agents.
3
+ """
4
+
5
+ import time
6
+ from typing import Dict, Tuple, Optional, Any
7
+
8
+
9
+ class KillSwitch:
10
+ """
11
+ Safety limits for autonomous agents to prevent infinite loops,
12
+ excessive costs, or hanging processes.
13
+ """
14
+
15
+ def __init__(self,
16
+ max_iterations: int = 10,
17
+ max_cost: float = 1.0,
18
+ max_same_action: int = 2,
19
+ max_time: int = 300):
20
+ """
21
+ Initialize KillSwitch with safety limits.
22
+
23
+ Args:
24
+ max_iterations: Maximum number of steps/loops allowed.
25
+ max_cost: Maximum total cost in USD allowed.
26
+ max_same_action: Maximum number of times the same action can be repeated consecutively.
27
+ max_time: Maximum time in seconds allowed.
28
+ """
29
+ self.max_iterations = max_iterations
30
+ self.max_cost_usd = max_cost
31
+ self.max_same_action = max_same_action
32
+ self.max_time_seconds = max_time
33
+
34
+ def check(self, state: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
35
+ """
36
+ Check all kill switch limits against the current agent state.
37
+
38
+ Args:
39
+ state: Dictionary containing current agent state:
40
+ - steps (int): Current iteration count
41
+ - total_cost (float): Accumulated cost
42
+ - start_time (float): Time when the process started (time.time())
43
+ - actions (list): List of actions taken
44
+ - completed (bool): Whether the goal is achieved
45
+
46
+ Returns:
47
+ Tuple (should_stop, reason)
48
+ """
49
+
50
+ # Limit 1: Iteration cap
51
+ if state.get('steps', 0) >= self.max_iterations:
52
+ return True, f"Max iterations ({self.max_iterations}) reached."
53
+
54
+ # Limit 2: Budget cap
55
+ if state.get('total_cost', 0.0) >= self.max_cost_usd:
56
+ return True, f"Budget exceeded (${self.max_cost_usd})."
57
+
58
+ # Limit 3: Time limit
59
+ if 'start_time' in state:
60
+ elapsed = time.time() - state['start_time']
61
+ if elapsed >= self.max_time_seconds:
62
+ return True, f"Time limit ({self.max_time_seconds}s) exceeded."
63
+
64
+ # Limit 4: Stupidity check (repeated actions)
65
+ actions = state.get('actions', [])
66
+ if len(actions) >= self.max_same_action:
67
+ recent = [a.get('type') for a in actions[-self.max_same_action:]]
68
+ if len(set(recent)) == 1 and recent[0] is not None:
69
+ return True, f"Stuck in loop (same action '{recent[0]}' repeated {self.max_same_action} times)."
70
+
71
+ # Limit 5: Goal completed
72
+ if state.get('completed', False):
73
+ return True, "Goal achieved."
74
+
75
+ return False, None
kite/tool.py ADDED
@@ -0,0 +1,183 @@
1
+ """
2
+ General-Purpose Tool
3
+ Wrap any function as a tool for agents.
4
+ """
5
+
6
+ from typing import Callable, Dict, Any
7
+ import asyncio
8
+
9
+
10
+ class Tool:
11
+ """
12
+ General-purpose tool wrapper.
13
+
14
+ Example:
15
+ def search_database(query: str):
16
+ return db.execute(query)
17
+
18
+ tool = ai.create_tool(
19
+ name="search_database",
20
+ func=search_database,
21
+ description="Search database with SQL query"
22
+ )
23
+ """
24
+
25
+ def __init__(self,
26
+ name: str,
27
+ func: Callable,
28
+ description: str):
29
+ self.name = name
30
+ self.func = func
31
+ self.description = description
32
+
33
+ # Stats
34
+ self.call_count = 0
35
+ self.error_count = 0
36
+
37
+ def to_schema(self) -> Dict:
38
+ """
39
+ Generate JSON Schema for the tool (OpenAI/Ollama compatible).
40
+ """
41
+ import inspect
42
+ sig = inspect.signature(self.func)
43
+ doc = inspect.getdoc(self.func) or self.description
44
+
45
+ properties = {}
46
+ required = []
47
+
48
+ for name, param in sig.parameters.items():
49
+ if name == "self" or name == "framework":
50
+ continue
51
+
52
+ # Skip *args and **kwargs
53
+ if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
54
+ continue
55
+
56
+ # Map Python types to JSON types
57
+ param_type = "string" # default
58
+ if param.annotation == int: param_type = "integer"
59
+ elif param.annotation == float: param_type = "number"
60
+ elif param.annotation == bool: param_type = "boolean"
61
+ elif param.annotation == dict: param_type = "object"
62
+ elif param.annotation == list: param_type = "array"
63
+
64
+ # Extract description from docstring if possible (simple parsing)
65
+ # Todo: use a better parser later
66
+
67
+ properties[name] = {
68
+ "type": param_type,
69
+ "description": f"Parameter {name}"
70
+ }
71
+
72
+ if param.default == inspect.Parameter.empty:
73
+ required.append(name)
74
+
75
+ return {
76
+ "type": "function",
77
+ "function": {
78
+ "name": self.name,
79
+ "description": doc,
80
+ "parameters": {
81
+ "type": "object",
82
+ "properties": properties,
83
+ "required": required
84
+ }
85
+ }
86
+ }
87
+
88
+ async def execute(self, *args, **kwargs) -> Any:
89
+ """Execute tool."""
90
+ self.call_count += 1
91
+
92
+ try:
93
+ # General Monitoring: Emit tool start
94
+ if kwargs.get('framework'):
95
+ kwargs['framework'].event_bus.emit("tool:start", {
96
+ "tool": self.name,
97
+ "args": kwargs.get('args', args) # Best effort capture
98
+ })
99
+
100
+ # Pre-inject framework if required by signature
101
+ import inspect
102
+ sig = inspect.signature(self.func)
103
+
104
+ # Check if we should inject the framework
105
+ should_inject = False
106
+ if 'framework' in sig.parameters:
107
+ should_inject = True
108
+ elif any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()):
109
+ should_inject = True # Accepts **kwargs
110
+
111
+ if should_inject and kwargs.get('framework'):
112
+ # We need to make sure we don't double-provide if it's already in args/kwargs
113
+ # But since we control the call, we can just ensure it's in kwargs for binding
114
+ pass # It's already in kwargs passed to execute
115
+ elif 'framework' in kwargs and not should_inject:
116
+ # CRITICAL: Remove framework from kwargs if the function DOES NOT accept it
117
+ del kwargs['framework']
118
+
119
+ bound_args = sig.bind_partial(*args, **kwargs)
120
+
121
+ casted_args = {}
122
+ # (Rest of casting logic remains same)
123
+ for param_name, value in bound_args.arguments.items():
124
+ param = sig.parameters.get(param_name)
125
+ if param and param.annotation != inspect.Parameter.empty:
126
+ try:
127
+ annotation = param.annotation
128
+ if annotation == str: casted_args[param_name] = str(value)
129
+ elif annotation == float: casted_args[param_name] = float(str(value).replace('$', '').replace(',', '').strip())
130
+ elif annotation == int: casted_args[param_name] = int(str(value).replace('$', '').replace(',', '').split('.')[0].strip())
131
+ elif annotation == bool: casted_args[param_name] = str(value).lower() in ("true", "1", "yes", "on")
132
+ else: casted_args[param_name] = value
133
+ except: casted_args[param_name] = value
134
+ else:
135
+ casted_args[param_name] = value
136
+
137
+
138
+ if asyncio.iscoroutinefunction(self.func):
139
+ result = await self.func(**casted_args)
140
+ else:
141
+ # Regular sync function
142
+ result = await asyncio.to_thread(self.func, **casted_args)
143
+
144
+ # General Monitoring: Emit tool end
145
+ if kwargs.get('framework'):
146
+ kwargs['framework'].event_bus.emit("tool:end", {
147
+ "tool": self.name,
148
+ "status": "success"
149
+ })
150
+
151
+ return result
152
+ except Exception as e:
153
+ self.error_count += 1
154
+ raise
155
+ except Exception as e:
156
+ self.error_count += 1
157
+ raise
158
+
159
+ def get_definition(self) -> Dict:
160
+ """Get tool definition for LLM."""
161
+ import inspect
162
+ sig = inspect.signature(self.func)
163
+ params = {}
164
+ for name, param in sig.parameters.items():
165
+ params[name] = {
166
+ "type": str(param.annotation) if param.annotation != inspect.Parameter.empty else "any",
167
+ "default": str(param.default) if param.default != inspect.Parameter.empty else None,
168
+ "required": param.default == inspect.Parameter.empty
169
+ }
170
+
171
+ return {
172
+ "name": self.name,
173
+ "description": self.description,
174
+ "parameters": params
175
+ }
176
+
177
+ def get_metrics(self) -> Dict:
178
+ """Get tool metrics."""
179
+ return {
180
+ "name": self.name,
181
+ "calls": self.call_count,
182
+ "errors": self.error_count
183
+ }
kite/tool_registry.py ADDED
@@ -0,0 +1,87 @@
1
+ """
2
+ Tool Registry - Register and manage tools.
3
+ """
4
+
5
+ from typing import Dict
6
+ import logging
7
+
8
+
9
+ class ToolRegistry:
10
+ """
11
+ Registry for all tools.
12
+
13
+ Example:
14
+ ai.tools.register("my_tool", tool)
15
+ tool = ai.tools.get("my_tool")
16
+ all_tools = ai.tools.list()
17
+ """
18
+
19
+ def __init__(self, config, logger):
20
+ self.config = config
21
+ self.logger = logger
22
+ self._tools = {}
23
+
24
+ # Optionally initialize MCP servers
25
+ self._init_mcp_servers()
26
+
27
+ def load_standard_tools(self, framework):
28
+ """Automatically load and register all standard contrib tools."""
29
+ from .tool import Tool
30
+ from .tools.contrib import (
31
+ get_current_datetime,
32
+ )
33
+
34
+ # Try to import optional dependencies
35
+ try:
36
+ from .tools.web_search import web_search
37
+ has_web_search = True
38
+ except:
39
+ has_web_search = False
40
+
41
+ try:
42
+ from .tools.contrib.calculator import calculator
43
+ has_calculator = True
44
+ except:
45
+ has_calculator = False
46
+
47
+ # Basic tools that are always available
48
+ standard_tools = [
49
+ ("get_datetime", get_current_datetime, "Get current date and time"),
50
+ ]
51
+
52
+ # Add optional tools if available
53
+ if has_web_search:
54
+ standard_tools.append(("web_search", web_search, "Search the web for information"))
55
+ if has_calculator:
56
+ standard_tools.append(("calculator", calculator, "Evaluate mathematical expressions"))
57
+
58
+ for name, func, desc in standard_tools:
59
+ if name not in self._tools:
60
+ self.register(name, Tool(name, func, desc))
61
+
62
+ def _init_mcp_servers(self):
63
+ """Initialize MCP servers if credentials available."""
64
+ try:
65
+ from .tools_manager import MCPServers
66
+ self.mcp = MCPServers(self.config, self.logger)
67
+ self.logger.info(" [OK] Tools (MCP)")
68
+ except Exception as e:
69
+ self.logger.info(" Tools (manual registration)")
70
+ self.mcp = None
71
+
72
+ def register(self, name: str, tool):
73
+ """Register a tool."""
74
+ self._tools[name] = tool
75
+ self.logger.info(f" [OK] Registered tool: {name}")
76
+
77
+ def get(self, name: str):
78
+ """Get a tool by name."""
79
+ return self._tools.get(name)
80
+
81
+ def list(self):
82
+ """List all registered tools."""
83
+ return list(self._tools.keys())
84
+
85
+ def get_all(self):
86
+ """Get all tools."""
87
+ return self._tools
kite/tools/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ """
2
+ Kite Tools Module
3
+
4
+ Standard tools that agents can use directly:
5
+ - WebSearchTool: Web search using DuckDuckGo
6
+ - PythonReplTool: Safe Python code execution
7
+ - ShellTool: Shell command execution (with whitelisting)
8
+
9
+ MCP Servers are in the mcp/ subpackage.
10
+ """
11
+
12
+ from .search import WebSearchTool
13
+ from .code_execution import PythonReplTool
14
+ from .system_tools import ShellTool
15
+
16
+ __all__ = [
17
+ 'WebSearchTool',
18
+ 'PythonReplTool',
19
+ 'ShellTool'
20
+ ]
21
+