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/models.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Model adapters for Tsugite agents."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def resolve_model_alias(model_string: str) -> str:
|
|
9
|
+
"""Resolve a model alias to its full model string.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
model_string: Either an alias name or full model string
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Full model string (resolved alias or original string)
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
>>> resolve_model_alias("cheap") # if alias exists
|
|
19
|
+
"openai:gpt-4o-mini"
|
|
20
|
+
>>> resolve_model_alias("ollama:qwen2.5-coder:7b")
|
|
21
|
+
"ollama:qwen2.5-coder:7b"
|
|
22
|
+
"""
|
|
23
|
+
from tsugite.config import get_model_alias
|
|
24
|
+
|
|
25
|
+
if ":" in model_string:
|
|
26
|
+
return model_string
|
|
27
|
+
|
|
28
|
+
alias_value = get_model_alias(model_string)
|
|
29
|
+
if alias_value:
|
|
30
|
+
return alias_value
|
|
31
|
+
|
|
32
|
+
return model_string
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_model_string(model_string: str) -> tuple[str, str, Optional[str]]:
|
|
36
|
+
"""Parse Tsugite model string format.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
model_string: Format like "ollama:qwen2.5-coder:14b" or "openai:gpt-4"
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Tuple of (provider, model_name, variant)
|
|
43
|
+
"""
|
|
44
|
+
parts = model_string.split(":")
|
|
45
|
+
if len(parts) < 2:
|
|
46
|
+
raise ValueError(f"Invalid model string format: {model_string}")
|
|
47
|
+
|
|
48
|
+
provider = parts[0]
|
|
49
|
+
model_name = parts[1]
|
|
50
|
+
variant = parts[2] if len(parts) > 2 else None
|
|
51
|
+
|
|
52
|
+
return provider, model_name, variant
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def is_reasoning_model_without_stop_support(model_string: str) -> bool:
|
|
56
|
+
"""Check if model is a reasoning model that doesn't support stop sequences.
|
|
57
|
+
|
|
58
|
+
OpenAI's o1/o3 reasoning models don't support the stop parameter.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
model_string: Full model string like "openai:o1-mini"
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
True if this is a reasoning model that doesn't support stop sequences
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
provider, model_name, variant = parse_model_string(model_string)
|
|
68
|
+
except ValueError:
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
# Only OpenAI reasoning models have this limitation
|
|
72
|
+
if provider != "openai":
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
# Check if it's an o1 or o3 model (with optional version suffix)
|
|
76
|
+
# Matches: o1, o1-mini, o1-preview, o1-2024-12-17, o3, o3-mini, o3-2025-01-31, etc.
|
|
77
|
+
pattern = r"^(o1|o3)(-mini|-preview)?(-\d{4}-\d{2}-\d{2})?$"
|
|
78
|
+
return bool(re.match(pattern, model_name))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_model_params(model_string: str, **kwargs) -> dict:
|
|
82
|
+
"""Get parameters for direct litellm.acompletion() calls.
|
|
83
|
+
|
|
84
|
+
Returns a dict with model ID and parameters for direct LiteLLM usage.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
model_string: Model specification like "openai:gpt-4o-mini" or an alias
|
|
88
|
+
**kwargs: Additional model parameters (reasoning_effort, response_format, temperature, etc.)
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Dict with "model" key and all parameters ready for litellm.acompletion()
|
|
92
|
+
|
|
93
|
+
Examples:
|
|
94
|
+
>>> params = get_model_params("openai:gpt-4o-mini", temperature=0.7)
|
|
95
|
+
>>> params["model"]
|
|
96
|
+
'openai/gpt-4o-mini'
|
|
97
|
+
>>> params["temperature"]
|
|
98
|
+
0.7
|
|
99
|
+
|
|
100
|
+
>>> # Reasoning models - unsupported params filtered out
|
|
101
|
+
>>> params = get_model_params("openai:o1", temperature=0.7)
|
|
102
|
+
>>> "temperature" not in params # Filtered for o1
|
|
103
|
+
True
|
|
104
|
+
"""
|
|
105
|
+
# Resolve aliases and parse model string
|
|
106
|
+
resolved_model = resolve_model_alias(model_string)
|
|
107
|
+
provider, model_name, variant = parse_model_string(resolved_model)
|
|
108
|
+
|
|
109
|
+
# Build parameters dict
|
|
110
|
+
params = dict(kwargs)
|
|
111
|
+
|
|
112
|
+
# Filter parameters for reasoning models
|
|
113
|
+
if is_reasoning_model_without_stop_support(resolved_model):
|
|
114
|
+
params = filter_reasoning_model_params(model_name, params)
|
|
115
|
+
|
|
116
|
+
# Build provider-specific parameters
|
|
117
|
+
if provider == "ollama":
|
|
118
|
+
return build_ollama_params(model_name, variant, params)
|
|
119
|
+
elif provider == "openai":
|
|
120
|
+
return build_openai_params(model_name, params)
|
|
121
|
+
elif provider == "anthropic":
|
|
122
|
+
return build_anthropic_params(model_name, params)
|
|
123
|
+
elif provider == "google":
|
|
124
|
+
return build_google_params(model_name, params)
|
|
125
|
+
elif provider == "github_copilot":
|
|
126
|
+
return build_github_copilot_params(model_name, params)
|
|
127
|
+
else:
|
|
128
|
+
return build_fallback_params(provider, model_name, params)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def filter_reasoning_model_params(model_name: str, params: dict) -> dict:
|
|
132
|
+
"""Filter out unsupported parameters for reasoning models.
|
|
133
|
+
|
|
134
|
+
OpenAI's o1/o3 reasoning models don't support certain parameters.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
model_name: The model name (e.g., "o1", "o1-mini")
|
|
138
|
+
params: Dictionary of parameters
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Filtered parameters dict (modifies in-place and returns)
|
|
142
|
+
"""
|
|
143
|
+
unsupported_params = ["stop", "temperature", "top_p", "presence_penalty", "frequency_penalty"]
|
|
144
|
+
|
|
145
|
+
# o1-mini specifically doesn't support reasoning_effort
|
|
146
|
+
if "o1-mini" in model_name:
|
|
147
|
+
unsupported_params.append("reasoning_effort")
|
|
148
|
+
|
|
149
|
+
# Remove unsupported parameters
|
|
150
|
+
for param in unsupported_params:
|
|
151
|
+
params.pop(param, None)
|
|
152
|
+
|
|
153
|
+
return params
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def build_ollama_params(model_name: str, variant: str | None, params: dict) -> dict:
|
|
157
|
+
"""Build parameters for Ollama provider.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
model_name: The model name
|
|
161
|
+
variant: Optional model variant
|
|
162
|
+
params: Base parameters dict
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Updated parameters dict
|
|
166
|
+
"""
|
|
167
|
+
full_model_name = f"{model_name}:{variant}" if variant else model_name
|
|
168
|
+
params["model"] = full_model_name
|
|
169
|
+
params.setdefault("api_base", os.getenv("OLLAMA_BASE_URL", "http://localhost:11434/v1"))
|
|
170
|
+
params.setdefault("api_key", "ollama")
|
|
171
|
+
return params
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def build_openai_params(model_name: str, params: dict) -> dict:
|
|
175
|
+
"""Build parameters for OpenAI provider.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
model_name: The model name
|
|
179
|
+
params: Base parameters dict
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Updated parameters dict
|
|
183
|
+
"""
|
|
184
|
+
params["model"] = f"openai/{model_name}"
|
|
185
|
+
if "api_key" not in params:
|
|
186
|
+
params["api_key"] = os.getenv("OPENAI_API_KEY")
|
|
187
|
+
return params
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def build_anthropic_params(model_name: str, params: dict) -> dict:
|
|
191
|
+
"""Build parameters for Anthropic provider.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
model_name: The model name
|
|
195
|
+
params: Base parameters dict
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Updated parameters dict
|
|
199
|
+
"""
|
|
200
|
+
params["model"] = f"anthropic/{model_name}"
|
|
201
|
+
if "api_key" not in params:
|
|
202
|
+
params["api_key"] = os.getenv("ANTHROPIC_API_KEY")
|
|
203
|
+
return params
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def build_google_params(model_name: str, params: dict) -> dict:
|
|
207
|
+
"""Build parameters for Google provider.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
model_name: The model name
|
|
211
|
+
params: Base parameters dict
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Updated parameters dict
|
|
215
|
+
"""
|
|
216
|
+
params["model"] = f"gemini/{model_name}"
|
|
217
|
+
if "api_key" not in params:
|
|
218
|
+
params["api_key"] = os.getenv("GOOGLE_API_KEY")
|
|
219
|
+
return params
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def build_github_copilot_params(model_name: str, params: dict) -> dict:
|
|
223
|
+
"""Build parameters for GitHub Copilot provider.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
model_name: The model name
|
|
227
|
+
params: Base parameters dict
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Updated parameters dict
|
|
231
|
+
"""
|
|
232
|
+
params["model"] = f"github_copilot/{model_name}"
|
|
233
|
+
|
|
234
|
+
# GitHub Copilot requires specific headers
|
|
235
|
+
extra_headers = params.get("extra_headers", {})
|
|
236
|
+
if "editor-version" not in extra_headers:
|
|
237
|
+
extra_headers["editor-version"] = "vscode/1.95.0"
|
|
238
|
+
if "Copilot-Integration-Id" not in extra_headers:
|
|
239
|
+
extra_headers["Copilot-Integration-Id"] = "vscode-chat"
|
|
240
|
+
params["extra_headers"] = extra_headers
|
|
241
|
+
|
|
242
|
+
return params
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def build_fallback_params(provider: str, model_name: str, params: dict) -> dict:
|
|
246
|
+
"""Build parameters for fallback/unknown providers.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
provider: The provider name
|
|
250
|
+
model_name: The model name
|
|
251
|
+
params: Base parameters dict
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Updated parameters dict
|
|
255
|
+
"""
|
|
256
|
+
params["model"] = f"{provider}/{model_name}"
|
|
257
|
+
return params
|
tsugite/renderer.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Jinja2 template rendering for agent content."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
from jinja2 import DictLoader, Environment, StrictUndefined
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def now() -> str:
|
|
12
|
+
return datetime.now().isoformat()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def today() -> str:
|
|
16
|
+
return datetime.now().strftime("%Y-%m-%d")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def slugify(text: str) -> str:
|
|
20
|
+
import re
|
|
21
|
+
|
|
22
|
+
text = text.lower()
|
|
23
|
+
# Replace special characters with dashes, then keep only ASCII letters, numbers, and dashes
|
|
24
|
+
text = re.sub(r"[^\w\s-]", "-", text, flags=re.ASCII)
|
|
25
|
+
text = re.sub(r"[-\s]+", "-", text)
|
|
26
|
+
return text.strip("-")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def file_exists(path: str) -> bool:
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
return Path(path).exists()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_file(path: str) -> bool:
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
p = Path(path)
|
|
39
|
+
return p.exists() and p.is_file()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def is_dir(path: str) -> bool:
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
|
|
45
|
+
p = Path(path)
|
|
46
|
+
return p.exists() and p.is_dir()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def read_text(path: str, default: str = "") -> str:
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
return Path(path).read_text(encoding="utf-8")
|
|
54
|
+
except Exception:
|
|
55
|
+
return default
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def strip_ignored_sections(content: str) -> str:
|
|
59
|
+
"""Remove <!-- tsu:ignore --> blocks from content.
|
|
60
|
+
|
|
61
|
+
Supports both block and inline forms:
|
|
62
|
+
- Block: <!-- tsu:ignore -->\\ncontent\\n<!-- /tsu:ignore -->
|
|
63
|
+
- Inline: <!-- tsu:ignore -->content<!-- /tsu:ignore -->
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
content: Raw markdown content
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Content with ignored sections removed
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
>>> content = '''
|
|
73
|
+
... Normal content
|
|
74
|
+
... <!-- tsu:ignore -->
|
|
75
|
+
... This is ignored
|
|
76
|
+
... <!-- /tsu:ignore -->
|
|
77
|
+
... More content
|
|
78
|
+
... '''
|
|
79
|
+
>>> result = strip_ignored_sections(content)
|
|
80
|
+
>>> 'This is ignored' not in result
|
|
81
|
+
True
|
|
82
|
+
"""
|
|
83
|
+
# Pattern matches opening tag, content (non-greedy), closing tag
|
|
84
|
+
# Handles both <!-- tsu:ignore --> and <!--tsu:ignore--> (with/without spaces)
|
|
85
|
+
# Uses non-greedy match (.*?) to handle multiple blocks correctly
|
|
86
|
+
# DOTALL flag allows matching across newlines
|
|
87
|
+
pattern = r"<!--\s*tsu:ignore\s*-->.*?<!--\s*/tsu:ignore\s*-->"
|
|
88
|
+
|
|
89
|
+
# Remove all ignore blocks
|
|
90
|
+
result = re.sub(pattern, "", content, flags=re.DOTALL)
|
|
91
|
+
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class AgentRenderer:
|
|
96
|
+
"""Jinja2 template renderer for agent content."""
|
|
97
|
+
|
|
98
|
+
def __init__(self):
|
|
99
|
+
self.env = Environment(
|
|
100
|
+
loader=DictLoader({}),
|
|
101
|
+
undefined=StrictUndefined,
|
|
102
|
+
trim_blocks=True,
|
|
103
|
+
lstrip_blocks=True,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Add helper functions
|
|
107
|
+
self.env.globals.update(
|
|
108
|
+
{
|
|
109
|
+
"now": now,
|
|
110
|
+
"today": today,
|
|
111
|
+
"slugify": slugify,
|
|
112
|
+
"file_exists": file_exists,
|
|
113
|
+
"is_file": is_file,
|
|
114
|
+
"is_dir": is_dir,
|
|
115
|
+
"read_text": read_text,
|
|
116
|
+
"env": dict(os.environ),
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Add filters
|
|
121
|
+
self.env.filters["slugify"] = slugify
|
|
122
|
+
|
|
123
|
+
def render(self, content: str, context: Dict[str, Any] = None) -> str:
|
|
124
|
+
"""Render agent content with Jinja2.
|
|
125
|
+
|
|
126
|
+
Preprocessing steps:
|
|
127
|
+
1. Strip <!-- tsu:ignore --> blocks
|
|
128
|
+
2. Render Jinja2 template with context
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
content: Raw markdown content
|
|
132
|
+
context: Template variables
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Rendered content
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
ValueError: If template rendering fails
|
|
139
|
+
"""
|
|
140
|
+
if context is None:
|
|
141
|
+
context = {}
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
# Step 1: Strip ignored sections BEFORE rendering
|
|
145
|
+
preprocessed = strip_ignored_sections(content)
|
|
146
|
+
|
|
147
|
+
# Step 2: Render Jinja2 template
|
|
148
|
+
template = self.env.from_string(preprocessed)
|
|
149
|
+
return template.render(**context)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
raise ValueError(f"Template rendering failed: {e}") from e
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Configuration loader for custom shell tools."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from .tools.shell_tools import ShellToolDefinition, ShellToolParameter
|
|
9
|
+
from .xdg import get_xdg_config_path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_custom_tools_config_path() -> Path:
|
|
13
|
+
"""Get the path to custom_tools.yaml config file."""
|
|
14
|
+
return get_xdg_config_path("custom_tools.yaml")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_custom_tools_config(path: Optional[Path] = None) -> List[ShellToolDefinition]:
|
|
18
|
+
"""Load custom tool definitions from YAML config.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
path: Path to custom_tools.yaml. If None, uses default XDG path.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
List of ShellToolDefinition objects
|
|
25
|
+
|
|
26
|
+
Example YAML:
|
|
27
|
+
tools:
|
|
28
|
+
- name: file_search
|
|
29
|
+
description: Search files with ripgrep
|
|
30
|
+
command: "rg {pattern} {path}"
|
|
31
|
+
timeout: 30
|
|
32
|
+
parameters:
|
|
33
|
+
pattern:
|
|
34
|
+
type: str
|
|
35
|
+
description: Search pattern
|
|
36
|
+
required: true
|
|
37
|
+
path:
|
|
38
|
+
type: str
|
|
39
|
+
description: Directory to search
|
|
40
|
+
default: "."
|
|
41
|
+
"""
|
|
42
|
+
if path is None:
|
|
43
|
+
path = get_custom_tools_config_path()
|
|
44
|
+
|
|
45
|
+
if not path.exists():
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
50
|
+
config = yaml.safe_load(f)
|
|
51
|
+
|
|
52
|
+
if not config or "tools" not in config:
|
|
53
|
+
return []
|
|
54
|
+
|
|
55
|
+
definitions = []
|
|
56
|
+
for tool_def in config["tools"]:
|
|
57
|
+
# Parse parameters
|
|
58
|
+
parameters = {}
|
|
59
|
+
for param_name, param_config in tool_def.get("parameters", {}).items():
|
|
60
|
+
# Handle multiple formats
|
|
61
|
+
if isinstance(param_config, dict):
|
|
62
|
+
# Full dict format
|
|
63
|
+
param_type = param_config.get("type", "str")
|
|
64
|
+
description = param_config.get("description", "")
|
|
65
|
+
required = param_config.get("required", False)
|
|
66
|
+
default = param_config.get("default")
|
|
67
|
+
flag = param_config.get("flag")
|
|
68
|
+
elif isinstance(param_config, str):
|
|
69
|
+
# Simple string format - assume it's the type
|
|
70
|
+
param_type = param_config if param_config else "str"
|
|
71
|
+
description = ""
|
|
72
|
+
required = False
|
|
73
|
+
default = None
|
|
74
|
+
flag = None
|
|
75
|
+
elif param_config is None:
|
|
76
|
+
# Just parameter name, no config
|
|
77
|
+
param_type = "str"
|
|
78
|
+
description = ""
|
|
79
|
+
required = False
|
|
80
|
+
default = None
|
|
81
|
+
flag = None
|
|
82
|
+
else:
|
|
83
|
+
# Value provided - infer type and use as default
|
|
84
|
+
if isinstance(param_config, bool):
|
|
85
|
+
param_type = "bool"
|
|
86
|
+
default = param_config
|
|
87
|
+
elif isinstance(param_config, int):
|
|
88
|
+
param_type = "int"
|
|
89
|
+
default = param_config
|
|
90
|
+
elif isinstance(param_config, float):
|
|
91
|
+
param_type = "float"
|
|
92
|
+
default = param_config
|
|
93
|
+
else:
|
|
94
|
+
param_type = "str"
|
|
95
|
+
default = str(param_config)
|
|
96
|
+
description = ""
|
|
97
|
+
required = False
|
|
98
|
+
flag = None
|
|
99
|
+
|
|
100
|
+
parameters[param_name] = ShellToolParameter(
|
|
101
|
+
name=param_name,
|
|
102
|
+
type=param_type,
|
|
103
|
+
description=description,
|
|
104
|
+
required=required,
|
|
105
|
+
default=default,
|
|
106
|
+
flag=flag,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Create definition
|
|
110
|
+
definition = ShellToolDefinition(
|
|
111
|
+
name=tool_def["name"],
|
|
112
|
+
description=tool_def.get("description", ""),
|
|
113
|
+
command=tool_def["command"],
|
|
114
|
+
parameters=parameters,
|
|
115
|
+
timeout=tool_def.get("timeout", 30),
|
|
116
|
+
safe_mode=tool_def.get("safe_mode", True),
|
|
117
|
+
shell=tool_def.get("shell", True),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
definitions.append(definition)
|
|
121
|
+
|
|
122
|
+
return definitions
|
|
123
|
+
|
|
124
|
+
except yaml.YAMLError as e:
|
|
125
|
+
raise ValueError(f"Failed to parse custom tools config: {e}") from e
|
|
126
|
+
except Exception as e:
|
|
127
|
+
raise RuntimeError(f"Failed to load custom tools config: {e}") from e
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def save_custom_tools_config(definitions: List[ShellToolDefinition], path: Optional[Path] = None) -> None:
|
|
131
|
+
"""Save custom tool definitions to YAML config.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
definitions: List of tool definitions to save
|
|
135
|
+
path: Path to custom_tools.yaml. If None, uses default XDG path.
|
|
136
|
+
"""
|
|
137
|
+
if path is None:
|
|
138
|
+
path = get_custom_tools_config_path()
|
|
139
|
+
|
|
140
|
+
# Ensure parent directory exists
|
|
141
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
142
|
+
|
|
143
|
+
# Convert definitions to YAML-friendly format
|
|
144
|
+
tools_config = []
|
|
145
|
+
for definition in definitions:
|
|
146
|
+
tool_dict = {
|
|
147
|
+
"name": definition.name,
|
|
148
|
+
"description": definition.description,
|
|
149
|
+
"command": definition.command,
|
|
150
|
+
"timeout": definition.timeout,
|
|
151
|
+
"safe_mode": definition.safe_mode,
|
|
152
|
+
"shell": definition.shell,
|
|
153
|
+
"parameters": {},
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# Convert parameters
|
|
157
|
+
for param_name, param_def in definition.parameters.items():
|
|
158
|
+
tool_dict["parameters"][param_name] = {
|
|
159
|
+
"type": param_def.type,
|
|
160
|
+
"description": param_def.description,
|
|
161
|
+
"required": param_def.required,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if param_def.default is not None:
|
|
165
|
+
tool_dict["parameters"][param_name]["default"] = param_def.default
|
|
166
|
+
|
|
167
|
+
if param_def.flag:
|
|
168
|
+
tool_dict["parameters"][param_name]["flag"] = param_def.flag
|
|
169
|
+
|
|
170
|
+
tools_config.append(tool_dict)
|
|
171
|
+
|
|
172
|
+
config = {"tools": tools_config}
|
|
173
|
+
|
|
174
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
175
|
+
yaml.safe_dump(config, f, default_flow_style=False, sort_keys=False)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def parse_tool_definition_from_dict(tool_dict: Dict) -> ShellToolDefinition:
|
|
179
|
+
"""Parse a tool definition from a dictionary (e.g., from agent frontmatter).
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
tool_dict: Dictionary with tool configuration
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
ShellToolDefinition object
|
|
186
|
+
|
|
187
|
+
Example:
|
|
188
|
+
{
|
|
189
|
+
"name": "file_search",
|
|
190
|
+
"command": "rg {pattern} {path}",
|
|
191
|
+
"parameters": {
|
|
192
|
+
"pattern": {"type": "str", "required": true},
|
|
193
|
+
"path": ".", # Infer type from default value
|
|
194
|
+
"recursive": true # Infer bool from value
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
"""
|
|
198
|
+
parameters = {}
|
|
199
|
+
for param_name, param_config in tool_dict.get("parameters", {}).items():
|
|
200
|
+
if isinstance(param_config, dict):
|
|
201
|
+
# Full dict format
|
|
202
|
+
param_type = param_config.get("type", "str")
|
|
203
|
+
description = param_config.get("description", "")
|
|
204
|
+
required = param_config.get("required", False)
|
|
205
|
+
default = param_config.get("default")
|
|
206
|
+
flag = param_config.get("flag")
|
|
207
|
+
elif isinstance(param_config, str):
|
|
208
|
+
# String - assume it's either type name or default value
|
|
209
|
+
# If it looks like a type name (str, int, bool, float), use it as type
|
|
210
|
+
if param_config in ("str", "int", "bool", "float"):
|
|
211
|
+
param_type = param_config
|
|
212
|
+
description = ""
|
|
213
|
+
required = False
|
|
214
|
+
default = None
|
|
215
|
+
flag = None
|
|
216
|
+
else:
|
|
217
|
+
# Otherwise it's a default value
|
|
218
|
+
param_type = "str"
|
|
219
|
+
description = ""
|
|
220
|
+
required = False
|
|
221
|
+
default = param_config
|
|
222
|
+
flag = None
|
|
223
|
+
elif param_config is None:
|
|
224
|
+
# Just parameter name
|
|
225
|
+
param_type = "str"
|
|
226
|
+
description = ""
|
|
227
|
+
required = False
|
|
228
|
+
default = None
|
|
229
|
+
flag = None
|
|
230
|
+
else:
|
|
231
|
+
# Value provided - infer type and use as default
|
|
232
|
+
if isinstance(param_config, bool):
|
|
233
|
+
param_type = "bool"
|
|
234
|
+
default = param_config
|
|
235
|
+
elif isinstance(param_config, int):
|
|
236
|
+
param_type = "int"
|
|
237
|
+
default = param_config
|
|
238
|
+
elif isinstance(param_config, float):
|
|
239
|
+
param_type = "float"
|
|
240
|
+
default = param_config
|
|
241
|
+
else:
|
|
242
|
+
param_type = "str"
|
|
243
|
+
default = str(param_config)
|
|
244
|
+
description = ""
|
|
245
|
+
required = False
|
|
246
|
+
flag = None
|
|
247
|
+
|
|
248
|
+
parameters[param_name] = ShellToolParameter(
|
|
249
|
+
name=param_name,
|
|
250
|
+
type=param_type,
|
|
251
|
+
description=description,
|
|
252
|
+
required=required,
|
|
253
|
+
default=default,
|
|
254
|
+
flag=flag,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
return ShellToolDefinition(
|
|
258
|
+
name=tool_dict["name"],
|
|
259
|
+
description=tool_dict.get("description", ""),
|
|
260
|
+
command=tool_dict["command"],
|
|
261
|
+
parameters=parameters,
|
|
262
|
+
timeout=tool_dict.get("timeout", 30),
|
|
263
|
+
safe_mode=tool_dict.get("safe_mode", True),
|
|
264
|
+
shell=tool_dict.get("shell", True),
|
|
265
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: assistant
|
|
3
|
+
description: General-purpose assistant for questions and tasks
|
|
4
|
+
max_steps: 5
|
|
5
|
+
tools: []
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Assistant
|
|
9
|
+
|
|
10
|
+
You are a helpful assistant. Answer questions clearly and directly, provide explanations when needed, and help with general tasks. Focus on being accurate, concise, and useful.
|
|
11
|
+
|
|
12
|
+
When you have completed the task, use the final_answer() function to provide your response.
|
|
13
|
+
|
|
14
|
+
**Task**: {{ user_prompt }}
|