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,219 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stripe MCP Server Implementation
|
|
3
|
+
Based on Chapter 4: Model Context Protocol
|
|
4
|
+
|
|
5
|
+
Lessons from $15K Refund Loop (Chapter 1):
|
|
6
|
+
- Idempotency keys MANDATORY
|
|
7
|
+
- Circuit breaker REQUIRED
|
|
8
|
+
- Confirmation prompts for all write operations
|
|
9
|
+
- Detailed audit logging
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import json
|
|
14
|
+
import hashlib
|
|
15
|
+
from typing import Dict, List, Optional, Any
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import datetime, timedelta
|
|
18
|
+
from enum import Enum
|
|
19
|
+
from dotenv import load_dotenv
|
|
20
|
+
|
|
21
|
+
load_dotenv()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ============================================================================
|
|
25
|
+
# SAFETY COMPONENTS
|
|
26
|
+
# ============================================================================
|
|
27
|
+
|
|
28
|
+
class RefundStatus(Enum):
|
|
29
|
+
"""Refund processing status."""
|
|
30
|
+
PENDING = "pending"
|
|
31
|
+
PROCESSING = "processing"
|
|
32
|
+
SUCCEEDED = "succeeded"
|
|
33
|
+
FAILED = "failed"
|
|
34
|
+
DUPLICATE = "duplicate"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class IdempotencyKey:
|
|
39
|
+
"""Idempotency key for preventing duplicates."""
|
|
40
|
+
key: str
|
|
41
|
+
created_at: datetime
|
|
42
|
+
result: Optional[Dict] = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class IdempotencyManager:
|
|
46
|
+
"""Prevents duplicate operations using idempotency keys."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, ttl_seconds: int = 86400):
|
|
49
|
+
self.cache: Dict[str, IdempotencyKey] = {}
|
|
50
|
+
self.ttl_seconds = ttl_seconds
|
|
51
|
+
|
|
52
|
+
def generate_key(self, operation: str, **params) -> str:
|
|
53
|
+
data = json.dumps({"operation": operation, **params}, sort_keys=True)
|
|
54
|
+
key = hashlib.sha256(data.encode()).hexdigest()[:16]
|
|
55
|
+
return f"idem_{key}"
|
|
56
|
+
|
|
57
|
+
def get_cached(self, key: str) -> Optional[Dict]:
|
|
58
|
+
if key in self.cache:
|
|
59
|
+
idem = self.cache[key]
|
|
60
|
+
if (datetime.now() - idem.created_at).seconds < self.ttl_seconds:
|
|
61
|
+
return idem.result
|
|
62
|
+
del self.cache[key]
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def store(self, key: str, result: Dict):
|
|
66
|
+
self.cache[key] = IdempotencyKey(key=key, created_at=datetime.now(), result=result)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class RefundCircuitBreaker:
|
|
70
|
+
"""Circuit breaker to stop repeated refund failures."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, failure_threshold: int = 3, timeout_seconds: int = 60):
|
|
73
|
+
self.failure_threshold = failure_threshold
|
|
74
|
+
self.timeout_seconds = timeout_seconds
|
|
75
|
+
self.failure_count = 0
|
|
76
|
+
self.last_failure_time: Optional[datetime] = None
|
|
77
|
+
self.state = "closed"
|
|
78
|
+
|
|
79
|
+
def can_attempt(self) -> tuple[bool, str]:
|
|
80
|
+
if self.state == "closed": return True, "OK"
|
|
81
|
+
if self.state == "open":
|
|
82
|
+
if self.last_failure_time and (datetime.now() - self.last_failure_time).seconds >= self.timeout_seconds:
|
|
83
|
+
self.state = "half_open"
|
|
84
|
+
return True, "Testing"
|
|
85
|
+
return False, "Circuit open"
|
|
86
|
+
return True, "Testing"
|
|
87
|
+
|
|
88
|
+
def record_success(self):
|
|
89
|
+
self.failure_count = 0
|
|
90
|
+
self.state = "closed"
|
|
91
|
+
|
|
92
|
+
def record_failure(self):
|
|
93
|
+
self.failure_count += 1
|
|
94
|
+
self.last_failure_time = datetime.now()
|
|
95
|
+
if self.failure_count >= self.failure_threshold:
|
|
96
|
+
self.state = "open"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ============================================================================
|
|
100
|
+
# CONFIGURATION
|
|
101
|
+
# ============================================================================
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class StripeConfig:
|
|
105
|
+
"""Configuration for Stripe MCP server."""
|
|
106
|
+
api_key: str = ""
|
|
107
|
+
max_refund_amount: float = 1000.00
|
|
108
|
+
require_confirmation: bool = True
|
|
109
|
+
max_refunds_per_hour: int = 10
|
|
110
|
+
rate_limit_per_minute: int = 30
|
|
111
|
+
enable_read: bool = True
|
|
112
|
+
enable_refund: bool = False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ============================================================================
|
|
116
|
+
# STRIPE MCP SERVER
|
|
117
|
+
# ============================================================================
|
|
118
|
+
|
|
119
|
+
class StripeMCPServer:
|
|
120
|
+
"""
|
|
121
|
+
MCP Server for Stripe integration with safety defaults.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(self, config: StripeConfig = None, client = None, **kwargs):
|
|
125
|
+
# Handle api_key if passed directly
|
|
126
|
+
if 'api_key' in kwargs and config is None:
|
|
127
|
+
config = StripeConfig(api_key=kwargs['api_key'])
|
|
128
|
+
|
|
129
|
+
self.config = config or StripeConfig()
|
|
130
|
+
self.stripe = client # Should be a real stripe client in production
|
|
131
|
+
|
|
132
|
+
# SAFETY COMPONENTS
|
|
133
|
+
self.idempotency = IdempotencyManager()
|
|
134
|
+
self.circuit_breaker = RefundCircuitBreaker()
|
|
135
|
+
|
|
136
|
+
# Rate limiting
|
|
137
|
+
self.request_count = 0
|
|
138
|
+
self.refund_count_hour = 0
|
|
139
|
+
self.window_start = datetime.now()
|
|
140
|
+
self.hour_start = datetime.now()
|
|
141
|
+
|
|
142
|
+
# Audit log
|
|
143
|
+
self.audit_log: List[Dict] = []
|
|
144
|
+
|
|
145
|
+
print(f"[OK] Stripe MCP Server initialized (SAFETY MODE: EXTREME)")
|
|
146
|
+
|
|
147
|
+
def _check_rate_limit(self) -> bool:
|
|
148
|
+
now = datetime.now()
|
|
149
|
+
if (now - self.window_start).seconds >= 60:
|
|
150
|
+
self.request_count = 0
|
|
151
|
+
self.window_start = now
|
|
152
|
+
if self.request_count >= self.config.rate_limit_per_minute:
|
|
153
|
+
return False
|
|
154
|
+
self.request_count += 1
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
def _check_refund_limit(self) -> bool:
|
|
158
|
+
now = datetime.now()
|
|
159
|
+
if (now - self.hour_start).seconds >= 3600:
|
|
160
|
+
self.refund_count_hour = 0
|
|
161
|
+
self.hour_start = now
|
|
162
|
+
return self.refund_count_hour < self.config.max_refunds_per_hour
|
|
163
|
+
|
|
164
|
+
def _log_audit(self, operation: str, params: Dict, result: Dict):
|
|
165
|
+
self.audit_log.append({
|
|
166
|
+
"timestamp": datetime.now().isoformat(),
|
|
167
|
+
"operation": operation,
|
|
168
|
+
"params": params,
|
|
169
|
+
"result": result
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
def get_payment(self, charge_id: str) -> Dict[str, Any]:
|
|
173
|
+
"""Get payment details (READ-ONLY)."""
|
|
174
|
+
if not self.config.enable_read or not self._check_rate_limit():
|
|
175
|
+
return {"success": False, "error": "Access denied or rate limited"}
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
# Placeholder for real stripe call
|
|
179
|
+
if not self.stripe: return {"success": False, "error": "Stripe client not configured"}
|
|
180
|
+
charge = self.stripe.charges_retrieve(charge_id)
|
|
181
|
+
return {"success": True, "charge_id": charge_id, "amount": charge["amount"]/100}
|
|
182
|
+
except Exception as e:
|
|
183
|
+
return {"success": False, "error": str(e)}
|
|
184
|
+
|
|
185
|
+
def process_refund(self, charge_id: str, amount: Optional[float] = None) -> Dict[str, Any]:
|
|
186
|
+
"""Process refund with safety checks."""
|
|
187
|
+
if not self.config.enable_refund: return {"success": False, "error": "Refunds disabled"}
|
|
188
|
+
if not self._check_rate_limit() or not self._check_refund_limit():
|
|
189
|
+
return {"success": False, "error": "Limit exceeded"}
|
|
190
|
+
|
|
191
|
+
idem_key = self.idempotency.generate_key("refund", charge_id=charge_id, amount=amount)
|
|
192
|
+
cached = self.idempotency.get_cached(idem_key)
|
|
193
|
+
if cached: return {**cached, "duplicate_prevented": True}
|
|
194
|
+
|
|
195
|
+
can_attempt, _ = self.circuit_breaker.can_attempt()
|
|
196
|
+
if not can_attempt: return {"success": False, "error": "Circuit breaker open"}
|
|
197
|
+
|
|
198
|
+
if amount and amount > self.config.max_refund_amount and self.config.require_confirmation:
|
|
199
|
+
return {"success": False, "error": "Approval required", "requires_approval": True}
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
if not self.stripe: raise Exception("Stripe client not configured")
|
|
203
|
+
refund = self.stripe.refunds_create(charge=charge_id, amount=int(amount*100) if amount else None)
|
|
204
|
+
self.circuit_breaker.record_success()
|
|
205
|
+
self.refund_count_hour += 1
|
|
206
|
+
result = {"success": True, "refund_id": refund["id"], "charge_id": charge_id}
|
|
207
|
+
self.idempotency.store(idem_key, result)
|
|
208
|
+
self._log_audit("process_refund", {"charge_id": charge_id}, result)
|
|
209
|
+
return result
|
|
210
|
+
except Exception as e:
|
|
211
|
+
self.circuit_breaker.record_failure()
|
|
212
|
+
return {"success": False, "error": str(e)}
|
|
213
|
+
|
|
214
|
+
def get_tool_definitions(self) -> List[Dict]:
|
|
215
|
+
"""Get MCP tool definitions."""
|
|
216
|
+
return [
|
|
217
|
+
{"name": "stripe_get_payment", "description": "Get payment details (READ)"},
|
|
218
|
+
{"name": "stripe_process_refund", "description": "Process refund (WRITE)"}
|
|
219
|
+
]
|
kite/tools/search.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Standard Web Search Tool using DuckDuckGo Lite.
|
|
3
|
+
"""
|
|
4
|
+
import requests
|
|
5
|
+
import asyncio
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
from typing import Dict, Any
|
|
9
|
+
|
|
10
|
+
class WebSearchTool:
|
|
11
|
+
"""Real web search using DuckDuckGo Lite (No API key required)."""
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.name = "web_search"
|
|
14
|
+
self.description = "Search the real web for current information. Returns snippets."
|
|
15
|
+
self.session = requests.Session()
|
|
16
|
+
self.session.headers.update({
|
|
17
|
+
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
def get_definition(self):
|
|
21
|
+
return {
|
|
22
|
+
"type": "function",
|
|
23
|
+
"function": {
|
|
24
|
+
"name": self.name,
|
|
25
|
+
"description": self.description,
|
|
26
|
+
"parameters": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"properties": {
|
|
29
|
+
"query": {"type": "string", "description": "Search query"}
|
|
30
|
+
},
|
|
31
|
+
"required": ["query"]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
def to_schema(self):
|
|
37
|
+
return self.get_definition()
|
|
38
|
+
|
|
39
|
+
async def execute(self, query: str = None, **kwargs):
|
|
40
|
+
if not query:
|
|
41
|
+
return "Error: Please provide a search query."
|
|
42
|
+
|
|
43
|
+
print(f" [Tool] Real Web Search for: '{query}'...")
|
|
44
|
+
await asyncio.sleep(1) # Be polite (Async sleep)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
# DuckDuckGo Lite (HTML version)
|
|
48
|
+
url = "https://html.duckduckgo.com/html/"
|
|
49
|
+
data = {'q': query}
|
|
50
|
+
|
|
51
|
+
# Using asyncio toThread for blocking request if needed, but requests is sync.
|
|
52
|
+
# In a real async framework we might use aiohttp, but here we wrap or accept block for now.
|
|
53
|
+
# Or use run_in_executor to avoid blocking the loop
|
|
54
|
+
loop = asyncio.get_running_loop()
|
|
55
|
+
resp = await loop.run_in_executor(None, lambda: self.session.post(url, data=data, timeout=10))
|
|
56
|
+
|
|
57
|
+
if resp.status_code != 200:
|
|
58
|
+
return f"Error: Search failed with status {resp.status_code}"
|
|
59
|
+
|
|
60
|
+
# Regex scrape (Simple but effective for DDG Lite)
|
|
61
|
+
results = []
|
|
62
|
+
|
|
63
|
+
# Extract result blocks (approximate)
|
|
64
|
+
# Try BS4 if available, else regex
|
|
65
|
+
try:
|
|
66
|
+
from bs4 import BeautifulSoup
|
|
67
|
+
soup = BeautifulSoup(resp.text, 'html.parser')
|
|
68
|
+
links = soup.find_all('a', class_='result__a', limit=5)
|
|
69
|
+
for link in links:
|
|
70
|
+
title = link.get_text(strip=True)
|
|
71
|
+
href = link.get('href')
|
|
72
|
+
results.append(f"Title: {title}\nURL: {href}")
|
|
73
|
+
except ImportError:
|
|
74
|
+
# Fallback regex
|
|
75
|
+
titles = re.findall(r'class="result__a"[^>]*>(.*?)</a>', resp.text)
|
|
76
|
+
for i in range(min(len(titles), 5)):
|
|
77
|
+
results.append(f"Title: {titles[i]}")
|
|
78
|
+
|
|
79
|
+
if not results:
|
|
80
|
+
# Regex fallback if BS4 logic missed or wasn't used
|
|
81
|
+
titles = re.findall(r'class="result__a"[^>]*>(.*?)</a>', resp.text)
|
|
82
|
+
for i in range(min(len(titles), 5)):
|
|
83
|
+
results.append(f"Title: {titles[i]}")
|
|
84
|
+
|
|
85
|
+
output = "\n---\n".join(results[:5])
|
|
86
|
+
if not output: return "No results found."
|
|
87
|
+
return output
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
return f"Search error: {e}"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
System & Shell Tools
|
|
3
|
+
===================
|
|
4
|
+
Tools for interacting with the operating system.
|
|
5
|
+
Designed for DevOps agents.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import subprocess
|
|
9
|
+
import shlex
|
|
10
|
+
from kite.tool import Tool
|
|
11
|
+
|
|
12
|
+
class ShellTool(Tool):
|
|
13
|
+
def __init__(self, allowed_commands=None):
|
|
14
|
+
"""
|
|
15
|
+
Args:
|
|
16
|
+
allowed_commands: List of allowed command prefixes (e.g. ['ls', 'grep']).
|
|
17
|
+
If None, allows everything (DANGEROUS).
|
|
18
|
+
"""
|
|
19
|
+
super().__init__(
|
|
20
|
+
name="shell_execute",
|
|
21
|
+
func=self.execute,
|
|
22
|
+
description="Execute a shell command. Use for system inspection, git operations, or file checks."
|
|
23
|
+
)
|
|
24
|
+
self.allowed_commands = allowed_commands or ["ls", "cat", "grep", "echo", "git", "pwd", "whoami"]
|
|
25
|
+
|
|
26
|
+
async def execute(self, command: str, **kwargs) -> str:
|
|
27
|
+
"""Executes shell command."""
|
|
28
|
+
cmd_parts = shlex.split(command)
|
|
29
|
+
if not cmd_parts:
|
|
30
|
+
return "Empty command."
|
|
31
|
+
|
|
32
|
+
base_cmd = cmd_parts[0]
|
|
33
|
+
|
|
34
|
+
# Security Check
|
|
35
|
+
if base_cmd not in self.allowed_commands:
|
|
36
|
+
return f"Security Alert: Command '{base_cmd}' is not allowed. Allowed: {self.allowed_commands}"
|
|
37
|
+
|
|
38
|
+
if "rm" in cmd_parts and "-rf" in cmd_parts:
|
|
39
|
+
return "Security Alert: 'rm -rf' is strictly forbidden."
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
result = subprocess.run(
|
|
43
|
+
command,
|
|
44
|
+
shell=True,
|
|
45
|
+
capture_output=True,
|
|
46
|
+
text=True,
|
|
47
|
+
timeout=10
|
|
48
|
+
)
|
|
49
|
+
if result.returncode == 0:
|
|
50
|
+
return result.stdout.strip() or "[Command executed with no output]"
|
|
51
|
+
else:
|
|
52
|
+
return f"Error: {result.stderr}"
|
|
53
|
+
except Exception as e:
|
|
54
|
+
return f"Execution Failed: {e}"
|
kite/tools_manager.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Tools (MCP Servers) Manager"""
|
|
2
|
+
|
|
3
|
+
class MCPServers:
|
|
4
|
+
def __init__(self, config, logger):
|
|
5
|
+
self.config = config
|
|
6
|
+
self.logger = logger
|
|
7
|
+
self._init_servers()
|
|
8
|
+
|
|
9
|
+
def _init_servers(self):
|
|
10
|
+
"""Initialize MCP servers from the tools.mcp package."""
|
|
11
|
+
from .tools.mcp import (
|
|
12
|
+
PostgresMCPServer,
|
|
13
|
+
SlackMCPServer,
|
|
14
|
+
StripeMCPServer,
|
|
15
|
+
GmailMCPServer,
|
|
16
|
+
GDriveMCPServer
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
self.postgres = PostgresMCPServer(connection_string=self.config.get('postgres_url'))
|
|
20
|
+
self.slack = SlackMCPServer(bot_token=self.config.get('slack_token'))
|
|
21
|
+
self.stripe = StripeMCPServer(api_key=self.config.get('stripe_key'))
|
|
22
|
+
self.gmail = GmailMCPServer(credentials_path=self.config.get('gmail_credentials'))
|
|
23
|
+
self.gdrive = GDriveMCPServer(credentials_path=self.config.get('gdrive_credentials'))
|
|
24
|
+
|
|
25
|
+
self.logger.info(" [OK] Tools (MCP)")
|
|
26
|
+
|
|
27
|
+
|