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.
Files changed (144) hide show
  1. image-service/main.py +178 -0
  2. infra/chat/app/main.py +84 -0
  3. kairo/backend/__init__.py +0 -0
  4. kairo/backend/api/__init__.py +0 -0
  5. kairo/backend/api/admin/__init__.py +23 -0
  6. kairo/backend/api/admin/audit.py +54 -0
  7. kairo/backend/api/admin/content.py +142 -0
  8. kairo/backend/api/admin/incidents.py +148 -0
  9. kairo/backend/api/admin/stats.py +125 -0
  10. kairo/backend/api/admin/system.py +87 -0
  11. kairo/backend/api/admin/users.py +279 -0
  12. kairo/backend/api/agents.py +94 -0
  13. kairo/backend/api/api_keys.py +85 -0
  14. kairo/backend/api/auth.py +116 -0
  15. kairo/backend/api/billing.py +41 -0
  16. kairo/backend/api/chat.py +72 -0
  17. kairo/backend/api/conversations.py +125 -0
  18. kairo/backend/api/device_auth.py +100 -0
  19. kairo/backend/api/files.py +83 -0
  20. kairo/backend/api/health.py +36 -0
  21. kairo/backend/api/images.py +80 -0
  22. kairo/backend/api/openai_compat.py +225 -0
  23. kairo/backend/api/projects.py +102 -0
  24. kairo/backend/api/usage.py +32 -0
  25. kairo/backend/api/webhooks.py +79 -0
  26. kairo/backend/app.py +297 -0
  27. kairo/backend/config.py +179 -0
  28. kairo/backend/core/__init__.py +0 -0
  29. kairo/backend/core/admin_auth.py +24 -0
  30. kairo/backend/core/api_key_auth.py +55 -0
  31. kairo/backend/core/database.py +28 -0
  32. kairo/backend/core/dependencies.py +70 -0
  33. kairo/backend/core/logging.py +23 -0
  34. kairo/backend/core/rate_limit.py +73 -0
  35. kairo/backend/core/security.py +29 -0
  36. kairo/backend/models/__init__.py +19 -0
  37. kairo/backend/models/agent.py +30 -0
  38. kairo/backend/models/api_key.py +25 -0
  39. kairo/backend/models/api_usage.py +29 -0
  40. kairo/backend/models/audit_log.py +26 -0
  41. kairo/backend/models/conversation.py +48 -0
  42. kairo/backend/models/device_code.py +30 -0
  43. kairo/backend/models/feature_flag.py +21 -0
  44. kairo/backend/models/image_generation.py +24 -0
  45. kairo/backend/models/incident.py +28 -0
  46. kairo/backend/models/project.py +28 -0
  47. kairo/backend/models/uptime_record.py +24 -0
  48. kairo/backend/models/usage.py +24 -0
  49. kairo/backend/models/user.py +49 -0
  50. kairo/backend/schemas/__init__.py +0 -0
  51. kairo/backend/schemas/admin/__init__.py +0 -0
  52. kairo/backend/schemas/admin/audit.py +28 -0
  53. kairo/backend/schemas/admin/content.py +53 -0
  54. kairo/backend/schemas/admin/stats.py +77 -0
  55. kairo/backend/schemas/admin/system.py +44 -0
  56. kairo/backend/schemas/admin/users.py +48 -0
  57. kairo/backend/schemas/agent.py +42 -0
  58. kairo/backend/schemas/api_key.py +30 -0
  59. kairo/backend/schemas/auth.py +57 -0
  60. kairo/backend/schemas/chat.py +26 -0
  61. kairo/backend/schemas/conversation.py +39 -0
  62. kairo/backend/schemas/device_auth.py +40 -0
  63. kairo/backend/schemas/image.py +15 -0
  64. kairo/backend/schemas/openai_compat.py +76 -0
  65. kairo/backend/schemas/project.py +21 -0
  66. kairo/backend/schemas/status.py +81 -0
  67. kairo/backend/schemas/usage.py +15 -0
  68. kairo/backend/services/__init__.py +0 -0
  69. kairo/backend/services/admin/__init__.py +0 -0
  70. kairo/backend/services/admin/audit_service.py +78 -0
  71. kairo/backend/services/admin/content_service.py +119 -0
  72. kairo/backend/services/admin/incident_service.py +94 -0
  73. kairo/backend/services/admin/stats_service.py +281 -0
  74. kairo/backend/services/admin/system_service.py +126 -0
  75. kairo/backend/services/admin/user_service.py +157 -0
  76. kairo/backend/services/agent_service.py +107 -0
  77. kairo/backend/services/api_key_service.py +66 -0
  78. kairo/backend/services/api_usage_service.py +126 -0
  79. kairo/backend/services/auth_service.py +101 -0
  80. kairo/backend/services/chat_service.py +501 -0
  81. kairo/backend/services/conversation_service.py +264 -0
  82. kairo/backend/services/device_auth_service.py +193 -0
  83. kairo/backend/services/email_service.py +55 -0
  84. kairo/backend/services/image_service.py +181 -0
  85. kairo/backend/services/llm_service.py +186 -0
  86. kairo/backend/services/project_service.py +109 -0
  87. kairo/backend/services/status_service.py +167 -0
  88. kairo/backend/services/stripe_service.py +78 -0
  89. kairo/backend/services/usage_service.py +150 -0
  90. kairo/backend/services/web_search_service.py +96 -0
  91. kairo/migrations/env.py +60 -0
  92. kairo/migrations/versions/001_initial.py +55 -0
  93. kairo/migrations/versions/002_usage_tracking_and_indexes.py +66 -0
  94. kairo/migrations/versions/003_username_to_email.py +21 -0
  95. kairo/migrations/versions/004_add_plans_and_verification.py +67 -0
  96. kairo/migrations/versions/005_add_projects.py +52 -0
  97. kairo/migrations/versions/006_add_image_generation.py +63 -0
  98. kairo/migrations/versions/007_add_admin_portal.py +107 -0
  99. kairo/migrations/versions/008_add_device_code_auth.py +76 -0
  100. kairo/migrations/versions/009_add_status_page.py +65 -0
  101. kairo/tools/extract_claude_data.py +465 -0
  102. kairo/tools/filter_claude_data.py +303 -0
  103. kairo/tools/generate_curated_data.py +157 -0
  104. kairo/tools/mix_training_data.py +295 -0
  105. kairo_code/__init__.py +3 -0
  106. kairo_code/agents/__init__.py +25 -0
  107. kairo_code/agents/architect.py +98 -0
  108. kairo_code/agents/audit.py +100 -0
  109. kairo_code/agents/base.py +463 -0
  110. kairo_code/agents/coder.py +155 -0
  111. kairo_code/agents/database.py +77 -0
  112. kairo_code/agents/docs.py +88 -0
  113. kairo_code/agents/explorer.py +62 -0
  114. kairo_code/agents/guardian.py +80 -0
  115. kairo_code/agents/planner.py +66 -0
  116. kairo_code/agents/reviewer.py +91 -0
  117. kairo_code/agents/security.py +94 -0
  118. kairo_code/agents/terraform.py +88 -0
  119. kairo_code/agents/testing.py +97 -0
  120. kairo_code/agents/uiux.py +88 -0
  121. kairo_code/auth.py +232 -0
  122. kairo_code/config.py +172 -0
  123. kairo_code/conversation.py +173 -0
  124. kairo_code/heartbeat.py +63 -0
  125. kairo_code/llm.py +291 -0
  126. kairo_code/logging_config.py +156 -0
  127. kairo_code/main.py +818 -0
  128. kairo_code/router.py +217 -0
  129. kairo_code/sandbox.py +248 -0
  130. kairo_code/settings.py +183 -0
  131. kairo_code/tools/__init__.py +51 -0
  132. kairo_code/tools/analysis.py +509 -0
  133. kairo_code/tools/base.py +417 -0
  134. kairo_code/tools/code.py +58 -0
  135. kairo_code/tools/definitions.py +617 -0
  136. kairo_code/tools/files.py +315 -0
  137. kairo_code/tools/review.py +390 -0
  138. kairo_code/tools/search.py +185 -0
  139. kairo_code/ui.py +418 -0
  140. kairo_code-0.1.0.dist-info/METADATA +13 -0
  141. kairo_code-0.1.0.dist-info/RECORD +144 -0
  142. kairo_code-0.1.0.dist-info/WHEEL +5 -0
  143. kairo_code-0.1.0.dist-info/entry_points.txt +2 -0
  144. kairo_code-0.1.0.dist-info/top_level.txt +4 -0
@@ -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))
@@ -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