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
|
@@ -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
|
+
|