code-puppy 0.0.373__py3-none-any.whl → 0.0.374__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.
- code_puppy/agents/agent_creator_agent.py +49 -1
- code_puppy/agents/agent_helios.py +122 -0
- code_puppy/agents/agent_manager.py +26 -2
- code_puppy/agents/json_agent.py +30 -7
- code_puppy/command_line/colors_menu.py +2 -0
- code_puppy/command_line/command_handler.py +1 -0
- code_puppy/command_line/config_commands.py +3 -1
- code_puppy/command_line/uc_menu.py +890 -0
- code_puppy/config.py +29 -0
- code_puppy/messaging/messages.py +18 -0
- code_puppy/messaging/rich_renderer.py +35 -0
- code_puppy/messaging/subagent_console.py +0 -1
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +304 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/tools/__init__.py +138 -1
- code_puppy/tools/universal_constructor.py +889 -0
- {code_puppy-0.0.373.dist-info → code_puppy-0.0.374.dist-info}/METADATA +1 -1
- {code_puppy-0.0.373.dist-info → code_puppy-0.0.374.dist-info}/RECORD +26 -18
- {code_puppy-0.0.373.data → code_puppy-0.0.374.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.373.data → code_puppy-0.0.374.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.373.dist-info → code_puppy-0.0.374.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.373.dist-info → code_puppy-0.0.374.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.373.dist-info → code_puppy-0.0.374.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,889 @@
|
|
|
1
|
+
"""Universal Constructor Tool - Dynamic tool creation and management.
|
|
2
|
+
|
|
3
|
+
This module provides the universal_constructor tool that enables users
|
|
4
|
+
to create, manage, and call custom tools dynamically during a session.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import subprocess
|
|
8
|
+
import time
|
|
9
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
10
|
+
from concurrent.futures import TimeoutError as FuturesTimeoutError
|
|
11
|
+
from typing import Literal, Optional
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
from pydantic_ai import RunContext
|
|
15
|
+
|
|
16
|
+
from code_puppy.messaging import get_message_bus
|
|
17
|
+
from code_puppy.messaging.messages import UniversalConstructorMessage
|
|
18
|
+
from code_puppy.plugins.universal_constructor.models import (
|
|
19
|
+
UCCallOutput,
|
|
20
|
+
UCCreateOutput,
|
|
21
|
+
UCInfoOutput,
|
|
22
|
+
UCListOutput,
|
|
23
|
+
UCUpdateOutput,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UniversalConstructorOutput(BaseModel):
|
|
28
|
+
"""Unified response model for universal_constructor operations.
|
|
29
|
+
|
|
30
|
+
Wraps all action-specific outputs with a common interface.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
action: str = Field(..., description="The action that was performed")
|
|
34
|
+
success: bool = Field(..., description="Whether the operation succeeded")
|
|
35
|
+
error: Optional[str] = Field(default=None, description="Error message if failed")
|
|
36
|
+
|
|
37
|
+
# Action-specific results (only one will be populated based on action)
|
|
38
|
+
list_result: Optional[UCListOutput] = Field(
|
|
39
|
+
default=None, description="Result of list action"
|
|
40
|
+
)
|
|
41
|
+
call_result: Optional[UCCallOutput] = Field(
|
|
42
|
+
default=None, description="Result of call action"
|
|
43
|
+
)
|
|
44
|
+
create_result: Optional[UCCreateOutput] = Field(
|
|
45
|
+
default=None, description="Result of create action"
|
|
46
|
+
)
|
|
47
|
+
update_result: Optional[UCUpdateOutput] = Field(
|
|
48
|
+
default=None, description="Result of update action"
|
|
49
|
+
)
|
|
50
|
+
info_result: Optional[UCInfoOutput] = Field(
|
|
51
|
+
default=None, description="Result of info action"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _stub_not_implemented(action: str) -> UniversalConstructorOutput:
|
|
58
|
+
"""Return a stub response for unimplemented actions."""
|
|
59
|
+
return UniversalConstructorOutput(
|
|
60
|
+
action=action,
|
|
61
|
+
success=False,
|
|
62
|
+
error="Not implemented yet",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _run_ruff_format(file_path) -> Optional[str]:
|
|
67
|
+
"""Run ruff format on a file.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
file_path: Path to the file to format (str or Path)
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Warning message if formatting failed, None on success
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
result = subprocess.run(
|
|
77
|
+
["ruff", "format", str(file_path)],
|
|
78
|
+
capture_output=True,
|
|
79
|
+
text=True,
|
|
80
|
+
timeout=10,
|
|
81
|
+
)
|
|
82
|
+
if result.returncode != 0:
|
|
83
|
+
return f"ruff format failed: {result.stderr.strip()}"
|
|
84
|
+
return None
|
|
85
|
+
except FileNotFoundError:
|
|
86
|
+
return "ruff not found - code not formatted"
|
|
87
|
+
except subprocess.TimeoutExpired:
|
|
88
|
+
return "ruff format timed out"
|
|
89
|
+
except Exception as e:
|
|
90
|
+
return f"ruff format error: {e}"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _generate_preview(code: str, max_lines: int = 10) -> str:
|
|
94
|
+
"""Generate a preview of the first N lines of code.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
code: The source code to preview
|
|
98
|
+
max_lines: Maximum number of lines to include (default 10)
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
A string with the first N lines, with "..." appended if truncated
|
|
102
|
+
"""
|
|
103
|
+
lines = code.splitlines()
|
|
104
|
+
if len(lines) <= max_lines:
|
|
105
|
+
return code
|
|
106
|
+
preview_lines = lines[:max_lines]
|
|
107
|
+
return "\n".join(preview_lines) + "\n... (truncated)"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _emit_uc_message(
|
|
111
|
+
action: str,
|
|
112
|
+
success: bool,
|
|
113
|
+
summary: str,
|
|
114
|
+
tool_name: Optional[str] = None,
|
|
115
|
+
details: Optional[str] = None,
|
|
116
|
+
) -> None:
|
|
117
|
+
"""Emit a UniversalConstructorMessage to the message bus.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
action: The UC action performed (list/call/create/update/info)
|
|
121
|
+
success: Whether the operation succeeded
|
|
122
|
+
summary: Brief summary of the result
|
|
123
|
+
tool_name: Tool name if applicable
|
|
124
|
+
details: Additional details (optional)
|
|
125
|
+
"""
|
|
126
|
+
bus = get_message_bus()
|
|
127
|
+
msg = UniversalConstructorMessage(
|
|
128
|
+
action=action,
|
|
129
|
+
tool_name=tool_name,
|
|
130
|
+
success=success,
|
|
131
|
+
summary=summary,
|
|
132
|
+
details=details,
|
|
133
|
+
)
|
|
134
|
+
bus.emit(msg)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def universal_constructor_impl(
|
|
138
|
+
context: RunContext,
|
|
139
|
+
action: Literal["list", "call", "create", "update", "info"],
|
|
140
|
+
tool_name: Optional[str] = None,
|
|
141
|
+
tool_args: Optional[dict] = None,
|
|
142
|
+
python_code: Optional[str] = None,
|
|
143
|
+
description: Optional[str] = None,
|
|
144
|
+
) -> UniversalConstructorOutput:
|
|
145
|
+
"""Implementation of the universal_constructor tool.
|
|
146
|
+
|
|
147
|
+
Routes to appropriate action handler based on the action parameter.
|
|
148
|
+
All actions are currently stubbed out and will return "Not implemented yet".
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
context: The run context from pydantic-ai
|
|
152
|
+
action: The operation to perform:
|
|
153
|
+
- "list": List all available UC tools
|
|
154
|
+
- "call": Execute a specific UC tool
|
|
155
|
+
- "create": Create a new UC tool from Python code
|
|
156
|
+
- "update": Modify an existing UC tool
|
|
157
|
+
- "info": Get detailed info about a specific tool
|
|
158
|
+
tool_name: Name of tool (for call/update/info). Supports "namespace.name" format.
|
|
159
|
+
tool_args: Arguments to pass when calling a tool (for call action)
|
|
160
|
+
python_code: Python source code for the tool (for create/update actions)
|
|
161
|
+
description: Human-readable description (for create action)
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
UniversalConstructorOutput with action-specific results
|
|
165
|
+
"""
|
|
166
|
+
# Route to appropriate action handler
|
|
167
|
+
if action == "list":
|
|
168
|
+
result = _handle_list_action(context)
|
|
169
|
+
elif action == "call":
|
|
170
|
+
result = _handle_call_action(context, tool_name, tool_args)
|
|
171
|
+
elif action == "create":
|
|
172
|
+
result = _handle_create_action(context, tool_name, python_code, description)
|
|
173
|
+
elif action == "update":
|
|
174
|
+
result = _handle_update_action(context, tool_name, python_code, description)
|
|
175
|
+
elif action == "info":
|
|
176
|
+
result = _handle_info_action(context, tool_name)
|
|
177
|
+
else:
|
|
178
|
+
result = UniversalConstructorOutput(
|
|
179
|
+
action=action,
|
|
180
|
+
success=False,
|
|
181
|
+
error=f"Unknown action: {action}",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Emit the banner message after the action completes
|
|
185
|
+
summary = _build_summary(result)
|
|
186
|
+
_emit_uc_message(
|
|
187
|
+
action=action,
|
|
188
|
+
success=result.success,
|
|
189
|
+
summary=summary,
|
|
190
|
+
tool_name=tool_name,
|
|
191
|
+
details=result.error if not result.success else None,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return result
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _build_summary(result: UniversalConstructorOutput) -> str:
|
|
198
|
+
"""Build a brief summary string from a UC result.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
result: The UniversalConstructorOutput to summarize
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
A brief human-readable summary string
|
|
205
|
+
"""
|
|
206
|
+
if not result.success:
|
|
207
|
+
return result.error or "Operation failed"
|
|
208
|
+
|
|
209
|
+
if result.list_result:
|
|
210
|
+
return f"Found {result.list_result.enabled_count} enabled tools (of {result.list_result.total_count} total)"
|
|
211
|
+
elif result.call_result:
|
|
212
|
+
exec_time = result.call_result.execution_time or 0
|
|
213
|
+
return f"Executed in {exec_time:.2f}s"
|
|
214
|
+
elif result.create_result:
|
|
215
|
+
return f"Created {result.create_result.tool_name}"
|
|
216
|
+
elif result.update_result:
|
|
217
|
+
return f"Updated {result.update_result.tool_name}"
|
|
218
|
+
elif result.info_result and result.info_result.tool:
|
|
219
|
+
return f"Info for {result.info_result.tool.full_name}"
|
|
220
|
+
else:
|
|
221
|
+
return "Operation completed"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _handle_list_action(context: RunContext) -> UniversalConstructorOutput:
|
|
225
|
+
"""Handle the 'list' action - list all available UC tools.
|
|
226
|
+
|
|
227
|
+
Lists all enabled tools from the UC registry, returning their
|
|
228
|
+
metadata, signatures, and source paths.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
context: The run context from pydantic-ai (unused for list action)
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
UniversalConstructorOutput with list_result containing all enabled tools.
|
|
235
|
+
"""
|
|
236
|
+
from code_puppy.plugins.universal_constructor.registry import get_registry
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
registry = get_registry()
|
|
240
|
+
# Get all tools (including disabled for count)
|
|
241
|
+
all_tools = registry.list_tools(include_disabled=True)
|
|
242
|
+
enabled_tools = [t for t in all_tools if t.meta.enabled]
|
|
243
|
+
|
|
244
|
+
return UniversalConstructorOutput(
|
|
245
|
+
action="list",
|
|
246
|
+
success=True,
|
|
247
|
+
list_result=UCListOutput(
|
|
248
|
+
tools=enabled_tools,
|
|
249
|
+
total_count=len(all_tools),
|
|
250
|
+
enabled_count=len(enabled_tools),
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
return UniversalConstructorOutput(
|
|
255
|
+
action="list",
|
|
256
|
+
success=False,
|
|
257
|
+
error=f"Failed to list tools: {e}",
|
|
258
|
+
list_result=UCListOutput(
|
|
259
|
+
tools=[],
|
|
260
|
+
total_count=0,
|
|
261
|
+
enabled_count=0,
|
|
262
|
+
error=str(e),
|
|
263
|
+
),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _handle_call_action(
|
|
268
|
+
context: RunContext,
|
|
269
|
+
tool_name: Optional[str],
|
|
270
|
+
tool_args: Optional[dict],
|
|
271
|
+
) -> UniversalConstructorOutput:
|
|
272
|
+
"""Handle the 'call' action - execute a UC tool.
|
|
273
|
+
|
|
274
|
+
Validates the tool exists and is enabled, then executes it with a timeout.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
context: The run context from pydantic-ai
|
|
278
|
+
tool_name: Name of the tool to call (required)
|
|
279
|
+
tool_args: Arguments to pass to the tool function
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
UniversalConstructorOutput with call_result on success or error on failure
|
|
283
|
+
"""
|
|
284
|
+
if not tool_name:
|
|
285
|
+
return UniversalConstructorOutput(
|
|
286
|
+
action="call",
|
|
287
|
+
success=False,
|
|
288
|
+
error="tool_name is required for call action",
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
from code_puppy.plugins.universal_constructor.registry import get_registry
|
|
292
|
+
|
|
293
|
+
registry = get_registry()
|
|
294
|
+
tool = registry.get_tool(tool_name)
|
|
295
|
+
|
|
296
|
+
if not tool:
|
|
297
|
+
return UniversalConstructorOutput(
|
|
298
|
+
action="call",
|
|
299
|
+
success=False,
|
|
300
|
+
error=f"Tool '{tool_name}' not found",
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
if not tool.meta.enabled:
|
|
304
|
+
return UniversalConstructorOutput(
|
|
305
|
+
action="call",
|
|
306
|
+
success=False,
|
|
307
|
+
error=f"Tool '{tool_name}' is disabled",
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Read source for preview
|
|
311
|
+
source_preview = None
|
|
312
|
+
if tool.source_path:
|
|
313
|
+
try:
|
|
314
|
+
from pathlib import Path
|
|
315
|
+
|
|
316
|
+
source_code = Path(tool.source_path).read_text(encoding="utf-8")
|
|
317
|
+
source_preview = _generate_preview(source_code)
|
|
318
|
+
except Exception:
|
|
319
|
+
pass # Preview is optional, don't fail on read errors
|
|
320
|
+
|
|
321
|
+
func = registry.get_tool_function(tool_name)
|
|
322
|
+
if not func:
|
|
323
|
+
return UniversalConstructorOutput(
|
|
324
|
+
action="call",
|
|
325
|
+
success=False,
|
|
326
|
+
error=f"Could not load function for '{tool_name}'",
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Handle tool_args being passed as a JSON string (XML marshaling issue)
|
|
330
|
+
args = tool_args or {}
|
|
331
|
+
if isinstance(args, str):
|
|
332
|
+
try:
|
|
333
|
+
import json
|
|
334
|
+
|
|
335
|
+
args = json.loads(args)
|
|
336
|
+
except json.JSONDecodeError:
|
|
337
|
+
return UniversalConstructorOutput(
|
|
338
|
+
action="call",
|
|
339
|
+
success=False,
|
|
340
|
+
error=f"Invalid tool_args: expected dict or JSON string, got: {args[:100]}",
|
|
341
|
+
)
|
|
342
|
+
if not isinstance(args, dict):
|
|
343
|
+
return UniversalConstructorOutput(
|
|
344
|
+
action="call",
|
|
345
|
+
success=False,
|
|
346
|
+
error=f"tool_args must be a dict, got {type(args).__name__}",
|
|
347
|
+
)
|
|
348
|
+
start_time = time.time()
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
# Execute with timeout using ThreadPoolExecutor
|
|
352
|
+
with ThreadPoolExecutor(max_workers=1) as executor:
|
|
353
|
+
future = executor.submit(func, **args)
|
|
354
|
+
result = future.result(timeout=30)
|
|
355
|
+
|
|
356
|
+
execution_time = time.time() - start_time
|
|
357
|
+
|
|
358
|
+
return UniversalConstructorOutput(
|
|
359
|
+
action="call",
|
|
360
|
+
success=True,
|
|
361
|
+
call_result=UCCallOutput(
|
|
362
|
+
success=True,
|
|
363
|
+
tool_name=tool_name,
|
|
364
|
+
result=result,
|
|
365
|
+
execution_time=execution_time,
|
|
366
|
+
source_preview=source_preview,
|
|
367
|
+
),
|
|
368
|
+
)
|
|
369
|
+
except FuturesTimeoutError:
|
|
370
|
+
return UniversalConstructorOutput(
|
|
371
|
+
action="call",
|
|
372
|
+
success=False,
|
|
373
|
+
error=f"Tool '{tool_name}' timed out after 30s",
|
|
374
|
+
)
|
|
375
|
+
except TypeError as e:
|
|
376
|
+
# Invalid arguments
|
|
377
|
+
return UniversalConstructorOutput(
|
|
378
|
+
action="call",
|
|
379
|
+
success=False,
|
|
380
|
+
error=f"Invalid arguments for '{tool_name}': {e!s}",
|
|
381
|
+
)
|
|
382
|
+
except Exception as e:
|
|
383
|
+
return UniversalConstructorOutput(
|
|
384
|
+
action="call",
|
|
385
|
+
success=False,
|
|
386
|
+
error=f"Tool execution failed: {e!s}",
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _handle_create_action(
|
|
391
|
+
context: RunContext,
|
|
392
|
+
tool_name: Optional[str],
|
|
393
|
+
python_code: Optional[str],
|
|
394
|
+
description: Optional[str],
|
|
395
|
+
) -> UniversalConstructorOutput:
|
|
396
|
+
"""Handle the 'create' action - create a new UC tool.
|
|
397
|
+
|
|
398
|
+
Creates a new tool from Python source code. The code can either include
|
|
399
|
+
a TOOL_META dictionary, or one will be generated from the provided
|
|
400
|
+
tool_name and description parameters.
|
|
401
|
+
|
|
402
|
+
Supports namespacing via dot notation in tool_name:
|
|
403
|
+
- "weather" → weather.py
|
|
404
|
+
- "api.weather" → api/weather.py
|
|
405
|
+
- "api.finance.stocks" → api/finance/stocks.py
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
context: The run context from pydantic-ai
|
|
409
|
+
tool_name: Name of the tool (with optional namespace). Required if
|
|
410
|
+
code doesn't contain TOOL_META.
|
|
411
|
+
python_code: Python source code defining the tool function (required)
|
|
412
|
+
description: Description of what the tool does. Used if no TOOL_META
|
|
413
|
+
in code.
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
UniversalConstructorOutput with create_result on success
|
|
417
|
+
"""
|
|
418
|
+
from datetime import datetime
|
|
419
|
+
from pathlib import Path
|
|
420
|
+
|
|
421
|
+
from code_puppy.plugins.universal_constructor import USER_UC_DIR
|
|
422
|
+
from code_puppy.plugins.universal_constructor.registry import get_registry
|
|
423
|
+
from code_puppy.plugins.universal_constructor.sandbox import (
|
|
424
|
+
_extract_tool_meta,
|
|
425
|
+
_validate_tool_meta,
|
|
426
|
+
check_dangerous_patterns,
|
|
427
|
+
extract_function_info,
|
|
428
|
+
validate_syntax,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Validate python_code is provided
|
|
432
|
+
if not python_code or not python_code.strip():
|
|
433
|
+
return UniversalConstructorOutput(
|
|
434
|
+
action="create",
|
|
435
|
+
success=False,
|
|
436
|
+
error="python_code is required for create action",
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# Validate syntax
|
|
440
|
+
syntax_result = validate_syntax(python_code)
|
|
441
|
+
if not syntax_result.valid:
|
|
442
|
+
error_msg = "; ".join(syntax_result.errors)
|
|
443
|
+
return UniversalConstructorOutput(
|
|
444
|
+
action="create",
|
|
445
|
+
success=False,
|
|
446
|
+
error=f"Syntax error in code: {error_msg}",
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
# Extract function info
|
|
450
|
+
func_result = extract_function_info(python_code)
|
|
451
|
+
if not func_result.functions:
|
|
452
|
+
return UniversalConstructorOutput(
|
|
453
|
+
action="create",
|
|
454
|
+
success=False,
|
|
455
|
+
error="No functions found in code - tool must have at least one function",
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
# Get the first function as the main tool function
|
|
459
|
+
main_func = func_result.functions[0]
|
|
460
|
+
|
|
461
|
+
# Try to extract TOOL_META from code
|
|
462
|
+
existing_meta = _extract_tool_meta(python_code)
|
|
463
|
+
|
|
464
|
+
# Determine final tool name and namespace
|
|
465
|
+
if existing_meta and "name" in existing_meta:
|
|
466
|
+
# Use name from TOOL_META
|
|
467
|
+
final_name = existing_meta["name"]
|
|
468
|
+
final_namespace = existing_meta.get("namespace", "")
|
|
469
|
+
elif tool_name:
|
|
470
|
+
# Parse namespace from tool_name (e.g., "api.weather" → namespace="api", name="weather")
|
|
471
|
+
parts = tool_name.rsplit(".", 1)
|
|
472
|
+
if len(parts) == 2:
|
|
473
|
+
final_namespace, final_name = parts[0], parts[1]
|
|
474
|
+
else:
|
|
475
|
+
final_namespace, final_name = "", parts[0]
|
|
476
|
+
else:
|
|
477
|
+
# Use function name as tool name
|
|
478
|
+
final_name = main_func.name
|
|
479
|
+
final_namespace = ""
|
|
480
|
+
|
|
481
|
+
# Validate we have a name
|
|
482
|
+
if not final_name:
|
|
483
|
+
return UniversalConstructorOutput(
|
|
484
|
+
action="create",
|
|
485
|
+
success=False,
|
|
486
|
+
error="Could not determine tool name - provide tool_name or include TOOL_META in code",
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
# Build file path based on namespace
|
|
490
|
+
if final_namespace:
|
|
491
|
+
# Convert dot notation to path (api.finance → api/finance/)
|
|
492
|
+
namespace_path = Path(*final_namespace.split("."))
|
|
493
|
+
file_dir = USER_UC_DIR / namespace_path
|
|
494
|
+
else:
|
|
495
|
+
file_dir = USER_UC_DIR
|
|
496
|
+
|
|
497
|
+
file_path = file_dir / f"{final_name}.py"
|
|
498
|
+
|
|
499
|
+
# Build the final code to write
|
|
500
|
+
validation_warnings = []
|
|
501
|
+
|
|
502
|
+
if existing_meta:
|
|
503
|
+
# Validate existing TOOL_META has required fields
|
|
504
|
+
meta_errors = _validate_tool_meta(existing_meta)
|
|
505
|
+
if meta_errors:
|
|
506
|
+
return UniversalConstructorOutput(
|
|
507
|
+
action="create",
|
|
508
|
+
success=False,
|
|
509
|
+
error="Invalid TOOL_META: " + "; ".join(meta_errors),
|
|
510
|
+
)
|
|
511
|
+
# Code already has TOOL_META, use as-is
|
|
512
|
+
final_code = python_code
|
|
513
|
+
# Collect any validation warnings
|
|
514
|
+
validation_warnings.extend(func_result.warnings)
|
|
515
|
+
else:
|
|
516
|
+
# Generate TOOL_META and prepend to code
|
|
517
|
+
final_description = description or main_func.docstring or f"Tool: {final_name}"
|
|
518
|
+
|
|
519
|
+
generated_meta = {
|
|
520
|
+
"name": final_name,
|
|
521
|
+
"namespace": final_namespace,
|
|
522
|
+
"description": final_description,
|
|
523
|
+
"enabled": True,
|
|
524
|
+
"version": "1.0.0",
|
|
525
|
+
"author": "user",
|
|
526
|
+
"created_at": datetime.now().isoformat(),
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
# Format TOOL_META as a dict literal
|
|
530
|
+
meta_str = f"TOOL_META = {repr(generated_meta)}\n\n"
|
|
531
|
+
final_code = meta_str + python_code
|
|
532
|
+
validation_warnings.append("TOOL_META was auto-generated")
|
|
533
|
+
validation_warnings.extend(func_result.warnings)
|
|
534
|
+
|
|
535
|
+
# Check for dangerous patterns (warning only, don't block)
|
|
536
|
+
safety_result = check_dangerous_patterns(python_code)
|
|
537
|
+
validation_warnings.extend(safety_result.warnings)
|
|
538
|
+
|
|
539
|
+
# Ensure directory exists and write file
|
|
540
|
+
try:
|
|
541
|
+
file_dir.mkdir(parents=True, exist_ok=True)
|
|
542
|
+
file_path.write_text(final_code, encoding="utf-8")
|
|
543
|
+
except Exception as e:
|
|
544
|
+
return UniversalConstructorOutput(
|
|
545
|
+
action="create",
|
|
546
|
+
success=False,
|
|
547
|
+
error=f"Failed to write tool file: {e}",
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
# Run ruff format on the new file
|
|
551
|
+
format_warning = _run_ruff_format(file_path)
|
|
552
|
+
if format_warning:
|
|
553
|
+
validation_warnings.append(format_warning)
|
|
554
|
+
|
|
555
|
+
# Read formatted code for preview
|
|
556
|
+
formatted_code = file_path.read_text(encoding="utf-8")
|
|
557
|
+
|
|
558
|
+
# Reload registry to pick up the new tool
|
|
559
|
+
try:
|
|
560
|
+
registry = get_registry()
|
|
561
|
+
registry.reload()
|
|
562
|
+
except Exception as e:
|
|
563
|
+
# Tool was written but registry reload failed - still a partial success
|
|
564
|
+
validation_warnings.append(f"Tool created but registry reload failed: {e}")
|
|
565
|
+
|
|
566
|
+
# Build full name for response
|
|
567
|
+
full_name = f"{final_namespace}.{final_name}" if final_namespace else final_name
|
|
568
|
+
|
|
569
|
+
return UniversalConstructorOutput(
|
|
570
|
+
action="create",
|
|
571
|
+
success=True,
|
|
572
|
+
create_result=UCCreateOutput(
|
|
573
|
+
success=True,
|
|
574
|
+
tool_name=full_name,
|
|
575
|
+
source_path=str(file_path),
|
|
576
|
+
preview=_generate_preview(formatted_code),
|
|
577
|
+
validation_warnings=validation_warnings,
|
|
578
|
+
),
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _handle_update_action(
|
|
583
|
+
context: RunContext,
|
|
584
|
+
tool_name: Optional[str],
|
|
585
|
+
python_code: Optional[str],
|
|
586
|
+
description: Optional[str],
|
|
587
|
+
) -> UniversalConstructorOutput:
|
|
588
|
+
"""Handle the 'update' action - modify an existing UC tool.
|
|
589
|
+
|
|
590
|
+
Replaces an existing tool's code with new Python source code.
|
|
591
|
+
The new code must contain a valid TOOL_META dictionary.
|
|
592
|
+
|
|
593
|
+
Note: To update description or other metadata, include the changes
|
|
594
|
+
in the TOOL_META of the python_code. The description parameter is
|
|
595
|
+
reserved for future use but currently ignored.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
context: The run context from pydantic-ai
|
|
599
|
+
tool_name: Name of the tool to update (required)
|
|
600
|
+
python_code: New Python source code (required)
|
|
601
|
+
description: Reserved for future use (currently ignored)
|
|
602
|
+
|
|
603
|
+
Returns:
|
|
604
|
+
UniversalConstructorOutput with update_result on success
|
|
605
|
+
"""
|
|
606
|
+
from pathlib import Path
|
|
607
|
+
|
|
608
|
+
from code_puppy.plugins.universal_constructor.registry import get_registry
|
|
609
|
+
from code_puppy.plugins.universal_constructor.sandbox import (
|
|
610
|
+
_extract_tool_meta,
|
|
611
|
+
_validate_tool_meta,
|
|
612
|
+
validate_syntax,
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
if not tool_name:
|
|
616
|
+
return UniversalConstructorOutput(
|
|
617
|
+
action="update",
|
|
618
|
+
success=False,
|
|
619
|
+
error="tool_name is required for update action",
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
# python_code is required for updates
|
|
623
|
+
if not python_code:
|
|
624
|
+
return UniversalConstructorOutput(
|
|
625
|
+
action="update",
|
|
626
|
+
success=False,
|
|
627
|
+
error="python_code is required for update action",
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
registry = get_registry()
|
|
631
|
+
tool = registry.get_tool(tool_name)
|
|
632
|
+
|
|
633
|
+
if not tool:
|
|
634
|
+
return UniversalConstructorOutput(
|
|
635
|
+
action="update",
|
|
636
|
+
success=False,
|
|
637
|
+
error=f"Tool '{tool_name}' not found",
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
source_path = tool.source_path
|
|
641
|
+
source_path_obj = Path(source_path) if source_path else None
|
|
642
|
+
if not source_path_obj or not source_path_obj.exists():
|
|
643
|
+
return UniversalConstructorOutput(
|
|
644
|
+
action="update",
|
|
645
|
+
success=False,
|
|
646
|
+
error="Tool has no source path or file does not exist",
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
try:
|
|
650
|
+
# Validate new code syntax
|
|
651
|
+
syntax_result = validate_syntax(python_code)
|
|
652
|
+
if not syntax_result.valid:
|
|
653
|
+
error_msg = "; ".join(syntax_result.errors)
|
|
654
|
+
return UniversalConstructorOutput(
|
|
655
|
+
action="update",
|
|
656
|
+
success=False,
|
|
657
|
+
error=f"Syntax error in new code: {error_msg}",
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
# Validate TOOL_META exists in new code
|
|
661
|
+
new_meta = _extract_tool_meta(python_code)
|
|
662
|
+
if new_meta is None:
|
|
663
|
+
return UniversalConstructorOutput(
|
|
664
|
+
action="update",
|
|
665
|
+
success=False,
|
|
666
|
+
error="New code must contain a valid TOOL_META dictionary",
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
# Validate TOOL_META has required fields
|
|
670
|
+
meta_errors = _validate_tool_meta(new_meta)
|
|
671
|
+
if meta_errors:
|
|
672
|
+
return UniversalConstructorOutput(
|
|
673
|
+
action="update",
|
|
674
|
+
success=False,
|
|
675
|
+
error="Invalid TOOL_META: " + "; ".join(meta_errors),
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
# Write updated code
|
|
679
|
+
source_path_obj.write_text(python_code, encoding="utf-8")
|
|
680
|
+
|
|
681
|
+
# Run ruff format on the updated file
|
|
682
|
+
format_warning = _run_ruff_format(source_path_obj)
|
|
683
|
+
changes = ["Replaced source code"]
|
|
684
|
+
if format_warning:
|
|
685
|
+
changes.append(f"Format warning: {format_warning}")
|
|
686
|
+
else:
|
|
687
|
+
changes.append("Formatted with ruff")
|
|
688
|
+
|
|
689
|
+
# Read formatted code for preview
|
|
690
|
+
formatted_code = source_path_obj.read_text(encoding="utf-8")
|
|
691
|
+
|
|
692
|
+
# Reload registry to pick up changes
|
|
693
|
+
registry.reload()
|
|
694
|
+
|
|
695
|
+
return UniversalConstructorOutput(
|
|
696
|
+
action="update",
|
|
697
|
+
success=True,
|
|
698
|
+
update_result=UCUpdateOutput(
|
|
699
|
+
success=True,
|
|
700
|
+
tool_name=tool_name,
|
|
701
|
+
source_path=source_path,
|
|
702
|
+
preview=_generate_preview(formatted_code),
|
|
703
|
+
changes_applied=changes,
|
|
704
|
+
),
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
except Exception as e:
|
|
708
|
+
return UniversalConstructorOutput(
|
|
709
|
+
action="update",
|
|
710
|
+
success=False,
|
|
711
|
+
error=f"Failed to update tool: {e}",
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _handle_info_action(
|
|
716
|
+
context: RunContext,
|
|
717
|
+
tool_name: Optional[str],
|
|
718
|
+
) -> UniversalConstructorOutput:
|
|
719
|
+
"""Handle the 'info' action - get detailed tool information.
|
|
720
|
+
|
|
721
|
+
Retrieves comprehensive information about a UC tool including its
|
|
722
|
+
metadata, source code, and function signature.
|
|
723
|
+
|
|
724
|
+
Args:
|
|
725
|
+
context: The run context from pydantic-ai
|
|
726
|
+
tool_name: Full name of the tool (including namespace)
|
|
727
|
+
|
|
728
|
+
Returns:
|
|
729
|
+
UniversalConstructorOutput with info_result containing tool details
|
|
730
|
+
"""
|
|
731
|
+
from pathlib import Path
|
|
732
|
+
|
|
733
|
+
from code_puppy.plugins.universal_constructor.registry import get_registry
|
|
734
|
+
|
|
735
|
+
if not tool_name:
|
|
736
|
+
return UniversalConstructorOutput(
|
|
737
|
+
action="info",
|
|
738
|
+
success=False,
|
|
739
|
+
error="tool_name is required for info action",
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
registry = get_registry()
|
|
743
|
+
tool = registry.get_tool(tool_name)
|
|
744
|
+
|
|
745
|
+
if not tool:
|
|
746
|
+
return UniversalConstructorOutput(
|
|
747
|
+
action="info",
|
|
748
|
+
success=False,
|
|
749
|
+
error=f"Tool '{tool_name}' not found",
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
# Read source code from file
|
|
753
|
+
source_code = ""
|
|
754
|
+
source_path = tool.source_path
|
|
755
|
+
source_path_obj = Path(source_path) if source_path else None
|
|
756
|
+
if source_path_obj and source_path_obj.exists():
|
|
757
|
+
try:
|
|
758
|
+
source_code = source_path_obj.read_text(encoding="utf-8")
|
|
759
|
+
except Exception:
|
|
760
|
+
source_code = "[Could not read source]"
|
|
761
|
+
else:
|
|
762
|
+
source_code = "[Source file not found]"
|
|
763
|
+
|
|
764
|
+
return UniversalConstructorOutput(
|
|
765
|
+
action="info",
|
|
766
|
+
success=True,
|
|
767
|
+
info_result=UCInfoOutput(
|
|
768
|
+
success=True,
|
|
769
|
+
tool=tool,
|
|
770
|
+
source_code=source_code,
|
|
771
|
+
),
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def register_universal_constructor(agent):
|
|
776
|
+
"""Register the universal_constructor tool with an agent.
|
|
777
|
+
|
|
778
|
+
Args:
|
|
779
|
+
agent: The pydantic-ai agent to register the tool with
|
|
780
|
+
"""
|
|
781
|
+
|
|
782
|
+
@agent.tool
|
|
783
|
+
async def universal_constructor(
|
|
784
|
+
context: RunContext,
|
|
785
|
+
action: Literal["list", "call", "create", "update", "info"],
|
|
786
|
+
tool_name: Optional[str] = None,
|
|
787
|
+
tool_args: Optional[dict] = None,
|
|
788
|
+
python_code: Optional[str] = None,
|
|
789
|
+
description: Optional[str] = None,
|
|
790
|
+
) -> UniversalConstructorOutput:
|
|
791
|
+
"""Universal Constructor - Your gateway to unlimited capabilities.
|
|
792
|
+
|
|
793
|
+
**YOU CAN BUILD ANYTHING.** The Universal Constructor empowers you to create
|
|
794
|
+
any tool you can imagine using Python. If you can write Python code for it,
|
|
795
|
+
you can make it a permanent, reusable tool. This is your superpower.
|
|
796
|
+
|
|
797
|
+
**CAPABILITIES YOU CAN CREATE:**
|
|
798
|
+
- API integrations (REST, GraphQL, webhooks, any HTTP service)
|
|
799
|
+
- Database connectors (SQL, NoSQL, any data store)
|
|
800
|
+
- File processors (parsers, converters, analyzers for any format)
|
|
801
|
+
- Web scrapers and data extractors
|
|
802
|
+
- Mathematical and scientific computations
|
|
803
|
+
- Text processing and NLP utilities
|
|
804
|
+
- Image and media manipulation (with PIL, etc.)
|
|
805
|
+
- System utilities and automation scripts
|
|
806
|
+
- Network tools (DNS, HTTP, socket operations)
|
|
807
|
+
- Cryptographic functions
|
|
808
|
+
- Data transformation pipelines
|
|
809
|
+
- Custom business logic for any domain
|
|
810
|
+
- Integration with any Python library (requests, pandas, numpy, etc.)
|
|
811
|
+
- Scheduled tasks and background operations
|
|
812
|
+
- Custom validators and formatters
|
|
813
|
+
- Domain-specific calculators
|
|
814
|
+
- ANY Python functionality you need!
|
|
815
|
+
|
|
816
|
+
**WHEN TO USE THIS:**
|
|
817
|
+
- User asks for something your current tools can't do
|
|
818
|
+
- You need to call an external API or service
|
|
819
|
+
- You need custom data processing logic
|
|
820
|
+
- You want to automate a repetitive task
|
|
821
|
+
- You need domain-specific functionality
|
|
822
|
+
- ANY time you think "I wish I had a tool that..."
|
|
823
|
+
|
|
824
|
+
**PHILOSOPHY:** Don't say "I can't do that" - instead, BUILD a tool that can!
|
|
825
|
+
Tools you create persist across sessions, so build once, use forever.
|
|
826
|
+
|
|
827
|
+
Args:
|
|
828
|
+
action: The operation to perform:
|
|
829
|
+
- "list": List all available custom tools with their metadata
|
|
830
|
+
- "call": Execute a specific custom tool with provided arguments
|
|
831
|
+
- "create": Create a new tool from Python code
|
|
832
|
+
- "update": Modify an existing tool's code or metadata
|
|
833
|
+
- "info": Get detailed information about a specific tool
|
|
834
|
+
tool_name: Name of the tool (required for call/update/info actions).
|
|
835
|
+
Supports namespaced format like "namespace.tool_name" for organization.
|
|
836
|
+
tool_args: Dictionary of arguments to pass when calling a tool.
|
|
837
|
+
Only used with action="call".
|
|
838
|
+
python_code: Python source code defining the tool function.
|
|
839
|
+
Required for action="create" and action="update".
|
|
840
|
+
You have access to the FULL Python standard library plus any
|
|
841
|
+
installed packages (requests, etc.).
|
|
842
|
+
description: Human-readable description of what the tool does.
|
|
843
|
+
Used with action="create".
|
|
844
|
+
|
|
845
|
+
Returns:
|
|
846
|
+
UniversalConstructorOutput with action-specific results.
|
|
847
|
+
|
|
848
|
+
Examples:
|
|
849
|
+
# Create an API client tool
|
|
850
|
+
code = '''
|
|
851
|
+
import requests
|
|
852
|
+
TOOL_META = {"name": "weather", "description": "Get weather data"}
|
|
853
|
+
def weather(city: str) -> dict:
|
|
854
|
+
resp = requests.get(f"https://wttr.in/{city}?format=j1")
|
|
855
|
+
return resp.json()
|
|
856
|
+
'''
|
|
857
|
+
universal_constructor(ctx, action="create", python_code=code)
|
|
858
|
+
|
|
859
|
+
# Create a data processor
|
|
860
|
+
code = '''
|
|
861
|
+
import json
|
|
862
|
+
TOOL_META = {"name": "csv_to_json", "description": "Convert CSV to JSON"}
|
|
863
|
+
def csv_to_json(csv_text: str) -> list:
|
|
864
|
+
lines = csv_text.strip().split("\\n")
|
|
865
|
+
headers = lines[0].split(",")
|
|
866
|
+
return [{h: v for h, v in zip(headers, line.split(","))}
|
|
867
|
+
for line in lines[1:]]
|
|
868
|
+
'''
|
|
869
|
+
universal_constructor(ctx, action="create", python_code=code)
|
|
870
|
+
|
|
871
|
+
# Create a utility tool
|
|
872
|
+
code = '''
|
|
873
|
+
import hashlib
|
|
874
|
+
TOOL_META = {"name": "hasher", "description": "Hash strings"}
|
|
875
|
+
def hasher(text: str, algorithm: str = "sha256") -> str:
|
|
876
|
+
h = hashlib.new(algorithm)
|
|
877
|
+
h.update(text.encode())
|
|
878
|
+
return h.hexdigest()
|
|
879
|
+
'''
|
|
880
|
+
universal_constructor(ctx, action="create", python_code=code)
|
|
881
|
+
|
|
882
|
+
Note:
|
|
883
|
+
Tools are stored in ~/.code_puppy/plugins/universal_constructor/ and
|
|
884
|
+
persist forever. Organize with namespaces: "api.weather", "utils.hasher".
|
|
885
|
+
Code is auto-formatted with ruff. Check existing tools with action="list".
|
|
886
|
+
"""
|
|
887
|
+
return await universal_constructor_impl(
|
|
888
|
+
context, action, tool_name, tool_args, python_code, description
|
|
889
|
+
)
|