kairo-code 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.
- image-service/main.py +178 -0
- infra/chat/app/main.py +84 -0
- kairo/backend/__init__.py +0 -0
- kairo/backend/api/__init__.py +0 -0
- kairo/backend/api/admin/__init__.py +23 -0
- kairo/backend/api/admin/audit.py +54 -0
- kairo/backend/api/admin/content.py +142 -0
- kairo/backend/api/admin/incidents.py +148 -0
- kairo/backend/api/admin/stats.py +125 -0
- kairo/backend/api/admin/system.py +87 -0
- kairo/backend/api/admin/users.py +279 -0
- kairo/backend/api/agents.py +94 -0
- kairo/backend/api/api_keys.py +85 -0
- kairo/backend/api/auth.py +116 -0
- kairo/backend/api/billing.py +41 -0
- kairo/backend/api/chat.py +72 -0
- kairo/backend/api/conversations.py +125 -0
- kairo/backend/api/device_auth.py +100 -0
- kairo/backend/api/files.py +83 -0
- kairo/backend/api/health.py +36 -0
- kairo/backend/api/images.py +80 -0
- kairo/backend/api/openai_compat.py +225 -0
- kairo/backend/api/projects.py +102 -0
- kairo/backend/api/usage.py +32 -0
- kairo/backend/api/webhooks.py +79 -0
- kairo/backend/app.py +297 -0
- kairo/backend/config.py +179 -0
- kairo/backend/core/__init__.py +0 -0
- kairo/backend/core/admin_auth.py +24 -0
- kairo/backend/core/api_key_auth.py +55 -0
- kairo/backend/core/database.py +28 -0
- kairo/backend/core/dependencies.py +70 -0
- kairo/backend/core/logging.py +23 -0
- kairo/backend/core/rate_limit.py +73 -0
- kairo/backend/core/security.py +29 -0
- kairo/backend/models/__init__.py +19 -0
- kairo/backend/models/agent.py +30 -0
- kairo/backend/models/api_key.py +25 -0
- kairo/backend/models/api_usage.py +29 -0
- kairo/backend/models/audit_log.py +26 -0
- kairo/backend/models/conversation.py +48 -0
- kairo/backend/models/device_code.py +30 -0
- kairo/backend/models/feature_flag.py +21 -0
- kairo/backend/models/image_generation.py +24 -0
- kairo/backend/models/incident.py +28 -0
- kairo/backend/models/project.py +28 -0
- kairo/backend/models/uptime_record.py +24 -0
- kairo/backend/models/usage.py +24 -0
- kairo/backend/models/user.py +49 -0
- kairo/backend/schemas/__init__.py +0 -0
- kairo/backend/schemas/admin/__init__.py +0 -0
- kairo/backend/schemas/admin/audit.py +28 -0
- kairo/backend/schemas/admin/content.py +53 -0
- kairo/backend/schemas/admin/stats.py +77 -0
- kairo/backend/schemas/admin/system.py +44 -0
- kairo/backend/schemas/admin/users.py +48 -0
- kairo/backend/schemas/agent.py +42 -0
- kairo/backend/schemas/api_key.py +30 -0
- kairo/backend/schemas/auth.py +57 -0
- kairo/backend/schemas/chat.py +26 -0
- kairo/backend/schemas/conversation.py +39 -0
- kairo/backend/schemas/device_auth.py +40 -0
- kairo/backend/schemas/image.py +15 -0
- kairo/backend/schemas/openai_compat.py +76 -0
- kairo/backend/schemas/project.py +21 -0
- kairo/backend/schemas/status.py +81 -0
- kairo/backend/schemas/usage.py +15 -0
- kairo/backend/services/__init__.py +0 -0
- kairo/backend/services/admin/__init__.py +0 -0
- kairo/backend/services/admin/audit_service.py +78 -0
- kairo/backend/services/admin/content_service.py +119 -0
- kairo/backend/services/admin/incident_service.py +94 -0
- kairo/backend/services/admin/stats_service.py +281 -0
- kairo/backend/services/admin/system_service.py +126 -0
- kairo/backend/services/admin/user_service.py +157 -0
- kairo/backend/services/agent_service.py +107 -0
- kairo/backend/services/api_key_service.py +66 -0
- kairo/backend/services/api_usage_service.py +126 -0
- kairo/backend/services/auth_service.py +101 -0
- kairo/backend/services/chat_service.py +501 -0
- kairo/backend/services/conversation_service.py +264 -0
- kairo/backend/services/device_auth_service.py +193 -0
- kairo/backend/services/email_service.py +55 -0
- kairo/backend/services/image_service.py +181 -0
- kairo/backend/services/llm_service.py +186 -0
- kairo/backend/services/project_service.py +109 -0
- kairo/backend/services/status_service.py +167 -0
- kairo/backend/services/stripe_service.py +78 -0
- kairo/backend/services/usage_service.py +150 -0
- kairo/backend/services/web_search_service.py +96 -0
- kairo/migrations/env.py +60 -0
- kairo/migrations/versions/001_initial.py +55 -0
- kairo/migrations/versions/002_usage_tracking_and_indexes.py +66 -0
- kairo/migrations/versions/003_username_to_email.py +21 -0
- kairo/migrations/versions/004_add_plans_and_verification.py +67 -0
- kairo/migrations/versions/005_add_projects.py +52 -0
- kairo/migrations/versions/006_add_image_generation.py +63 -0
- kairo/migrations/versions/007_add_admin_portal.py +107 -0
- kairo/migrations/versions/008_add_device_code_auth.py +76 -0
- kairo/migrations/versions/009_add_status_page.py +65 -0
- kairo/tools/extract_claude_data.py +465 -0
- kairo/tools/filter_claude_data.py +303 -0
- kairo/tools/generate_curated_data.py +157 -0
- kairo/tools/mix_training_data.py +295 -0
- kairo_code/__init__.py +3 -0
- kairo_code/agents/__init__.py +25 -0
- kairo_code/agents/architect.py +98 -0
- kairo_code/agents/audit.py +100 -0
- kairo_code/agents/base.py +463 -0
- kairo_code/agents/coder.py +155 -0
- kairo_code/agents/database.py +77 -0
- kairo_code/agents/docs.py +88 -0
- kairo_code/agents/explorer.py +62 -0
- kairo_code/agents/guardian.py +80 -0
- kairo_code/agents/planner.py +66 -0
- kairo_code/agents/reviewer.py +91 -0
- kairo_code/agents/security.py +94 -0
- kairo_code/agents/terraform.py +88 -0
- kairo_code/agents/testing.py +97 -0
- kairo_code/agents/uiux.py +88 -0
- kairo_code/auth.py +232 -0
- kairo_code/config.py +172 -0
- kairo_code/conversation.py +173 -0
- kairo_code/heartbeat.py +63 -0
- kairo_code/llm.py +291 -0
- kairo_code/logging_config.py +156 -0
- kairo_code/main.py +818 -0
- kairo_code/router.py +217 -0
- kairo_code/sandbox.py +248 -0
- kairo_code/settings.py +183 -0
- kairo_code/tools/__init__.py +51 -0
- kairo_code/tools/analysis.py +509 -0
- kairo_code/tools/base.py +417 -0
- kairo_code/tools/code.py +58 -0
- kairo_code/tools/definitions.py +617 -0
- kairo_code/tools/files.py +315 -0
- kairo_code/tools/review.py +390 -0
- kairo_code/tools/search.py +185 -0
- kairo_code/ui.py +418 -0
- kairo_code-0.1.0.dist-info/METADATA +13 -0
- kairo_code-0.1.0.dist-info/RECORD +144 -0
- kairo_code-0.1.0.dist-info/WHEEL +5 -0
- kairo_code-0.1.0.dist-info/entry_points.txt +2 -0
- kairo_code-0.1.0.dist-info/top_level.txt +4 -0
kairo_code/tools/base.py
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""Tool system with robust registry and execution"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ToolResult:
|
|
12
|
+
"""Result from a tool execution."""
|
|
13
|
+
success: bool
|
|
14
|
+
output: str
|
|
15
|
+
error: str | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ParsedToolCall:
|
|
20
|
+
"""A parsed tool call from LLM output."""
|
|
21
|
+
tool_name: str
|
|
22
|
+
params: dict
|
|
23
|
+
raw_text: str
|
|
24
|
+
parse_error: str | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ToolParseResult:
|
|
29
|
+
"""Result of parsing tool calls from text."""
|
|
30
|
+
calls: list[ParsedToolCall] = field(default_factory=list)
|
|
31
|
+
has_thinking: bool = False
|
|
32
|
+
thinking_content: str = ""
|
|
33
|
+
raw_text: str = ""
|
|
34
|
+
parse_errors: list[str] = field(default_factory=list)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Tool(ABC):
|
|
38
|
+
"""Base class for all tools."""
|
|
39
|
+
|
|
40
|
+
name: str
|
|
41
|
+
description: str
|
|
42
|
+
parameters: dict # JSON schema for parameters
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def execute(self, **kwargs) -> ToolResult:
|
|
46
|
+
"""Execute the tool with given parameters."""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
def to_schema(self) -> dict:
|
|
50
|
+
"""Generate schema for LLM tool calling."""
|
|
51
|
+
return {
|
|
52
|
+
"name": self.name,
|
|
53
|
+
"description": self.description,
|
|
54
|
+
"parameters": self.parameters,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ToolRegistry:
|
|
59
|
+
"""Registry for managing available tools."""
|
|
60
|
+
|
|
61
|
+
def __init__(self):
|
|
62
|
+
self._tools: dict[str, Tool] = {}
|
|
63
|
+
|
|
64
|
+
def register(self, tool: Tool) -> None:
|
|
65
|
+
"""Register a tool."""
|
|
66
|
+
self._tools[tool.name] = tool
|
|
67
|
+
|
|
68
|
+
def get(self, name: str) -> Tool | None:
|
|
69
|
+
"""Get a tool by name, with strict matching only."""
|
|
70
|
+
# Normalize the name
|
|
71
|
+
name_lower = name.lower().strip()
|
|
72
|
+
|
|
73
|
+
# Exact match first
|
|
74
|
+
if name in self._tools:
|
|
75
|
+
return self._tools[name]
|
|
76
|
+
|
|
77
|
+
# Case-insensitive exact match
|
|
78
|
+
for tool_name in self._tools:
|
|
79
|
+
if tool_name.lower() == name_lower:
|
|
80
|
+
return self._tools[tool_name]
|
|
81
|
+
|
|
82
|
+
# Handle common variations (underscore vs no underscore)
|
|
83
|
+
name_normalized = name_lower.replace("_", "").replace("-", "")
|
|
84
|
+
for tool_name in self._tools:
|
|
85
|
+
if tool_name.lower().replace("_", "").replace("-", "") == name_normalized:
|
|
86
|
+
return self._tools[tool_name]
|
|
87
|
+
|
|
88
|
+
# No fuzzy matching - return None and let caller handle the error
|
|
89
|
+
# This prevents confusing matches like "write" -> "read_file"
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
def get_tool_names(self) -> list[str]:
|
|
93
|
+
"""Get list of all tool names."""
|
|
94
|
+
return list(self._tools.keys())
|
|
95
|
+
|
|
96
|
+
def list_tools(self) -> list[Tool]:
|
|
97
|
+
"""List all registered tools."""
|
|
98
|
+
return list(self._tools.values())
|
|
99
|
+
|
|
100
|
+
def get_schemas(self) -> list[dict]:
|
|
101
|
+
"""Get schemas for all tools."""
|
|
102
|
+
return [t.to_schema() for t in self._tools.values()]
|
|
103
|
+
|
|
104
|
+
def format_for_prompt(self) -> str:
|
|
105
|
+
"""Format tools for inclusion in system prompt with full details."""
|
|
106
|
+
lines = ["## Available Tools\n"]
|
|
107
|
+
|
|
108
|
+
for tool in self._tools.values():
|
|
109
|
+
lines.append(f"### {tool.name}")
|
|
110
|
+
lines.append(f"{tool.description}\n")
|
|
111
|
+
|
|
112
|
+
props = tool.parameters.get("properties", {})
|
|
113
|
+
required = tool.parameters.get("required", [])
|
|
114
|
+
|
|
115
|
+
if props:
|
|
116
|
+
lines.append("Parameters:")
|
|
117
|
+
for param_name, param_info in props.items():
|
|
118
|
+
req = "(required)" if param_name in required else "(optional)"
|
|
119
|
+
desc = param_info.get("description", "")
|
|
120
|
+
lines.append(f" - {param_name} {req}: {desc}")
|
|
121
|
+
|
|
122
|
+
lines.append("")
|
|
123
|
+
|
|
124
|
+
return "\n".join(lines)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def parse_tool_calls(text: str) -> ToolParseResult:
|
|
128
|
+
"""
|
|
129
|
+
Robustly parse tool calls from LLM output.
|
|
130
|
+
|
|
131
|
+
Supports multiple formats:
|
|
132
|
+
1. XML-style: <tool>name</tool><params>{...}</params>
|
|
133
|
+
2. Bracket-style: [TOOL: name][PARAMS: {...}]
|
|
134
|
+
3. Action-style: ACTION: name\nPARAMS: {...}
|
|
135
|
+
4. Function-style: name({...})
|
|
136
|
+
|
|
137
|
+
Also extracts thinking blocks.
|
|
138
|
+
"""
|
|
139
|
+
result = ToolParseResult(raw_text=text)
|
|
140
|
+
|
|
141
|
+
# Extract thinking blocks first
|
|
142
|
+
thinking_patterns = [
|
|
143
|
+
r"<thinking>(.*?)</thinking>",
|
|
144
|
+
r"<scratchpad>(.*?)</scratchpad>",
|
|
145
|
+
r"\[THINKING\](.*?)\[/THINKING\]",
|
|
146
|
+
r"```thinking\n(.*?)```",
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
for pattern in thinking_patterns:
|
|
150
|
+
match = re.search(pattern, text, re.DOTALL | re.IGNORECASE)
|
|
151
|
+
if match:
|
|
152
|
+
result.has_thinking = True
|
|
153
|
+
result.thinking_content = match.group(1).strip()
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
# Try multiple parsing strategies
|
|
157
|
+
calls = []
|
|
158
|
+
|
|
159
|
+
# Strategy 1: XML-style (most structured)
|
|
160
|
+
# Handle various whitespace issues
|
|
161
|
+
xml_pattern = r"<tool>\s*(\w+)\s*</tool>\s*<params>\s*(.*?)\s*</params>"
|
|
162
|
+
for match in re.finditer(xml_pattern, text, re.DOTALL | re.IGNORECASE):
|
|
163
|
+
parsed = _parse_params(match.group(2), match.group(1))
|
|
164
|
+
if parsed:
|
|
165
|
+
calls.append(parsed)
|
|
166
|
+
else:
|
|
167
|
+
result.parse_errors.append(f"Failed to parse params for {match.group(1)}")
|
|
168
|
+
|
|
169
|
+
# Strategy 2: Bracket-style
|
|
170
|
+
bracket_pattern = r"\[TOOL:\s*(\w+)\s*\]\s*\[PARAMS:\s*(.*?)\s*\]"
|
|
171
|
+
for match in re.finditer(bracket_pattern, text, re.DOTALL | re.IGNORECASE):
|
|
172
|
+
parsed = _parse_params(match.group(2), match.group(1))
|
|
173
|
+
if parsed:
|
|
174
|
+
calls.append(parsed)
|
|
175
|
+
|
|
176
|
+
# Strategy 3: Action-style (newline separated)
|
|
177
|
+
action_pattern = r"ACTION:\s*(\w+)\s*\nPARAMS:\s*(\{.*?\})"
|
|
178
|
+
for match in re.finditer(action_pattern, text, re.DOTALL):
|
|
179
|
+
parsed = _parse_params(match.group(2), match.group(1))
|
|
180
|
+
if parsed:
|
|
181
|
+
calls.append(parsed)
|
|
182
|
+
|
|
183
|
+
# Strategy 4: Markdown code block with tool call
|
|
184
|
+
code_pattern = r"```(?:tool|action)\n(\w+)\n(\{.*?\})\n```"
|
|
185
|
+
for match in re.finditer(code_pattern, text, re.DOTALL):
|
|
186
|
+
parsed = _parse_params(match.group(2), match.group(1))
|
|
187
|
+
if parsed:
|
|
188
|
+
calls.append(parsed)
|
|
189
|
+
|
|
190
|
+
# Strategy 5: Simple function call style: tool_name({"param": "value"})
|
|
191
|
+
func_pattern = r"(\w+)\s*\(\s*(\{.*?\})\s*\)"
|
|
192
|
+
for match in re.finditer(func_pattern, text, re.DOTALL):
|
|
193
|
+
# Only accept if it looks like a tool name (not random function)
|
|
194
|
+
name = match.group(1).lower()
|
|
195
|
+
if name in ['read_file', 'write_file', 'edit_file', 'list_files',
|
|
196
|
+
'search_files', 'web_search', 'bash', 'grep', 'tree']:
|
|
197
|
+
parsed = _parse_params(match.group(2), match.group(1))
|
|
198
|
+
if parsed:
|
|
199
|
+
calls.append(parsed)
|
|
200
|
+
|
|
201
|
+
# Strategy 6: Llama3-style direct tags: <tool_name>params</tool_name>
|
|
202
|
+
# e.g., <web_search>query="test"</web_search>
|
|
203
|
+
tool_names = ['read_file', 'write_file', 'edit_file', 'list_files',
|
|
204
|
+
'search_files', 'web_search', 'bash', 'grep', 'tree']
|
|
205
|
+
for tool_name in tool_names:
|
|
206
|
+
llama_pattern = rf"<{tool_name}>(.*?)</{tool_name}>"
|
|
207
|
+
for match in re.finditer(llama_pattern, text, re.DOTALL | re.IGNORECASE):
|
|
208
|
+
params_str = match.group(1).strip()
|
|
209
|
+
parsed = _parse_llama_params(params_str, tool_name)
|
|
210
|
+
if parsed:
|
|
211
|
+
calls.append(parsed)
|
|
212
|
+
|
|
213
|
+
# Strategy 7: write_file with separate path and content blocks
|
|
214
|
+
# Handles: <write_file path="app.py">code here</write_file>
|
|
215
|
+
# or: <write_file><path>app.py</path><content>code</content></write_file>
|
|
216
|
+
write_pattern_attr = r'<write_file\s+path=["\']([^"\']+)["\']\s*>(.*?)</write_file>'
|
|
217
|
+
for match in re.finditer(write_pattern_attr, text, re.DOTALL | re.IGNORECASE):
|
|
218
|
+
calls.append(ParsedToolCall(
|
|
219
|
+
tool_name="write_file",
|
|
220
|
+
params={"path": match.group(1), "content": match.group(2).strip()},
|
|
221
|
+
raw_text=match.group(0),
|
|
222
|
+
))
|
|
223
|
+
|
|
224
|
+
# Strategy 8: write_file with nested tags
|
|
225
|
+
write_pattern_nested = r'<write_file>\s*<path>([^<]+)</path>\s*<content>(.*?)</content>\s*</write_file>'
|
|
226
|
+
for match in re.finditer(write_pattern_nested, text, re.DOTALL | re.IGNORECASE):
|
|
227
|
+
calls.append(ParsedToolCall(
|
|
228
|
+
tool_name="write_file",
|
|
229
|
+
params={"path": match.group(1).strip(), "content": match.group(2).strip()},
|
|
230
|
+
raw_text=match.group(0),
|
|
231
|
+
))
|
|
232
|
+
|
|
233
|
+
# Strategy 9: edit_file with nested tags
|
|
234
|
+
# <edit_file path="file.py"><old>old code</old><new>new code</new></edit_file>
|
|
235
|
+
edit_pattern_nested = r'<edit_file\s+path=["\']([^"\']+)["\']\s*>\s*<old>(.*?)</old>\s*<new>(.*?)</new>\s*</edit_file>'
|
|
236
|
+
for match in re.finditer(edit_pattern_nested, text, re.DOTALL | re.IGNORECASE):
|
|
237
|
+
calls.append(ParsedToolCall(
|
|
238
|
+
tool_name="edit_file",
|
|
239
|
+
params={
|
|
240
|
+
"path": match.group(1).strip(),
|
|
241
|
+
"old_string": match.group(2).strip(),
|
|
242
|
+
"new_string": match.group(3).strip(),
|
|
243
|
+
},
|
|
244
|
+
raw_text=match.group(0),
|
|
245
|
+
))
|
|
246
|
+
|
|
247
|
+
# Deduplicate calls (same tool + same params)
|
|
248
|
+
seen = set()
|
|
249
|
+
unique_calls = []
|
|
250
|
+
for call in calls:
|
|
251
|
+
key = (call.tool_name, json.dumps(call.params, sort_keys=True))
|
|
252
|
+
if key not in seen:
|
|
253
|
+
seen.add(key)
|
|
254
|
+
unique_calls.append(call)
|
|
255
|
+
|
|
256
|
+
result.calls = unique_calls
|
|
257
|
+
return result
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _parse_llama_params(params_str: str, tool_name: str) -> ParsedToolCall | None:
|
|
261
|
+
"""Parse llama3-style params like: query="test" or path="file", content="code" """
|
|
262
|
+
params_str = params_str.strip()
|
|
263
|
+
params = {}
|
|
264
|
+
|
|
265
|
+
# Pattern for key="value" or key='value'
|
|
266
|
+
kv_pattern = r'(\w+)\s*=\s*["\']([^"\']*)["\']'
|
|
267
|
+
for match in re.finditer(kv_pattern, params_str):
|
|
268
|
+
params[match.group(1)] = match.group(2)
|
|
269
|
+
|
|
270
|
+
# Pattern for key={...} (JSON objects)
|
|
271
|
+
json_pattern = r'(\w+)\s*=\s*(\{[^}]*\})'
|
|
272
|
+
for match in re.finditer(json_pattern, params_str):
|
|
273
|
+
try:
|
|
274
|
+
params[match.group(1)] = json.loads(match.group(2))
|
|
275
|
+
except Exception:
|
|
276
|
+
params[match.group(1)] = match.group(2)
|
|
277
|
+
|
|
278
|
+
# If no key=value params found, treat raw text as the primary parameter
|
|
279
|
+
# based on tool type (common llama3 behavior)
|
|
280
|
+
if not params and params_str:
|
|
281
|
+
primary_param_map = {
|
|
282
|
+
'web_search': 'query',
|
|
283
|
+
'search_files': 'pattern',
|
|
284
|
+
'grep': 'pattern',
|
|
285
|
+
'bash': 'command',
|
|
286
|
+
'read_file': 'path',
|
|
287
|
+
}
|
|
288
|
+
primary_param = primary_param_map.get(tool_name.lower())
|
|
289
|
+
if primary_param:
|
|
290
|
+
params[primary_param] = params_str
|
|
291
|
+
|
|
292
|
+
if params:
|
|
293
|
+
return ParsedToolCall(
|
|
294
|
+
tool_name=tool_name.lower().strip(),
|
|
295
|
+
params=params,
|
|
296
|
+
raw_text=params_str,
|
|
297
|
+
)
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _parse_params(params_str: str, tool_name: str) -> ParsedToolCall | None:
|
|
302
|
+
"""Parse parameters JSON with error recovery."""
|
|
303
|
+
params_str = params_str.strip()
|
|
304
|
+
|
|
305
|
+
# Fix 0: Handle Python triple-quoted strings in JSON (common LLM mistake)
|
|
306
|
+
# Convert: {"content": """code"""} to {"content": "code"}
|
|
307
|
+
triple_quote_pattern = r'"""\s*(.*?)\s*"""'
|
|
308
|
+
if '"""' in params_str:
|
|
309
|
+
def replace_triple(match):
|
|
310
|
+
content = match.group(1)
|
|
311
|
+
# Escape for JSON
|
|
312
|
+
content = content.replace('\\', '\\\\')
|
|
313
|
+
content = content.replace('"', '\\"')
|
|
314
|
+
content = content.replace('\n', '\\n')
|
|
315
|
+
content = content.replace('\t', '\\t')
|
|
316
|
+
return f'"{content}"'
|
|
317
|
+
params_str = re.sub(triple_quote_pattern, replace_triple, params_str, flags=re.DOTALL)
|
|
318
|
+
|
|
319
|
+
# Try direct JSON parse
|
|
320
|
+
try:
|
|
321
|
+
params = json.loads(params_str)
|
|
322
|
+
return ParsedToolCall(
|
|
323
|
+
tool_name=tool_name.lower().strip(),
|
|
324
|
+
params=params,
|
|
325
|
+
raw_text=params_str,
|
|
326
|
+
)
|
|
327
|
+
except json.JSONDecodeError:
|
|
328
|
+
pass
|
|
329
|
+
|
|
330
|
+
# Try fixing common issues
|
|
331
|
+
|
|
332
|
+
# Fix 1: Single quotes to double quotes
|
|
333
|
+
fixed = params_str.replace("'", '"')
|
|
334
|
+
try:
|
|
335
|
+
params = json.loads(fixed)
|
|
336
|
+
return ParsedToolCall(
|
|
337
|
+
tool_name=tool_name.lower().strip(),
|
|
338
|
+
params=params,
|
|
339
|
+
raw_text=params_str,
|
|
340
|
+
)
|
|
341
|
+
except json.JSONDecodeError:
|
|
342
|
+
pass
|
|
343
|
+
|
|
344
|
+
# Fix 2: Add missing braces
|
|
345
|
+
if not params_str.startswith("{"):
|
|
346
|
+
params_str = "{" + params_str
|
|
347
|
+
if not params_str.endswith("}"):
|
|
348
|
+
params_str = params_str + "}"
|
|
349
|
+
try:
|
|
350
|
+
params = json.loads(params_str)
|
|
351
|
+
return ParsedToolCall(
|
|
352
|
+
tool_name=tool_name.lower().strip(),
|
|
353
|
+
params=params,
|
|
354
|
+
raw_text=params_str,
|
|
355
|
+
)
|
|
356
|
+
except json.JSONDecodeError:
|
|
357
|
+
pass
|
|
358
|
+
|
|
359
|
+
# Fix 3: Try to extract key-value pairs manually
|
|
360
|
+
try:
|
|
361
|
+
# Pattern: "key": "value" or "key": value
|
|
362
|
+
kv_pattern = r'"(\w+)":\s*"([^"]*)"'
|
|
363
|
+
matches = re.findall(kv_pattern, params_str)
|
|
364
|
+
if matches:
|
|
365
|
+
params = dict(matches)
|
|
366
|
+
return ParsedToolCall(
|
|
367
|
+
tool_name=tool_name.lower().strip(),
|
|
368
|
+
params=params,
|
|
369
|
+
raw_text=params_str,
|
|
370
|
+
)
|
|
371
|
+
except Exception:
|
|
372
|
+
pass
|
|
373
|
+
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def format_tool_error(error: str, tool_name: str = None) -> str:
|
|
378
|
+
"""Format a tool error message for feeding back to the model."""
|
|
379
|
+
if tool_name:
|
|
380
|
+
return f"""[TOOL ERROR]
|
|
381
|
+
Tool: {tool_name}
|
|
382
|
+
Error: {error}
|
|
383
|
+
|
|
384
|
+
Please fix the issue and try again. Make sure to use the correct format:
|
|
385
|
+
<tool>{tool_name}</tool>
|
|
386
|
+
<params>{{"param": "value"}}</params>
|
|
387
|
+
"""
|
|
388
|
+
return f"""[PARSE ERROR]
|
|
389
|
+
{error}
|
|
390
|
+
|
|
391
|
+
Your tool call could not be parsed. Please use this exact format:
|
|
392
|
+
<tool>tool_name</tool>
|
|
393
|
+
<params>{{"param": "value"}}</params>
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class FunctionTool(Tool):
|
|
398
|
+
"""Tool that wraps a simple function."""
|
|
399
|
+
|
|
400
|
+
def __init__(
|
|
401
|
+
self,
|
|
402
|
+
name: str,
|
|
403
|
+
description: str,
|
|
404
|
+
func: Callable,
|
|
405
|
+
parameters: dict,
|
|
406
|
+
):
|
|
407
|
+
self.name = name
|
|
408
|
+
self.description = description
|
|
409
|
+
self.func = func
|
|
410
|
+
self.parameters = parameters
|
|
411
|
+
|
|
412
|
+
def execute(self, **kwargs) -> ToolResult:
|
|
413
|
+
try:
|
|
414
|
+
result = self.func(**kwargs)
|
|
415
|
+
return ToolResult(success=True, output=str(result))
|
|
416
|
+
except Exception as e:
|
|
417
|
+
return ToolResult(success=False, output="", error=str(e))
|
kairo_code/tools/code.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Code generation and parsing utilities"""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def extract_code_blocks(text: str) -> list[dict]:
|
|
7
|
+
"""
|
|
8
|
+
Extract code blocks from markdown-formatted text.
|
|
9
|
+
|
|
10
|
+
Returns:
|
|
11
|
+
List of dicts with 'language' and 'code' keys
|
|
12
|
+
"""
|
|
13
|
+
pattern = r"```(\w*)\n(.*?)```"
|
|
14
|
+
matches = re.findall(pattern, text, re.DOTALL)
|
|
15
|
+
|
|
16
|
+
return [
|
|
17
|
+
{"language": lang or "text", "code": code.strip()}
|
|
18
|
+
for lang, code in matches
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_code_prompt(
|
|
23
|
+
request: str,
|
|
24
|
+
context: str | None = None,
|
|
25
|
+
language: str | None = None,
|
|
26
|
+
) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Build a prompt for code generation.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
request: What the user wants
|
|
32
|
+
context: Additional context (file contents, search results)
|
|
33
|
+
language: Preferred programming language
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Formatted prompt string
|
|
37
|
+
"""
|
|
38
|
+
parts = []
|
|
39
|
+
|
|
40
|
+
if context:
|
|
41
|
+
parts.append(f"Context:\n{context}\n")
|
|
42
|
+
|
|
43
|
+
if language:
|
|
44
|
+
parts.append(f"Language: {language}\n")
|
|
45
|
+
|
|
46
|
+
parts.append(f"Request: {request}")
|
|
47
|
+
|
|
48
|
+
return "\n".join(parts)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def build_explain_prompt(code: str, question: str | None = None) -> str:
|
|
52
|
+
"""Build a prompt for code explanation."""
|
|
53
|
+
prompt = f"Explain this code:\n\n```\n{code}\n```"
|
|
54
|
+
|
|
55
|
+
if question:
|
|
56
|
+
prompt += f"\n\nSpecifically: {question}"
|
|
57
|
+
|
|
58
|
+
return prompt
|