tsugite-cli 0.3.3__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.
- tsugite/__init__.py +6 -0
- tsugite/agent_composition.py +163 -0
- tsugite/agent_inheritance.py +479 -0
- tsugite/agent_preparation.py +236 -0
- tsugite/agent_runner/__init__.py +45 -0
- tsugite/agent_runner/helpers.py +106 -0
- tsugite/agent_runner/history_integration.py +248 -0
- tsugite/agent_runner/metrics.py +100 -0
- tsugite/agent_runner/runner.py +1879 -0
- tsugite/agent_runner/validation.py +70 -0
- tsugite/agent_utils.py +167 -0
- tsugite/attachments/__init__.py +65 -0
- tsugite/attachments/auto_context.py +199 -0
- tsugite/attachments/base.py +34 -0
- tsugite/attachments/file.py +51 -0
- tsugite/attachments/inline.py +31 -0
- tsugite/attachments/storage.py +178 -0
- tsugite/attachments/url.py +59 -0
- tsugite/attachments/youtube.py +101 -0
- tsugite/benchmark/__init__.py +62 -0
- tsugite/benchmark/config.py +183 -0
- tsugite/benchmark/core.py +292 -0
- tsugite/benchmark/discovery.py +377 -0
- tsugite/benchmark/evaluators.py +671 -0
- tsugite/benchmark/execution.py +657 -0
- tsugite/benchmark/metrics.py +204 -0
- tsugite/benchmark/reports.py +420 -0
- tsugite/benchmark/utils.py +288 -0
- tsugite/builtin_agents/chat-assistant.md +53 -0
- tsugite/builtin_agents/default.md +140 -0
- tsugite/builtin_agents.py +5 -0
- tsugite/cache.py +195 -0
- tsugite/cli/__init__.py +1042 -0
- tsugite/cli/agents.py +148 -0
- tsugite/cli/attachments.py +193 -0
- tsugite/cli/benchmark.py +663 -0
- tsugite/cli/cache.py +113 -0
- tsugite/cli/config.py +272 -0
- tsugite/cli/helpers.py +534 -0
- tsugite/cli/history.py +193 -0
- tsugite/cli/init.py +387 -0
- tsugite/cli/mcp.py +193 -0
- tsugite/cli/tools.py +419 -0
- tsugite/config.py +204 -0
- tsugite/console.py +48 -0
- tsugite/constants.py +21 -0
- tsugite/core/__init__.py +19 -0
- tsugite/core/agent.py +774 -0
- tsugite/core/executor.py +300 -0
- tsugite/core/memory.py +67 -0
- tsugite/core/tools.py +271 -0
- tsugite/docker_cli.py +270 -0
- tsugite/events/__init__.py +55 -0
- tsugite/events/base.py +46 -0
- tsugite/events/bus.py +62 -0
- tsugite/events/events.py +224 -0
- tsugite/exceptions.py +40 -0
- tsugite/history/__init__.py +29 -0
- tsugite/history/index.py +210 -0
- tsugite/history/models.py +106 -0
- tsugite/history/storage.py +157 -0
- tsugite/mcp_client.py +219 -0
- tsugite/mcp_config.py +174 -0
- tsugite/md_agents.py +751 -0
- tsugite/models.py +257 -0
- tsugite/renderer.py +151 -0
- tsugite/shell_tool_config.py +265 -0
- tsugite/templates/assistant.md +14 -0
- tsugite/tools/__init__.py +265 -0
- tsugite/tools/agents.py +312 -0
- tsugite/tools/edit_strategies.py +393 -0
- tsugite/tools/fs.py +329 -0
- tsugite/tools/http.py +239 -0
- tsugite/tools/interactive.py +430 -0
- tsugite/tools/shell.py +129 -0
- tsugite/tools/shell_tools.py +214 -0
- tsugite/tools/tasks.py +339 -0
- tsugite/tsugite.py +7 -0
- tsugite/ui/__init__.py +46 -0
- tsugite/ui/base.py +638 -0
- tsugite/ui/chat.py +265 -0
- tsugite/ui/chat.tcss +92 -0
- tsugite/ui/chat_history.py +286 -0
- tsugite/ui/helpers.py +102 -0
- tsugite/ui/jsonl.py +125 -0
- tsugite/ui/live_template.py +529 -0
- tsugite/ui/plain.py +419 -0
- tsugite/ui/textual_chat.py +642 -0
- tsugite/ui/textual_handler.py +225 -0
- tsugite/ui/widgets/__init__.py +6 -0
- tsugite/ui/widgets/base_scroll_log.py +27 -0
- tsugite/ui/widgets/message_list.py +121 -0
- tsugite/ui/widgets/thought_log.py +80 -0
- tsugite/ui_context.py +90 -0
- tsugite/utils.py +367 -0
- tsugite/xdg.py +104 -0
- tsugite_cli-0.3.3.dist-info/METADATA +325 -0
- tsugite_cli-0.3.3.dist-info/RECORD +101 -0
- tsugite_cli-0.3.3.dist-info/WHEEL +4 -0
- tsugite_cli-0.3.3.dist-info/entry_points.txt +5 -0
- tsugite_cli-0.3.3.dist-info/licenses/LICENSE +235 -0
tsugite/core/executor.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Code execution backends for agents.
|
|
2
|
+
|
|
3
|
+
Provides local execution using Python's exec().
|
|
4
|
+
WARNING: Not secure! Only use for development.
|
|
5
|
+
|
|
6
|
+
Maintains state (variables persist between runs).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
import io
|
|
11
|
+
import pprint
|
|
12
|
+
import sys
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
PPRINT_WIDTH = 100
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ExecutionResult:
|
|
22
|
+
"""Result from code execution."""
|
|
23
|
+
|
|
24
|
+
output: str
|
|
25
|
+
error: Optional[str]
|
|
26
|
+
stdout: str
|
|
27
|
+
stderr: str
|
|
28
|
+
final_answer: Optional[Any] = None
|
|
29
|
+
tools_called: List[str] = field(default_factory=list)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CodeExecutor(ABC):
|
|
33
|
+
"""Abstract interface for code execution.
|
|
34
|
+
|
|
35
|
+
Defines the contract for code executors used by agents.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
async def execute(self, code: str) -> ExecutionResult:
|
|
40
|
+
"""Execute Python code and return results.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
code: Python code to execute
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
ExecutionResult with output, errors, etc.
|
|
47
|
+
"""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
async def send_variables(self, variables: Dict[str, Any]):
|
|
52
|
+
"""Inject variables into execution namespace.
|
|
53
|
+
|
|
54
|
+
This is used for multi-step agents to pass results between steps.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
variables: Dict of {name: value} to make available in code
|
|
58
|
+
"""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class LocalExecutor(CodeExecutor):
|
|
63
|
+
"""Simple local code executor using Python's exec().
|
|
64
|
+
|
|
65
|
+
WARNING: This is NOT secure! Only use for development.
|
|
66
|
+
|
|
67
|
+
Uses Python's built-in exec() function. State persists between
|
|
68
|
+
runs by maintaining a shared namespace dict.
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
executor = LocalExecutor()
|
|
72
|
+
|
|
73
|
+
# First run
|
|
74
|
+
result = await executor.execute("x = 5")
|
|
75
|
+
|
|
76
|
+
# Second run - x still exists!
|
|
77
|
+
result = await executor.execute("print(x + 3)") # Prints: 8
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self):
|
|
81
|
+
"""Initialize executor with empty namespace."""
|
|
82
|
+
self.namespace = {}
|
|
83
|
+
self._final_answer_value = None
|
|
84
|
+
self._tools_called = []
|
|
85
|
+
|
|
86
|
+
# Inject final_answer function into namespace
|
|
87
|
+
def final_answer(value):
|
|
88
|
+
self._final_answer_value = value
|
|
89
|
+
# Don't print here - the UI handler will display it properly via FINAL_ANSWER event
|
|
90
|
+
|
|
91
|
+
self.namespace["final_answer"] = final_answer
|
|
92
|
+
|
|
93
|
+
def _split_code_for_last_expr(self, code: str) -> tuple[str, Optional[str]]:
|
|
94
|
+
"""Split code into setup and last expression if applicable.
|
|
95
|
+
|
|
96
|
+
If the last statement is an expression, return (setup, last_expr).
|
|
97
|
+
Otherwise, return (code, None).
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
code: Python code to analyze
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Tuple of (setup_code, last_expression_or_none)
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
tree = ast.parse(code)
|
|
107
|
+
if not tree.body:
|
|
108
|
+
return (code, None)
|
|
109
|
+
|
|
110
|
+
# Check if last statement is an expression
|
|
111
|
+
last_node = tree.body[-1]
|
|
112
|
+
if not isinstance(last_node, ast.Expr):
|
|
113
|
+
return (code, None)
|
|
114
|
+
|
|
115
|
+
# Split: everything except last statement vs last statement
|
|
116
|
+
if len(tree.body) == 1:
|
|
117
|
+
# Only one statement and it's an expression
|
|
118
|
+
setup_code = ""
|
|
119
|
+
last_expr = ast.unparse(last_node.value) # Unparse the expression itself
|
|
120
|
+
else:
|
|
121
|
+
# Multiple statements - use ast.unparse to reconstruct
|
|
122
|
+
setup_tree = ast.Module(body=tree.body[:-1], type_ignores=[])
|
|
123
|
+
setup_code = ast.unparse(setup_tree)
|
|
124
|
+
last_expr = ast.unparse(last_node.value)
|
|
125
|
+
|
|
126
|
+
return (setup_code, last_expr)
|
|
127
|
+
|
|
128
|
+
except SyntaxError:
|
|
129
|
+
# Code has syntax errors, let exec handle it normally
|
|
130
|
+
return (code, None)
|
|
131
|
+
|
|
132
|
+
def _format_value(self, value: Any) -> str:
|
|
133
|
+
"""Format a value for display.
|
|
134
|
+
|
|
135
|
+
Uses pprint for complex objects, repr for simple ones.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
value: Value to format
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Formatted string representation
|
|
142
|
+
"""
|
|
143
|
+
# For dicts and lists, use pprint for nice formatting
|
|
144
|
+
if isinstance(value, (dict, list, tuple, set)):
|
|
145
|
+
return pprint.pformat(value, width=PPRINT_WIDTH, compact=False)
|
|
146
|
+
else:
|
|
147
|
+
return repr(value)
|
|
148
|
+
|
|
149
|
+
def _check_code_safety(self, code: str) -> Optional[str]:
|
|
150
|
+
"""Check code for anti-patterns before execution.
|
|
151
|
+
|
|
152
|
+
Detects common mistakes where LLMs use built-in Python functions
|
|
153
|
+
instead of the provided tools.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
code: Python code to check
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Error message string if violations found, None if code is safe
|
|
160
|
+
"""
|
|
161
|
+
import re
|
|
162
|
+
|
|
163
|
+
# Check for file operations using open() instead of read_file/write_file
|
|
164
|
+
# Match: word boundary before 'open', then whitespace, then '('
|
|
165
|
+
# This avoids false positives like 'reopen(' or 'is_open()'
|
|
166
|
+
# Pattern explanation:
|
|
167
|
+
# \b - word boundary (not preceded by alphanumeric or _)
|
|
168
|
+
# open - literal 'open'
|
|
169
|
+
# \s* - optional whitespace
|
|
170
|
+
# \( - opening parenthesis
|
|
171
|
+
if re.search(r"\bopen\s*\(", code):
|
|
172
|
+
# Quick check to avoid false positives in strings/comments
|
|
173
|
+
# Remove strings and comments before checking
|
|
174
|
+
# This is a simple heuristic - not perfect but good enough
|
|
175
|
+
code_without_strings = re.sub(r'["\'].*?["\']', "", code) # Remove string contents
|
|
176
|
+
code_without_comments = re.sub(r"#.*$", "", code_without_strings, flags=re.MULTILINE) # Remove comments
|
|
177
|
+
|
|
178
|
+
# Check again after removing strings/comments
|
|
179
|
+
if re.search(r"\bopen\s*\(", code_without_comments):
|
|
180
|
+
return """Code Safety Check Failed: Detected use of 'open()' for file operations.
|
|
181
|
+
|
|
182
|
+
Please use the provided tools instead:
|
|
183
|
+
- read_file(path) - to read file contents
|
|
184
|
+
- write_file(path, content) - to write to files
|
|
185
|
+
|
|
186
|
+
Example:
|
|
187
|
+
# Instead of:
|
|
188
|
+
with open('file.txt') as f:
|
|
189
|
+
content = f.read()
|
|
190
|
+
|
|
191
|
+
# Use:
|
|
192
|
+
content = read_file('file.txt')
|
|
193
|
+
|
|
194
|
+
# Instead of:
|
|
195
|
+
with open('output.txt', 'w') as f:
|
|
196
|
+
f.write(data)
|
|
197
|
+
|
|
198
|
+
# Use:
|
|
199
|
+
write_file('output.txt', data)"""
|
|
200
|
+
|
|
201
|
+
# Code passed all safety checks
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
async def execute(self, code: str) -> ExecutionResult:
|
|
205
|
+
"""Execute code using exec().
|
|
206
|
+
|
|
207
|
+
Automatically displays the value of the last expression (REPL-like behavior).
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
code: Python code to execute
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
ExecutionResult with output, error, stdout, stderr, final_answer, and tools_called
|
|
214
|
+
"""
|
|
215
|
+
# Reset final answer and tool tracking
|
|
216
|
+
self._final_answer_value = None
|
|
217
|
+
self._tools_called = []
|
|
218
|
+
|
|
219
|
+
# Check code safety before execution
|
|
220
|
+
safety_error = self._check_code_safety(code)
|
|
221
|
+
if safety_error:
|
|
222
|
+
return ExecutionResult(
|
|
223
|
+
output="",
|
|
224
|
+
error=safety_error,
|
|
225
|
+
stdout="",
|
|
226
|
+
stderr=safety_error,
|
|
227
|
+
final_answer=None,
|
|
228
|
+
tools_called=[],
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Capture stdout/stderr
|
|
232
|
+
stdout_capture = io.StringIO()
|
|
233
|
+
stderr_capture = io.StringIO()
|
|
234
|
+
|
|
235
|
+
old_stdout = sys.stdout
|
|
236
|
+
old_stderr = sys.stderr
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
# Redirect output
|
|
240
|
+
sys.stdout = stdout_capture
|
|
241
|
+
sys.stderr = stderr_capture
|
|
242
|
+
|
|
243
|
+
# Check if we should handle the last expression specially
|
|
244
|
+
setup_code, last_expr = self._split_code_for_last_expr(code)
|
|
245
|
+
|
|
246
|
+
if last_expr:
|
|
247
|
+
# Execute setup code first (if any)
|
|
248
|
+
if setup_code.strip():
|
|
249
|
+
exec(setup_code, self.namespace)
|
|
250
|
+
|
|
251
|
+
# Evaluate the last expression and capture its value
|
|
252
|
+
result = eval(last_expr, self.namespace)
|
|
253
|
+
|
|
254
|
+
# Display the result if it's not None
|
|
255
|
+
if result is not None:
|
|
256
|
+
formatted = self._format_value(result)
|
|
257
|
+
print(formatted)
|
|
258
|
+
else:
|
|
259
|
+
# No special handling needed, execute normally
|
|
260
|
+
exec(code, self.namespace)
|
|
261
|
+
|
|
262
|
+
# Get output
|
|
263
|
+
output = stdout_capture.getvalue()
|
|
264
|
+
stderr_output = stderr_capture.getvalue()
|
|
265
|
+
|
|
266
|
+
return ExecutionResult(
|
|
267
|
+
output=output,
|
|
268
|
+
error=None,
|
|
269
|
+
stdout=output,
|
|
270
|
+
stderr=stderr_output,
|
|
271
|
+
final_answer=self._final_answer_value,
|
|
272
|
+
tools_called=self._tools_called.copy(),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
except Exception as e:
|
|
276
|
+
# Code execution failed
|
|
277
|
+
error_msg = f"{type(e).__name__}: {str(e)}"
|
|
278
|
+
return ExecutionResult(
|
|
279
|
+
output=stdout_capture.getvalue(),
|
|
280
|
+
error=error_msg,
|
|
281
|
+
stdout=stdout_capture.getvalue(),
|
|
282
|
+
stderr=stderr_capture.getvalue() + "\n" + error_msg,
|
|
283
|
+
final_answer=None,
|
|
284
|
+
tools_called=self._tools_called.copy(),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
finally:
|
|
288
|
+
# Restore stdout/stderr
|
|
289
|
+
sys.stdout = old_stdout
|
|
290
|
+
sys.stderr = old_stderr
|
|
291
|
+
|
|
292
|
+
async def send_variables(self, variables: Dict[str, Any]):
|
|
293
|
+
"""Inject variables into namespace.
|
|
294
|
+
|
|
295
|
+
Simply updates the shared namespace dict.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
variables: Dict of {name: value} to inject
|
|
299
|
+
"""
|
|
300
|
+
self.namespace.update(variables)
|
tsugite/core/memory.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Agent memory system.
|
|
2
|
+
|
|
3
|
+
Tracks execution history for building conversation context.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, List, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class StepResult:
|
|
12
|
+
"""Result from a single agent step."""
|
|
13
|
+
|
|
14
|
+
step_number: int
|
|
15
|
+
thought: str
|
|
16
|
+
code: str
|
|
17
|
+
output: str
|
|
18
|
+
error: Optional[str] = None
|
|
19
|
+
tools_called: List[str] = field(default_factory=list)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class AgentMemory:
|
|
24
|
+
"""Memory of agent execution.
|
|
25
|
+
|
|
26
|
+
Stores:
|
|
27
|
+
- The task
|
|
28
|
+
- All steps (thought/code/observation)
|
|
29
|
+
- Reasoning content (for o1/o3/Claude)
|
|
30
|
+
- Final answer
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
task: str = ""
|
|
34
|
+
steps: List[StepResult] = field(default_factory=list)
|
|
35
|
+
reasoning_history: List[str] = field(default_factory=list)
|
|
36
|
+
final_answer: Optional[Any] = None
|
|
37
|
+
|
|
38
|
+
def add_task(self, task: str) -> None:
|
|
39
|
+
"""Set the task."""
|
|
40
|
+
self.task = task
|
|
41
|
+
|
|
42
|
+
def add_step(
|
|
43
|
+
self,
|
|
44
|
+
thought: str,
|
|
45
|
+
code: str,
|
|
46
|
+
output: str,
|
|
47
|
+
error: Optional[str] = None,
|
|
48
|
+
tools_called: Optional[List[str]] = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Add a step to history."""
|
|
51
|
+
step = StepResult(
|
|
52
|
+
step_number=len(self.steps) + 1,
|
|
53
|
+
thought=thought,
|
|
54
|
+
code=code,
|
|
55
|
+
output=output,
|
|
56
|
+
error=error,
|
|
57
|
+
tools_called=tools_called or [],
|
|
58
|
+
)
|
|
59
|
+
self.steps.append(step)
|
|
60
|
+
|
|
61
|
+
def add_reasoning(self, reasoning: str) -> None:
|
|
62
|
+
"""Add reasoning content (from o1/o3/Claude thinking)."""
|
|
63
|
+
self.reasoning_history.append(reasoning)
|
|
64
|
+
|
|
65
|
+
def add_final_answer(self, answer: Any) -> None:
|
|
66
|
+
"""Set final answer."""
|
|
67
|
+
self.final_answer = answer
|
tsugite/core/tools.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Tool system for agents.
|
|
2
|
+
|
|
3
|
+
Provides a simple Tool class and converters to wrap:
|
|
4
|
+
- Existing tsugite tools
|
|
5
|
+
- MCP tools
|
|
6
|
+
- Custom functions
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import inspect
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Callable, Dict, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Tool:
|
|
17
|
+
"""A tool that agents can use.
|
|
18
|
+
|
|
19
|
+
Tools are Python functions with:
|
|
20
|
+
- name: Identifier
|
|
21
|
+
- description: What it does
|
|
22
|
+
- parameters: JSON schema of arguments
|
|
23
|
+
- function: The actual callable
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
def add(a: int, b: int) -> int:
|
|
27
|
+
'''Add two numbers'''
|
|
28
|
+
return a + b
|
|
29
|
+
|
|
30
|
+
tool = Tool(
|
|
31
|
+
name="add",
|
|
32
|
+
description="Add two numbers",
|
|
33
|
+
parameters={
|
|
34
|
+
"type": "object",
|
|
35
|
+
"properties": {
|
|
36
|
+
"a": {"type": "integer"},
|
|
37
|
+
"b": {"type": "integer"}
|
|
38
|
+
},
|
|
39
|
+
"required": ["a", "b"]
|
|
40
|
+
},
|
|
41
|
+
function=add
|
|
42
|
+
)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
name: str
|
|
46
|
+
description: str
|
|
47
|
+
parameters: Dict[str, Any]
|
|
48
|
+
function: Callable
|
|
49
|
+
|
|
50
|
+
def to_code_prompt(self) -> str:
|
|
51
|
+
"""Format tool as Python function for system prompt.
|
|
52
|
+
|
|
53
|
+
The agent sees tools as Python functions it can call.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
str: Python function signature with docstring
|
|
57
|
+
|
|
58
|
+
Example output:
|
|
59
|
+
def add(a: int, b: int) -> Any:
|
|
60
|
+
'''Add two numbers
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
a: First number
|
|
64
|
+
b: Second number
|
|
65
|
+
'''
|
|
66
|
+
pass
|
|
67
|
+
"""
|
|
68
|
+
# Extract parameter info from JSON schema
|
|
69
|
+
props = self.parameters.get("properties", {})
|
|
70
|
+
required = self.parameters.get("required", [])
|
|
71
|
+
|
|
72
|
+
# Build function signature
|
|
73
|
+
params = []
|
|
74
|
+
for param_name, param_info in props.items():
|
|
75
|
+
# Get type (default to Any)
|
|
76
|
+
param_type = param_info.get("type", "Any")
|
|
77
|
+
|
|
78
|
+
# Convert JSON schema types to Python types
|
|
79
|
+
type_map = {
|
|
80
|
+
"string": "str",
|
|
81
|
+
"integer": "int",
|
|
82
|
+
"number": "float",
|
|
83
|
+
"boolean": "bool",
|
|
84
|
+
"array": "list",
|
|
85
|
+
"object": "dict",
|
|
86
|
+
}
|
|
87
|
+
python_type = type_map.get(param_type, "Any")
|
|
88
|
+
|
|
89
|
+
# Add to params list
|
|
90
|
+
params.append(f"{param_name}: {python_type}")
|
|
91
|
+
|
|
92
|
+
# Add * to indicate keyword-only parameters
|
|
93
|
+
param_str = f"*, {', '.join(params)}" if params else ""
|
|
94
|
+
|
|
95
|
+
# Build docstring with parameter descriptions
|
|
96
|
+
param_docs = []
|
|
97
|
+
for param_name, param_info in props.items():
|
|
98
|
+
desc = param_info.get("description", "")
|
|
99
|
+
required_marker = " (required)" if param_name in required else ""
|
|
100
|
+
param_docs.append(f" {param_name}: {desc}{required_marker}")
|
|
101
|
+
|
|
102
|
+
param_doc_str = "\n".join(param_docs) if param_docs else " No parameters"
|
|
103
|
+
|
|
104
|
+
# Build usage example with keyword arguments
|
|
105
|
+
example_args = []
|
|
106
|
+
for param_name, param_info in props.items():
|
|
107
|
+
# Create example values based on type
|
|
108
|
+
param_type = param_info.get("type", "string")
|
|
109
|
+
if param_type == "string":
|
|
110
|
+
example_value = f'"{param_name}_value"'
|
|
111
|
+
elif param_type == "integer":
|
|
112
|
+
example_value = "42"
|
|
113
|
+
elif param_type == "number":
|
|
114
|
+
example_value = "3.14"
|
|
115
|
+
elif param_type == "boolean":
|
|
116
|
+
example_value = "True"
|
|
117
|
+
elif param_type == "array":
|
|
118
|
+
example_value = '["item1", "item2"]'
|
|
119
|
+
elif param_type == "object":
|
|
120
|
+
example_value = '{"key": "value"}'
|
|
121
|
+
else:
|
|
122
|
+
example_value = "value"
|
|
123
|
+
|
|
124
|
+
example_args.append(f"{param_name}={example_value}")
|
|
125
|
+
|
|
126
|
+
usage_example = (
|
|
127
|
+
f"result = {self.name}({', '.join(example_args)})" if example_args else f"result = {self.name}()"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Build full function definition
|
|
131
|
+
return f'''def {self.name}({param_str}) -> Any:
|
|
132
|
+
"""{self.description}
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
{param_doc_str}
|
|
136
|
+
|
|
137
|
+
Usage:
|
|
138
|
+
{usage_example}
|
|
139
|
+
"""
|
|
140
|
+
pass
|
|
141
|
+
'''
|
|
142
|
+
|
|
143
|
+
async def execute(self, **kwargs) -> Any:
|
|
144
|
+
"""Execute the tool with given arguments.
|
|
145
|
+
|
|
146
|
+
Handles both sync and async functions.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
**kwargs: Arguments to pass to the tool
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Tool execution result
|
|
153
|
+
"""
|
|
154
|
+
# Check if function is async
|
|
155
|
+
if asyncio.iscoroutinefunction(self.function):
|
|
156
|
+
return await self.function(**kwargs)
|
|
157
|
+
else:
|
|
158
|
+
# Run sync function
|
|
159
|
+
return self.function(**kwargs)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def create_tool_from_function(func: Callable, name: Optional[str] = None, description: Optional[str] = None) -> Tool:
|
|
163
|
+
"""Create a Tool from a Python function.
|
|
164
|
+
|
|
165
|
+
Extracts parameter info from function signature and docstring.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
func: The function to wrap
|
|
169
|
+
name: Tool name (defaults to function name)
|
|
170
|
+
description: Tool description (defaults to docstring)
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Tool: Wrapped function
|
|
174
|
+
|
|
175
|
+
Example:
|
|
176
|
+
def multiply(a: int, b: int) -> int:
|
|
177
|
+
'''Multiply two numbers'''
|
|
178
|
+
return a * b
|
|
179
|
+
|
|
180
|
+
tool = create_tool_from_function(multiply)
|
|
181
|
+
"""
|
|
182
|
+
# Get name and description
|
|
183
|
+
tool_name = name or func.__name__
|
|
184
|
+
tool_description = description or (func.__doc__ or "").strip().split("\n")[0]
|
|
185
|
+
|
|
186
|
+
# Extract parameters from signature
|
|
187
|
+
sig = inspect.signature(func)
|
|
188
|
+
parameters = {"type": "object", "properties": {}, "required": []}
|
|
189
|
+
|
|
190
|
+
for param_name, param in sig.parameters.items():
|
|
191
|
+
# Skip self/cls
|
|
192
|
+
if param_name in ("self", "cls"):
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
# Get type annotation and convert to JSON schema type
|
|
196
|
+
if param.annotation != inspect.Parameter.empty:
|
|
197
|
+
# Convert Python type to JSON schema type
|
|
198
|
+
type_map = {
|
|
199
|
+
str: "string",
|
|
200
|
+
int: "integer",
|
|
201
|
+
float: "number",
|
|
202
|
+
bool: "boolean",
|
|
203
|
+
list: "array",
|
|
204
|
+
dict: "object",
|
|
205
|
+
}
|
|
206
|
+
param_type = type_map.get(param.annotation)
|
|
207
|
+
|
|
208
|
+
if param_type:
|
|
209
|
+
# Known type - add type constraint
|
|
210
|
+
parameters["properties"][param_name] = {"type": param_type}
|
|
211
|
+
else:
|
|
212
|
+
# Unknown type - omit type constraint (accepts any value, but valid JSON Schema)
|
|
213
|
+
parameters["properties"][param_name] = {}
|
|
214
|
+
else:
|
|
215
|
+
# No annotation - omit type constraint
|
|
216
|
+
parameters["properties"][param_name] = {}
|
|
217
|
+
|
|
218
|
+
# Check if required (no default value)
|
|
219
|
+
if param.default == inspect.Parameter.empty:
|
|
220
|
+
parameters["required"].append(param_name)
|
|
221
|
+
|
|
222
|
+
return Tool(
|
|
223
|
+
name=tool_name,
|
|
224
|
+
description=tool_description,
|
|
225
|
+
parameters=parameters,
|
|
226
|
+
function=func,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def create_tool_from_tsugite(tool_name: str) -> Tool:
|
|
231
|
+
"""Convert existing tsugite tool to Tool object.
|
|
232
|
+
|
|
233
|
+
Tsugite has its own tool registry. This function wraps those
|
|
234
|
+
tools in our Tool interface.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
tool_name: Name of tool in tsugite registry
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Tool: Wrapped tsugite tool
|
|
241
|
+
|
|
242
|
+
Example:
|
|
243
|
+
tool = create_tool_from_tsugite("read_file")
|
|
244
|
+
result = await tool.execute(file_path="/path/to/file")
|
|
245
|
+
"""
|
|
246
|
+
from tsugite.tools import call_tool, get_tool
|
|
247
|
+
|
|
248
|
+
# Get tool info from registry
|
|
249
|
+
tool_info = get_tool(tool_name)
|
|
250
|
+
|
|
251
|
+
# Create async wrapper function that preserves signature
|
|
252
|
+
sig = inspect.signature(tool_info.func)
|
|
253
|
+
|
|
254
|
+
async def tool_wrapper(**kwargs):
|
|
255
|
+
"""Wrapper that calls tsugite tool."""
|
|
256
|
+
# call_tool might be sync or async, handle both
|
|
257
|
+
result = call_tool(tool_name, **kwargs)
|
|
258
|
+
# If result is a coroutine, await it
|
|
259
|
+
if inspect.iscoroutine(result):
|
|
260
|
+
return await result
|
|
261
|
+
return result
|
|
262
|
+
|
|
263
|
+
# Set the signature on the wrapper
|
|
264
|
+
tool_wrapper.__signature__ = sig
|
|
265
|
+
|
|
266
|
+
# Create Tool using function converter
|
|
267
|
+
return create_tool_from_function(
|
|
268
|
+
tool_wrapper,
|
|
269
|
+
name=tool_name,
|
|
270
|
+
description=tool_info.description,
|
|
271
|
+
)
|