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