clink-cli 0.2.1__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.
- clink/__init__.py +19 -0
- clink/agents/__init__.py +30 -0
- clink/agents/base.py +70 -0
- clink/agents/claude.py +7 -0
- clink/agents/codex.py +7 -0
- clink/agents/gemini.py +7 -0
- clink/allowed_roots.py +356 -0
- clink/backend.py +765 -0
- clink/cli.py +594 -0
- clink/constants.py +78 -0
- clink/contracts.py +167 -0
- clink/files.py +70 -0
- clink/generation.py +274 -0
- clink/generation_capabilities.py +207 -0
- clink/generation_contracts.py +214 -0
- clink/generation_policy.py +174 -0
- clink/models.py +243 -0
- clink/native.py +409 -0
- clink/native_adapters.py +244 -0
- clink/native_contracts.py +68 -0
- clink/native_generation.py +421 -0
- clink/native_generation_claude_policy.py +59 -0
- clink/native_generation_contracts.py +264 -0
- clink/native_generation_families.py +218 -0
- clink/native_generation_family_policy.py +14 -0
- clink/native_generation_files.py +153 -0
- clink/native_generation_policy.py +82 -0
- clink/native_policy.py +86 -0
- clink/parsers/__init__.py +34 -0
- clink/parsers/agy.py +53 -0
- clink/parsers/agy_native.py +161 -0
- clink/parsers/agy_native_generation.py +189 -0
- clink/parsers/base.py +209 -0
- clink/parsers/claude.py +104 -0
- clink/parsers/claude_native.py +113 -0
- clink/parsers/claude_native_generation.py +346 -0
- clink/parsers/codex.py +172 -0
- clink/parsers/codex_generation.py +212 -0
- clink/parsers/codex_native.py +236 -0
- clink/parsers/codex_native_generation.py +367 -0
- clink/parsers/gemini.py +110 -0
- clink/parsers/grok.py +63 -0
- clink/parsers/grok_native.py +91 -0
- clink/parsers/grok_native_generation.py +157 -0
- clink/policy.py +180 -0
- clink/registry.py +544 -0
- clink/resources/CODEX-LICENSE +201 -0
- clink/resources/CODEX-NOTICE +6 -0
- clink/resources/PROVENANCE.md +16 -0
- clink/resources/codex-0.166.0-models-direct.json +1284 -0
- clink/resources/codex-0.166.0-models-generation.json +1269 -0
- clink/resources/native-agy-1.4.12-admission.json +746 -0
- clink/resources/native-claude-2.1.304-admission.json +99 -0
- clink/resources/native-generation-claude-2.1.304-admission.json +277 -0
- clink/resources/native-generation-codex-0.166.0-admission.json +309 -0
- clink/resources/native-generation-grok-1.0.70-admission.json +329 -0
- clink/resources/native-grok-1.0.70-admission.json +644 -0
- clink/runner.py +303 -0
- clink/setup.py +399 -0
- clink/tasks.py +695 -0
- clink_cli-0.2.1.dist-info/METADATA +225 -0
- clink_cli-0.2.1.dist-info/RECORD +141 -0
- clink_cli-0.2.1.dist-info/WHEEL +5 -0
- clink_cli-0.2.1.dist-info/entry_points.txt +3 -0
- clink_cli-0.2.1.dist-info/licenses/LICENSE +197 -0
- clink_cli-0.2.1.dist-info/licenses/clink/resources/CODEX-LICENSE +201 -0
- clink_cli-0.2.1.dist-info/licenses/clink/resources/CODEX-NOTICE +6 -0
- clink_cli-0.2.1.dist-info/top_level.txt +7 -0
- conf/__init__.py +1 -0
- conf/cli_clients/agy.json +40 -0
- conf/cli_clients/claude.json +39 -0
- conf/cli_clients/codex.json +40 -0
- conf/cli_clients/gemini.json +23 -0
- conf/cli_clients/grok.json +40 -0
- config.py +40 -0
- server.py +896 -0
- systemprompts/__init__.py +35 -0
- systemprompts/analyze_prompt.py +87 -0
- systemprompts/chat_prompt.py +57 -0
- systemprompts/clink/codex_codereviewer.txt +8 -0
- systemprompts/clink/default.txt +8 -0
- systemprompts/clink/default_codereviewer.txt +8 -0
- systemprompts/clink/default_planner.txt +7 -0
- systemprompts/clink/native_codereviewer.txt +1 -0
- systemprompts/clink/native_default.txt +1 -0
- systemprompts/clink/native_planner.txt +1 -0
- systemprompts/codereview_prompt.py +91 -0
- systemprompts/consensus_prompt.py +120 -0
- systemprompts/debug_prompt.py +150 -0
- systemprompts/docgen_prompt.py +252 -0
- systemprompts/generate_code_prompt.py +181 -0
- systemprompts/planner_prompt.py +127 -0
- systemprompts/precommit_prompt.py +179 -0
- systemprompts/refactor_prompt.py +323 -0
- systemprompts/secaudit_prompt.py +420 -0
- systemprompts/testgen_prompt.py +133 -0
- systemprompts/thinkdeep_prompt.py +55 -0
- systemprompts/tracer_prompt.py +145 -0
- tools/__init__.py +43 -0
- tools/analyze.py +583 -0
- tools/apilookup.py +97 -0
- tools/challenge.py +198 -0
- tools/chat.py +187 -0
- tools/clink.py +130 -0
- tools/codereview.py +724 -0
- tools/consensus.py +790 -0
- tools/debug.py +573 -0
- tools/docgen.py +577 -0
- tools/listmodels.py +48 -0
- tools/models.py +373 -0
- tools/planner.py +533 -0
- tools/precommit.py +776 -0
- tools/refactor.py +645 -0
- tools/secaudit.py +685 -0
- tools/shared/__init__.py +19 -0
- tools/shared/base_models.py +167 -0
- tools/shared/base_tool.py +659 -0
- tools/shared/exceptions.py +20 -0
- tools/shared/schema_builders.py +159 -0
- tools/simple/__init__.py +18 -0
- tools/simple/base.py +539 -0
- tools/testgen.py +513 -0
- tools/thinkdeep.py +586 -0
- tools/tracer.py +763 -0
- tools/version.py +53 -0
- tools/workflow/__init__.py +22 -0
- tools/workflow/base.py +448 -0
- tools/workflow/schema_builders.py +166 -0
- tools/workflow/workflow_mixin.py +1195 -0
- utils/__init__.py +21 -0
- utils/call_state.py +349 -0
- utils/client_info.py +293 -0
- utils/consensus_manifest.py +131 -0
- utils/conversation_memory.py +582 -0
- utils/env.py +111 -0
- utils/file_types.py +271 -0
- utils/file_utils.py +803 -0
- utils/image_utils.py +94 -0
- utils/security_config.py +163 -0
- utils/storage_backend.py +177 -0
- utils/token_utils.py +54 -0
tools/simple/base.py
ADDED
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base class for simple MCP tools.
|
|
3
|
+
|
|
4
|
+
Simple tools follow a straightforward pattern:
|
|
5
|
+
1. Receive request
|
|
6
|
+
2. Prepare prompt (with absolute file paths, context, etc.)
|
|
7
|
+
3. Call AI model
|
|
8
|
+
4. Format and return response
|
|
9
|
+
|
|
10
|
+
They use the shared SchemaBuilder for consistent schema generation
|
|
11
|
+
and inherit all the conversation, file processing, and model handling
|
|
12
|
+
capabilities from BaseTool.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from abc import abstractmethod
|
|
17
|
+
from typing import Any, Optional
|
|
18
|
+
|
|
19
|
+
from tools.shared.base_models import ToolRequest
|
|
20
|
+
from tools.shared.base_tool import BaseTool
|
|
21
|
+
from tools.shared.exceptions import ToolExecutionError
|
|
22
|
+
from tools.shared.schema_builders import SchemaBuilder
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SimpleTool(BaseTool):
|
|
26
|
+
"""
|
|
27
|
+
Base class for simple (non-workflow) tools.
|
|
28
|
+
|
|
29
|
+
Simple tools are request/response tools that don't require multi-step workflows.
|
|
30
|
+
They benefit from:
|
|
31
|
+
- Automatic schema generation using SchemaBuilder
|
|
32
|
+
- Inherited conversation handling and file processing
|
|
33
|
+
- Standardized model integration
|
|
34
|
+
- Consistent error handling and response formatting
|
|
35
|
+
|
|
36
|
+
To create a simple tool:
|
|
37
|
+
1. Inherit from SimpleTool
|
|
38
|
+
2. Implement get_tool_fields() to define tool-specific fields
|
|
39
|
+
3. Implement prepare_prompt() for prompt preparation
|
|
40
|
+
4. Optionally override format_response() for custom formatting
|
|
41
|
+
5. Optionally override get_required_fields() for custom requirements
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
class ChatTool(SimpleTool):
|
|
45
|
+
def get_name(self) -> str:
|
|
46
|
+
return "chat"
|
|
47
|
+
|
|
48
|
+
def get_tool_fields(self) -> Dict[str, Dict[str, Any]]:
|
|
49
|
+
return {
|
|
50
|
+
"prompt": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"description": "Your question or idea...",
|
|
53
|
+
},
|
|
54
|
+
"absolute_file_paths": SimpleTool.FILES_FIELD,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def get_required_fields(self) -> List[str]:
|
|
58
|
+
return ["prompt"]
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
# Common field definitions that simple tools can reuse
|
|
62
|
+
FILES_FIELD = SchemaBuilder.SIMPLE_FIELD_SCHEMAS["absolute_file_paths"]
|
|
63
|
+
IMAGES_FIELD = SchemaBuilder.COMMON_FIELD_SCHEMAS["images"]
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def get_tool_fields(self) -> dict[str, dict[str, Any]]:
|
|
67
|
+
"""
|
|
68
|
+
Return tool-specific field definitions.
|
|
69
|
+
|
|
70
|
+
This method should return a dictionary mapping field names to their
|
|
71
|
+
JSON schema definitions. Common fields (model, temperature, etc.)
|
|
72
|
+
are added automatically by the base class.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Dict mapping field names to JSON schema objects
|
|
76
|
+
|
|
77
|
+
Example:
|
|
78
|
+
return {
|
|
79
|
+
"prompt": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"description": "The user's question or request",
|
|
82
|
+
},
|
|
83
|
+
"absolute_file_paths": SimpleTool.FILES_FIELD, # Reuse common field
|
|
84
|
+
"max_tokens": {
|
|
85
|
+
"type": "integer",
|
|
86
|
+
"minimum": 1,
|
|
87
|
+
"description": "Maximum tokens for response",
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
"""
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
def get_required_fields(self) -> list[str]:
|
|
94
|
+
"""
|
|
95
|
+
Return list of required field names.
|
|
96
|
+
|
|
97
|
+
Override this to specify which fields are required for your tool.
|
|
98
|
+
The model field is automatically added if in auto mode.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
List of required field names
|
|
102
|
+
"""
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
def get_annotations(self) -> Optional[dict[str, Any]]:
|
|
106
|
+
"""
|
|
107
|
+
Return tool annotations. Simple tools are read-only by default.
|
|
108
|
+
|
|
109
|
+
All simple tools perform operations without modifying the environment.
|
|
110
|
+
They may call external AI models for analysis or conversation, but they
|
|
111
|
+
don't write files or make system changes.
|
|
112
|
+
|
|
113
|
+
Override this method if your simple tool needs different annotations.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Dictionary with readOnlyHint set to True
|
|
117
|
+
"""
|
|
118
|
+
return {"readOnlyHint": True}
|
|
119
|
+
|
|
120
|
+
def format_response(self, response: str, request, model_info: Optional[dict] = None) -> str:
|
|
121
|
+
"""
|
|
122
|
+
Format the AI response before returning to the client.
|
|
123
|
+
|
|
124
|
+
This is a hook method that subclasses can override to customize
|
|
125
|
+
response formatting. The default implementation returns the response as-is.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
response: The raw response from the AI model
|
|
129
|
+
request: The validated request object
|
|
130
|
+
model_info: Optional model information dictionary
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Formatted response string
|
|
134
|
+
"""
|
|
135
|
+
return response
|
|
136
|
+
|
|
137
|
+
def get_input_schema(self) -> dict[str, Any]:
|
|
138
|
+
"""
|
|
139
|
+
Generate the complete input schema using SchemaBuilder.
|
|
140
|
+
|
|
141
|
+
This method automatically combines:
|
|
142
|
+
- Tool-specific fields from get_tool_fields()
|
|
143
|
+
- Common fields (temperature, thinking_mode, etc.)
|
|
144
|
+
- Model field with proper auto-mode handling
|
|
145
|
+
- Required fields from get_required_fields()
|
|
146
|
+
|
|
147
|
+
Tools can override this method for custom schema generation while
|
|
148
|
+
still benefiting from SimpleTool's convenience methods.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Complete JSON schema for the tool
|
|
152
|
+
"""
|
|
153
|
+
required_fields = list(self.get_required_fields())
|
|
154
|
+
return SchemaBuilder.build_schema(
|
|
155
|
+
tool_specific_fields=self.get_tool_fields(),
|
|
156
|
+
required_fields=required_fields,
|
|
157
|
+
model_field_schema=self.get_model_field_schema(),
|
|
158
|
+
auto_mode=self.is_effective_auto_mode(),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def get_request_model(self):
|
|
162
|
+
"""
|
|
163
|
+
Return the request model class.
|
|
164
|
+
|
|
165
|
+
Simple tools use the base ToolRequest by default.
|
|
166
|
+
Override this if your tool needs a custom request model.
|
|
167
|
+
"""
|
|
168
|
+
return ToolRequest
|
|
169
|
+
|
|
170
|
+
# Hook methods for safe attribute access without hasattr/getattr
|
|
171
|
+
|
|
172
|
+
def get_request_model_name(self, request) -> Optional[str]:
|
|
173
|
+
"""Get model name from request. Override for custom model name handling."""
|
|
174
|
+
try:
|
|
175
|
+
return request.model
|
|
176
|
+
except AttributeError:
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
def get_request_images(self, request) -> list:
|
|
180
|
+
"""Get images from request. Override for custom image handling."""
|
|
181
|
+
try:
|
|
182
|
+
return request.images if request.images is not None else []
|
|
183
|
+
except AttributeError:
|
|
184
|
+
return []
|
|
185
|
+
|
|
186
|
+
def get_request_continuation_id(self, request) -> Optional[str]:
|
|
187
|
+
"""Get continuation_id from request. Override for custom continuation handling."""
|
|
188
|
+
try:
|
|
189
|
+
return request.continuation_id
|
|
190
|
+
except AttributeError:
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
def get_request_prompt(self, request) -> str:
|
|
194
|
+
"""Get prompt from request. Override for custom prompt handling."""
|
|
195
|
+
try:
|
|
196
|
+
return request.prompt
|
|
197
|
+
except AttributeError:
|
|
198
|
+
return ""
|
|
199
|
+
|
|
200
|
+
def get_request_temperature(self, request) -> Optional[float]:
|
|
201
|
+
"""Get temperature from request. Override for custom temperature handling."""
|
|
202
|
+
try:
|
|
203
|
+
return request.temperature
|
|
204
|
+
except AttributeError:
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
def get_request_thinking_mode(self, request) -> Optional[str]:
|
|
208
|
+
"""Get thinking_mode from request. Override for custom thinking mode handling."""
|
|
209
|
+
try:
|
|
210
|
+
return request.thinking_mode
|
|
211
|
+
except AttributeError:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
def get_request_files(self, request) -> list:
|
|
215
|
+
"""Get absolute file paths from request. Override for custom file handling."""
|
|
216
|
+
try:
|
|
217
|
+
files = request.absolute_file_paths
|
|
218
|
+
except AttributeError:
|
|
219
|
+
files = None
|
|
220
|
+
if files is None:
|
|
221
|
+
return []
|
|
222
|
+
return files
|
|
223
|
+
|
|
224
|
+
def get_request_as_dict(self, request) -> dict:
|
|
225
|
+
"""Convert request to dictionary. Override for custom serialization."""
|
|
226
|
+
try:
|
|
227
|
+
# Try Pydantic v2 method first
|
|
228
|
+
return request.model_dump()
|
|
229
|
+
except AttributeError:
|
|
230
|
+
try:
|
|
231
|
+
# Fall back to Pydantic v1 method
|
|
232
|
+
return request.dict()
|
|
233
|
+
except AttributeError:
|
|
234
|
+
# Last resort - convert to dict manually
|
|
235
|
+
return {"prompt": self.get_request_prompt(request)}
|
|
236
|
+
|
|
237
|
+
def set_request_files(self, request, files: list) -> None:
|
|
238
|
+
"""Set absolute file paths on request. Override for custom file setting."""
|
|
239
|
+
try:
|
|
240
|
+
request.absolute_file_paths = files
|
|
241
|
+
except AttributeError:
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
def get_actually_processed_files(self) -> list:
|
|
245
|
+
"""Get actually processed files. Override for custom file tracking."""
|
|
246
|
+
try:
|
|
247
|
+
return self._actually_processed_files
|
|
248
|
+
except AttributeError:
|
|
249
|
+
return []
|
|
250
|
+
|
|
251
|
+
async def execute(self, arguments: dict[str, Any]) -> list:
|
|
252
|
+
"""Validate and delegate once; the server owns replay and atomic recording."""
|
|
253
|
+
from mcp.types import TextContent
|
|
254
|
+
from pydantic import ValidationError
|
|
255
|
+
|
|
256
|
+
from clink.contracts import BackendError
|
|
257
|
+
from tools.models import ToolOutput
|
|
258
|
+
from utils.call_state import StateError
|
|
259
|
+
|
|
260
|
+
self._current_arguments = arguments
|
|
261
|
+
try:
|
|
262
|
+
request = self.get_request_model()(**arguments)
|
|
263
|
+
path_error = self._validate_file_paths(request)
|
|
264
|
+
if path_error:
|
|
265
|
+
raise BackendError(
|
|
266
|
+
"path_not_allowed", path_error, "Supply existing absolute paths inside allowed roots."
|
|
267
|
+
)
|
|
268
|
+
files = self._resolve_cli_target(
|
|
269
|
+
request, files=self.get_request_files(request), images=self.get_request_images(request)
|
|
270
|
+
)
|
|
271
|
+
self.set_request_files(request, list(files))
|
|
272
|
+
prompt = await self.prepare_prompt(request)
|
|
273
|
+
result = await self._run_cli(
|
|
274
|
+
request,
|
|
275
|
+
prompt,
|
|
276
|
+
self.get_system_prompt() + "\n\n" + self.get_language_instruction(),
|
|
277
|
+
files=self.get_request_files(request),
|
|
278
|
+
images=self.get_request_images(request),
|
|
279
|
+
)
|
|
280
|
+
output = self._parse_response(result.content, request, {"metadata": result.to_metadata()})
|
|
281
|
+
return [TextContent(type="text", text=output.model_dump_json())]
|
|
282
|
+
except (ToolExecutionError, StateError):
|
|
283
|
+
raise
|
|
284
|
+
except BackendError as exc:
|
|
285
|
+
output = ToolOutput(status="error", content=str(exc), metadata=exc.to_dict())
|
|
286
|
+
raise ToolExecutionError(output.model_dump_json()) from exc
|
|
287
|
+
except (ValidationError, ValueError):
|
|
288
|
+
output = ToolOutput(
|
|
289
|
+
status="error",
|
|
290
|
+
content="Request validation failed; check the declared fields.",
|
|
291
|
+
metadata={"code": "invalid_request"},
|
|
292
|
+
)
|
|
293
|
+
raise ToolExecutionError(output.model_dump_json()) from None
|
|
294
|
+
except Exception as exc:
|
|
295
|
+
logging.getLogger(__name__).error(
|
|
296
|
+
"tool_execution_failed tool=%s error_type=%s", self.get_name(), type(exc).__name__
|
|
297
|
+
)
|
|
298
|
+
output = ToolOutput(
|
|
299
|
+
status="error",
|
|
300
|
+
content="The tool could not complete this request.",
|
|
301
|
+
metadata={"code": "tool_error", "error_type": type(exc).__name__},
|
|
302
|
+
)
|
|
303
|
+
raise ToolExecutionError(output.model_dump_json()) from None
|
|
304
|
+
|
|
305
|
+
def _parse_response(self, raw_text: str, request, model_info: Optional[dict] = None):
|
|
306
|
+
"""Keep the public envelope while sharing transaction-owned continuation IDs."""
|
|
307
|
+
from tools.models import ContinuationOffer, ToolOutput
|
|
308
|
+
from utils.call_state import get_current_call
|
|
309
|
+
from utils.conversation_memory import MAX_CONVERSATION_TURNS
|
|
310
|
+
|
|
311
|
+
metadata = dict((model_info or {}).get("metadata", {}))
|
|
312
|
+
metadata["tool_name"] = self.get_name()
|
|
313
|
+
offer = None
|
|
314
|
+
call = get_current_call()
|
|
315
|
+
if call is not None:
|
|
316
|
+
remaining = max(0, MAX_CONVERSATION_TURNS - call.context.successful_turns - 2)
|
|
317
|
+
if remaining:
|
|
318
|
+
offer = ContinuationOffer(
|
|
319
|
+
continuation_id=call.continuation_id,
|
|
320
|
+
remaining_turns=remaining,
|
|
321
|
+
note="Reuse this continuation_id to preserve conversation context.",
|
|
322
|
+
)
|
|
323
|
+
return ToolOutput(
|
|
324
|
+
status="continuation_available" if offer else "success",
|
|
325
|
+
content=self.format_response(raw_text, request, model_info),
|
|
326
|
+
continuation_offer=offer,
|
|
327
|
+
metadata=metadata,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Convenience methods for common tool patterns
|
|
331
|
+
|
|
332
|
+
def build_standard_prompt(
|
|
333
|
+
self, system_prompt: str, user_content: str, request, file_context_title: str = "CONTEXT FILES"
|
|
334
|
+
) -> str:
|
|
335
|
+
"""
|
|
336
|
+
Build a standard prompt with system prompt, user content, and optional files.
|
|
337
|
+
|
|
338
|
+
This is a convenience method that handles the common pattern of:
|
|
339
|
+
1. Adding file content if present
|
|
340
|
+
2. Checking token limits
|
|
341
|
+
3. Adding web search instructions
|
|
342
|
+
4. Combining everything into a well-formatted prompt
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
system_prompt: The system prompt for the tool
|
|
346
|
+
user_content: The main user request/content
|
|
347
|
+
request: The validated request object
|
|
348
|
+
file_context_title: Title for the file context section
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Complete formatted prompt ready for the AI model
|
|
352
|
+
"""
|
|
353
|
+
# Check size limits against raw user input before enriching with internal context
|
|
354
|
+
content_to_validate = self.get_prompt_content_for_size_validation(user_content)
|
|
355
|
+
self._validate_token_limit(content_to_validate, "Content")
|
|
356
|
+
|
|
357
|
+
# Only the common backend reads/embeds declared files, once under its byte cap.
|
|
358
|
+
files = self.get_request_files(request)
|
|
359
|
+
if files:
|
|
360
|
+
references = "\n".join(f"- {path}" for path in files)
|
|
361
|
+
user_content = f"{user_content}\n\n=== {file_context_title} REFERENCES ===\n{references}"
|
|
362
|
+
|
|
363
|
+
# Add standardized web search guidance
|
|
364
|
+
websearch_instruction = self.get_websearch_instruction(self.get_websearch_guidance())
|
|
365
|
+
|
|
366
|
+
# Combine system prompt with user content
|
|
367
|
+
full_prompt = f"""{system_prompt}{websearch_instruction}
|
|
368
|
+
|
|
369
|
+
=== USER REQUEST ===
|
|
370
|
+
{user_content}
|
|
371
|
+
=== END REQUEST ===
|
|
372
|
+
|
|
373
|
+
Please provide a thoughtful, comprehensive response:"""
|
|
374
|
+
|
|
375
|
+
return full_prompt
|
|
376
|
+
|
|
377
|
+
def get_prompt_content_for_size_validation(self, user_content: str) -> str:
|
|
378
|
+
"""
|
|
379
|
+
Override to use original user prompt for size validation when conversation history is embedded.
|
|
380
|
+
|
|
381
|
+
When server.py embeds conversation history into the prompt field, it also stores
|
|
382
|
+
the original user prompt in _original_user_prompt. We use that for size validation
|
|
383
|
+
to avoid incorrectly triggering size limits due to conversation history.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
user_content: The user content (may include conversation history)
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
The original user prompt if available, otherwise the full user content
|
|
390
|
+
"""
|
|
391
|
+
# Check if we have the current arguments from execute() method
|
|
392
|
+
current_args = getattr(self, "_current_arguments", None)
|
|
393
|
+
if current_args:
|
|
394
|
+
# If server.py embedded conversation history, it stores original prompt separately
|
|
395
|
+
original_user_prompt = current_args.get("_original_user_prompt")
|
|
396
|
+
if original_user_prompt is not None:
|
|
397
|
+
# Use original user prompt for size validation (excludes conversation history)
|
|
398
|
+
return original_user_prompt
|
|
399
|
+
|
|
400
|
+
# Fallback to default behavior (validate full user content)
|
|
401
|
+
return user_content
|
|
402
|
+
|
|
403
|
+
def get_websearch_guidance(self) -> Optional[str]:
|
|
404
|
+
"""
|
|
405
|
+
Return tool-specific web search guidance.
|
|
406
|
+
|
|
407
|
+
Override this to provide tool-specific guidance for when web searches
|
|
408
|
+
would be helpful. Return None to use the default guidance.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
Tool-specific web search guidance or None for default
|
|
412
|
+
"""
|
|
413
|
+
return None
|
|
414
|
+
|
|
415
|
+
def handle_prompt_file_with_fallback(self, request) -> str:
|
|
416
|
+
"""Use only an explicitly attached prompt.txt as this call's primary input."""
|
|
417
|
+
import os
|
|
418
|
+
|
|
419
|
+
from clink.contracts import BackendError
|
|
420
|
+
|
|
421
|
+
files = self.get_request_files(request)
|
|
422
|
+
original = self._current_arguments.get("_original_arguments")
|
|
423
|
+
if original is None:
|
|
424
|
+
explicit_files = files
|
|
425
|
+
else:
|
|
426
|
+
originals = {os.path.realpath(os.path.expanduser(path)) for path in original.get("absolute_file_paths", [])}
|
|
427
|
+
explicit_files = [path for path in files if os.path.realpath(path) in originals]
|
|
428
|
+
prompt_content, updated_files = self.handle_prompt_file(explicit_files)
|
|
429
|
+
if prompt_content is not None:
|
|
430
|
+
consumed = set(explicit_files) - set(updated_files or [])
|
|
431
|
+
self.set_request_files(request, [path for path in files if path not in consumed])
|
|
432
|
+
user_content = prompt_content if prompt_content is not None else self.get_request_prompt(request)
|
|
433
|
+
self._effective_user_prompt = user_content
|
|
434
|
+
size_check = self.check_prompt_size(user_content)
|
|
435
|
+
if size_check:
|
|
436
|
+
raise BackendError("input_limit", size_check["content"], size_check["metadata"]["remedy"])
|
|
437
|
+
return user_content
|
|
438
|
+
|
|
439
|
+
def get_chat_style_websearch_guidance(self) -> str:
|
|
440
|
+
"""
|
|
441
|
+
Get Chat tool-style web search guidance.
|
|
442
|
+
|
|
443
|
+
Returns web search guidance that matches the original Chat tool pattern.
|
|
444
|
+
This is useful for tools that want to maintain the same search behavior.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
Web search guidance text
|
|
448
|
+
"""
|
|
449
|
+
return """When discussing topics, consider if searches for these would help:
|
|
450
|
+
- Documentation for any technologies or concepts mentioned
|
|
451
|
+
- Current best practices and patterns
|
|
452
|
+
- Recent developments or updates
|
|
453
|
+
- Community discussions and solutions"""
|
|
454
|
+
|
|
455
|
+
def supports_custom_request_model(self) -> bool:
|
|
456
|
+
"""
|
|
457
|
+
Indicate whether this tool supports custom request models.
|
|
458
|
+
|
|
459
|
+
Simple tools support custom request models by default. Tools that override
|
|
460
|
+
get_request_model() to return something other than ToolRequest should
|
|
461
|
+
return True here.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
True if the tool uses a custom request model
|
|
465
|
+
"""
|
|
466
|
+
return self.get_request_model() != ToolRequest
|
|
467
|
+
|
|
468
|
+
def _validate_file_paths(self, request) -> Optional[str]:
|
|
469
|
+
"""
|
|
470
|
+
Validate that all file paths in the request are absolute paths.
|
|
471
|
+
|
|
472
|
+
This is a security measure to prevent path traversal attacks and ensure
|
|
473
|
+
proper access control. All file paths must be absolute (starting with '/').
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
request: The validated request object
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
Optional[str]: Error message if validation fails, None if all paths are valid
|
|
480
|
+
"""
|
|
481
|
+
import os
|
|
482
|
+
|
|
483
|
+
# Check if request has absolute file paths attribute (legacy tools may still provide 'files')
|
|
484
|
+
files = self.get_request_files(request)
|
|
485
|
+
if files:
|
|
486
|
+
for file_path in files:
|
|
487
|
+
if not os.path.isabs(file_path):
|
|
488
|
+
return (
|
|
489
|
+
f"Error: All file paths must be FULL absolute paths to real files / folders - DO NOT SHORTEN. "
|
|
490
|
+
f"Received relative path: {file_path}\n"
|
|
491
|
+
f"Please provide the full absolute path starting with '/' (must be FULL absolute paths to real files / folders - DO NOT SHORTEN)"
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
return None
|
|
495
|
+
|
|
496
|
+
def prepare_chat_style_prompt(self, request, system_prompt: str = None) -> str:
|
|
497
|
+
"""
|
|
498
|
+
Prepare a prompt using Chat tool-style patterns.
|
|
499
|
+
|
|
500
|
+
This convenience method replicates the Chat tool's prompt preparation logic:
|
|
501
|
+
1. Handle prompt.txt file if present
|
|
502
|
+
2. Add file context with specific formatting
|
|
503
|
+
3. Add web search guidance
|
|
504
|
+
4. Format with system prompt
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
request: The validated request object
|
|
508
|
+
system_prompt: System prompt to use (uses get_system_prompt() if None)
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
Complete formatted prompt
|
|
512
|
+
"""
|
|
513
|
+
# Use provided system prompt or get from tool
|
|
514
|
+
if system_prompt is None:
|
|
515
|
+
system_prompt = self.get_system_prompt()
|
|
516
|
+
|
|
517
|
+
# Get user content (handles prompt.txt files)
|
|
518
|
+
user_content = self.handle_prompt_file_with_fallback(request)
|
|
519
|
+
|
|
520
|
+
# Build standard prompt with Chat-style web search guidance
|
|
521
|
+
websearch_guidance = self.get_chat_style_websearch_guidance()
|
|
522
|
+
|
|
523
|
+
# Override the websearch guidance temporarily
|
|
524
|
+
original_guidance = self.get_websearch_guidance
|
|
525
|
+
self.get_websearch_guidance = lambda: websearch_guidance
|
|
526
|
+
|
|
527
|
+
try:
|
|
528
|
+
full_prompt = self.build_standard_prompt(system_prompt, user_content, request, "CONTEXT FILES")
|
|
529
|
+
finally:
|
|
530
|
+
# Restore original guidance method
|
|
531
|
+
self.get_websearch_guidance = original_guidance
|
|
532
|
+
|
|
533
|
+
if system_prompt:
|
|
534
|
+
marker = "\n\n=== USER REQUEST ===\n"
|
|
535
|
+
if marker in full_prompt:
|
|
536
|
+
_, user_section = full_prompt.split(marker, 1)
|
|
537
|
+
return f"=== USER REQUEST ===\n{user_section}"
|
|
538
|
+
|
|
539
|
+
return full_prompt
|