cli2api 0.2.0__tar.gz → 0.2.2__tar.gz
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.
- cli2api-0.2.2/.env.example +32 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/PKG-INFO +1 -1
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/__init__.py +1 -1
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/api/dependencies.py +21 -1
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/config/settings.py +28 -15
- cli2api-0.2.2/cli2api/prompts/tools_instruction.txt +58 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/providers/claude.py +33 -5
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/tools/handler.py +29 -47
- cli2api-0.2.2/cli2api/tools/validator.py +105 -0
- cli2api-0.2.2/cli2api/utils/__init__.py +0 -0
- cli2api-0.2.2/cli2api/utils/cli_detector.py +337 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/pyproject.toml +1 -1
- cli2api-0.2.2/tests/test_cli_detector.py +423 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/tests/test_config.py +21 -14
- {cli2api-0.2.0 → cli2api-0.2.2}/tests/test_providers.py +173 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/tests/test_streaming_tool_parser.py +19 -0
- cli2api-0.2.2/tests/test_tool_validator.py +201 -0
- cli2api-0.2.0/.env.example +0 -22
- {cli2api-0.2.0 → cli2api-0.2.2}/.dockerignore +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/.github/workflows/publish.yml +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/.gitignore +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/Dockerfile +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/README.md +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/__main__.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/api/__init__.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/api/router.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/api/utils.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/api/v1/__init__.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/api/v1/chat.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/api/v1/models.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/api/v1/responses.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/config/__init__.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/constants.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/main.py +0 -0
- {cli2api-0.2.0/cli2api/utils → cli2api-0.2.2/cli2api/prompts}/__init__.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/providers/__init__.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/schemas/__init__.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/schemas/internal.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/schemas/openai.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/services/__init__.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/services/completion.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/streaming/__init__.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/streaming/sse.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/streaming/tool_parser.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/tools/__init__.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api/utils/logging.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/cli2api.sh +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/docker-compose.yaml +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/tests/__init__.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/tests/conftest.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/tests/test_api.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/tests/test_integration.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/tests/test_schemas.py +0 -0
- {cli2api-0.2.0 → cli2api-0.2.2}/tests/test_streaming.py +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# CLI2API Configuration
|
|
2
|
+
# Copy this file to .env and customize as needed
|
|
3
|
+
|
|
4
|
+
# Server settings
|
|
5
|
+
CLI2API_HOST=0.0.0.0
|
|
6
|
+
CLI2API_PORT=8000
|
|
7
|
+
CLI2API_DEBUG=false
|
|
8
|
+
|
|
9
|
+
# CLI executable path (auto-detected if not set)
|
|
10
|
+
# Auto-detection checks (in order):
|
|
11
|
+
# 1. Cached path (~/.cache/cli2api/claude_cli_path)
|
|
12
|
+
# 2. System PATH (shutil.which)
|
|
13
|
+
# 3. Running Claude processes (ps aux / wmic)
|
|
14
|
+
# 4. VSCode extensions (~/.vscode/extensions/anthropic.claude-code-*)
|
|
15
|
+
# 5. NPM global packages
|
|
16
|
+
# 6. Common paths (/opt/homebrew/bin, /usr/local/bin, ~/.local/bin)
|
|
17
|
+
#
|
|
18
|
+
# Explicitly set if auto-detection fails or you want to override:
|
|
19
|
+
# CLI2API_CLAUDE_CLI_PATH=/opt/homebrew/bin/claude
|
|
20
|
+
# CLI2API_CLAUDE_CLI_PATH=/Users/username/.vscode/extensions/anthropic.claude-code-2.1.31-darwin-arm64/resources/native-binary/claude
|
|
21
|
+
|
|
22
|
+
# Timeout (in seconds)
|
|
23
|
+
CLI2API_DEFAULT_TIMEOUT=300
|
|
24
|
+
|
|
25
|
+
# Default model
|
|
26
|
+
CLI2API_DEFAULT_MODEL=sonnet
|
|
27
|
+
|
|
28
|
+
# Custom models (comma-separated)
|
|
29
|
+
# CLI2API_CLAUDE_MODELS=sonnet,opus,haiku
|
|
30
|
+
|
|
31
|
+
# Logging
|
|
32
|
+
CLI2API_LOG_LEVEL=INFO
|
|
@@ -29,7 +29,27 @@ def get_provider() -> ClaudeCodeProvider:
|
|
|
29
29
|
"""
|
|
30
30
|
settings = get_settings()
|
|
31
31
|
if not settings.claude_cli_path:
|
|
32
|
-
|
|
32
|
+
error_msg = """
|
|
33
|
+
Claude CLI not found. Please:
|
|
34
|
+
|
|
35
|
+
1. Install Claude Code: https://docs.anthropic.com/en/docs/claude-code
|
|
36
|
+
2. Or set environment variable: export CLI2API_CLAUDE_CLI_PATH=/path/to/claude
|
|
37
|
+
3. Or ensure Claude is in your PATH
|
|
38
|
+
|
|
39
|
+
Auto-detection checked:
|
|
40
|
+
- Cached path (~/.cache/cli2api/claude_cli_path)
|
|
41
|
+
- System PATH (shutil.which)
|
|
42
|
+
- Running processes (ps aux / wmic)
|
|
43
|
+
- VSCode extensions (platform-specific)
|
|
44
|
+
- NPM global packages
|
|
45
|
+
- Common paths (platform-specific)
|
|
46
|
+
|
|
47
|
+
For debugging, run: CLI2API_LOG_LEVEL=DEBUG cli2api
|
|
48
|
+
|
|
49
|
+
Note: Detected path is cached in ~/.cache/cli2api/claude_cli_path
|
|
50
|
+
To force re-detection, delete the cache file.
|
|
51
|
+
"""
|
|
52
|
+
raise RuntimeError(error_msg.strip())
|
|
33
53
|
|
|
34
54
|
return ClaudeCodeProvider(
|
|
35
55
|
executable_path=Path(settings.claude_cli_path),
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Application settings."""
|
|
2
2
|
|
|
3
|
-
import shutil
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
from typing import Optional
|
|
6
5
|
|
|
@@ -68,19 +67,33 @@ class Settings(BaseSettings):
|
|
|
68
67
|
@classmethod
|
|
69
68
|
def detect_claude_cli(cls, v: Optional[str]) -> Optional[str]:
|
|
70
69
|
"""Auto-detect claude CLI path if not provided."""
|
|
70
|
+
from cli2api.utils.cli_detector import (
|
|
71
|
+
cache_path,
|
|
72
|
+
detect_claude_cli,
|
|
73
|
+
verify_claude_executable,
|
|
74
|
+
)
|
|
75
|
+
from cli2api.utils.logging import get_logger
|
|
76
|
+
|
|
77
|
+
logger = get_logger(__name__)
|
|
78
|
+
|
|
71
79
|
if v:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
# User explicitly set path - verify it
|
|
81
|
+
path = Path(v)
|
|
82
|
+
if verify_claude_executable(path):
|
|
83
|
+
logger.info(f"Using explicitly set Claude CLI path: {v}")
|
|
84
|
+
# Cache explicitly set path
|
|
85
|
+
cache_path(path)
|
|
86
|
+
return v
|
|
87
|
+
logger.warning(f"Explicitly set Claude CLI path is invalid: {v}")
|
|
88
|
+
|
|
89
|
+
# Auto-detect (includes cache check)
|
|
90
|
+
detected_path = detect_claude_cli()
|
|
91
|
+
|
|
92
|
+
if detected_path:
|
|
93
|
+
logger.info(f"Auto-detected Claude CLI: {detected_path}")
|
|
94
|
+
# Cache detected path for next startup
|
|
95
|
+
cache_path(detected_path)
|
|
96
|
+
return str(detected_path)
|
|
97
|
+
|
|
98
|
+
logger.error("Claude CLI not found")
|
|
86
99
|
return None
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
You have access to the following tools:
|
|
2
|
+
|
|
3
|
+
{tool_definitions}
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
RESPONSE FORMAT:
|
|
7
|
+
|
|
8
|
+
When calling tools, wrap EACH tool call in <tool_call> tags.
|
|
9
|
+
You may include explanatory text before or after the tags.
|
|
10
|
+
|
|
11
|
+
For a SINGLE tool call:
|
|
12
|
+
<tool_call>
|
|
13
|
+
{{"name": "TOOL_NAME", "arguments": {{...}}}}
|
|
14
|
+
</tool_call>
|
|
15
|
+
|
|
16
|
+
For MULTIPLE tool calls, use SEPARATE tags for each:
|
|
17
|
+
<tool_call>
|
|
18
|
+
{{"name": "read_file", "arguments": {{"path": "file1.py"}}}}
|
|
19
|
+
</tool_call>
|
|
20
|
+
<tool_call>
|
|
21
|
+
{{"name": "read_file", "arguments": {{"path": "file2.py"}}}}
|
|
22
|
+
</tool_call>
|
|
23
|
+
|
|
24
|
+
CRITICAL RULES:
|
|
25
|
+
- ALWAYS wrap tool calls in <tool_call>...</tool_call> tags
|
|
26
|
+
- Each tool call must be in its own tag pair
|
|
27
|
+
- ALWAYS include the "arguments" key with ALL required parameters filled in
|
|
28
|
+
- NEVER output {{"name": "tool_name"}} without "arguments" — this is INVALID
|
|
29
|
+
- NEVER output empty arguments {{}} when the tool has required parameters
|
|
30
|
+
- You CAN include text explanation before/after tags
|
|
31
|
+
- Do NOT execute tools yourself — just output the tags
|
|
32
|
+
- If user wants to read a file -> use read_file tool
|
|
33
|
+
- If user wants to list files -> use list_files tool
|
|
34
|
+
|
|
35
|
+
WRONG — missing arguments:
|
|
36
|
+
<tool_call>
|
|
37
|
+
{{"name": "execute_command"}}
|
|
38
|
+
</tool_call>
|
|
39
|
+
|
|
40
|
+
WRONG — empty arguments when tool requires parameters:
|
|
41
|
+
<tool_call>
|
|
42
|
+
{{"name": "execute_command", "arguments": {{}}}}
|
|
43
|
+
</tool_call>
|
|
44
|
+
|
|
45
|
+
CORRECT:
|
|
46
|
+
<tool_call>
|
|
47
|
+
{{"name": "execute_command", "arguments": {{"command": "ls -la"}}}}
|
|
48
|
+
</tool_call>
|
|
49
|
+
|
|
50
|
+
{tool_examples}
|
|
51
|
+
|
|
52
|
+
ATTEMPT_COMPLETION USAGE:
|
|
53
|
+
When using attempt_completion, ALWAYS include the 'result' parameter:
|
|
54
|
+
<tool_call>
|
|
55
|
+
{{"name": "attempt_completion", "arguments": {{"result": "Your response text here"}}}}
|
|
56
|
+
</tool_call>
|
|
57
|
+
- The 'result' parameter is REQUIRED and must contain your final response
|
|
58
|
+
- Use attempt_completion ONLY after you have gathered all needed information
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
|
+
import uuid
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Any, AsyncIterator, Optional
|
|
7
8
|
|
|
9
|
+
from cli2api.constants import ID_HEX_LENGTH, TOOL_CALL_ID_PREFIX
|
|
8
10
|
from cli2api.schemas.internal import ProviderChunk, ProviderResult
|
|
9
11
|
from cli2api.schemas.openai import ChatMessage
|
|
10
12
|
from cli2api.streaming.tool_parser import StreamingToolParser
|
|
11
13
|
from cli2api.tools.handler import ToolHandler
|
|
14
|
+
from cli2api.tools.validator import filter_valid_tool_calls
|
|
12
15
|
from cli2api.utils.logging import get_logger
|
|
13
16
|
|
|
14
17
|
logger = get_logger(__name__)
|
|
@@ -221,7 +224,9 @@ class ClaudeCodeProvider:
|
|
|
221
224
|
) -> tuple[str, Optional[list[dict]]]:
|
|
222
225
|
"""Parse content for tool calls if tools are provided."""
|
|
223
226
|
if tools and content:
|
|
224
|
-
|
|
227
|
+
remaining, tool_calls = ToolHandler.parse_tool_calls(content)
|
|
228
|
+
tool_calls = filter_valid_tool_calls(tool_calls, tools)
|
|
229
|
+
return remaining, tool_calls
|
|
225
230
|
return content, None
|
|
226
231
|
|
|
227
232
|
def _create_final_chunk(
|
|
@@ -237,12 +242,27 @@ class ClaudeCodeProvider:
|
|
|
237
242
|
usage=usage,
|
|
238
243
|
)
|
|
239
244
|
|
|
245
|
+
@staticmethod
|
|
246
|
+
def _deserialize_string_values(obj: dict) -> dict:
|
|
247
|
+
"""Deserialize JSON-encoded string values in tool arguments.
|
|
248
|
+
|
|
249
|
+
Claude sometimes returns structured data (arrays, objects) as
|
|
250
|
+
JSON-encoded strings. Kilo Code expects native types, so we
|
|
251
|
+
parse them back. E.g. follow_up: '[{"text":"Yes"}]' -> [{"text":"Yes"}]
|
|
252
|
+
"""
|
|
253
|
+
result = {}
|
|
254
|
+
for key, value in obj.items():
|
|
255
|
+
if isinstance(value, str) and value and value[0] in ('[', '{'):
|
|
256
|
+
try:
|
|
257
|
+
result[key] = json.loads(value)
|
|
258
|
+
except (json.JSONDecodeError, ValueError):
|
|
259
|
+
result[key] = value
|
|
260
|
+
else:
|
|
261
|
+
result[key] = value
|
|
262
|
+
return result
|
|
263
|
+
|
|
240
264
|
def _extract_native_tool_call(self, block: dict) -> Optional[dict]:
|
|
241
265
|
"""Extract tool call from Claude native tool_use block."""
|
|
242
|
-
import json
|
|
243
|
-
import uuid
|
|
244
|
-
from cli2api.constants import ID_HEX_LENGTH, TOOL_CALL_ID_PREFIX
|
|
245
|
-
|
|
246
266
|
tool_name = block.get("name")
|
|
247
267
|
tool_input = block.get("input", {})
|
|
248
268
|
tool_id = block.get("id", f"{TOOL_CALL_ID_PREFIX}{uuid.uuid4().hex[:ID_HEX_LENGTH]}")
|
|
@@ -250,6 +270,9 @@ class ClaudeCodeProvider:
|
|
|
250
270
|
if not tool_name:
|
|
251
271
|
return None
|
|
252
272
|
|
|
273
|
+
if isinstance(tool_input, dict):
|
|
274
|
+
tool_input = self._deserialize_string_values(tool_input)
|
|
275
|
+
|
|
253
276
|
return {
|
|
254
277
|
"id": tool_id,
|
|
255
278
|
"type": "function",
|
|
@@ -391,12 +414,14 @@ class ClaudeCodeProvider:
|
|
|
391
414
|
# Native tool calling (stop_reason=tool_use)
|
|
392
415
|
if stop_reason == "tool_use":
|
|
393
416
|
tool_calls = native_tool_calls if native_tool_calls else None
|
|
417
|
+
tool_calls = filter_valid_tool_calls(tool_calls, tools)
|
|
394
418
|
logger.info(f"[STREAM] Native tool_calls: {len(tool_calls) if tool_calls else 0}")
|
|
395
419
|
elif tool_parser:
|
|
396
420
|
final_result = tool_parser.finalize()
|
|
397
421
|
if final_result.text:
|
|
398
422
|
yield ProviderChunk(content=final_result.text)
|
|
399
423
|
tool_calls = tool_parser.get_all_tool_calls()
|
|
424
|
+
tool_calls = filter_valid_tool_calls(tool_calls, tools)
|
|
400
425
|
else:
|
|
401
426
|
_, tool_calls = self._handle_tool_calls_in_content(
|
|
402
427
|
accumulated_content, tools
|
|
@@ -433,6 +458,7 @@ class ClaudeCodeProvider:
|
|
|
433
458
|
if final_result.text:
|
|
434
459
|
yield ProviderChunk(content=final_result.text)
|
|
435
460
|
tool_calls = tool_parser.get_all_tool_calls()
|
|
461
|
+
tool_calls = filter_valid_tool_calls(tool_calls, tools)
|
|
436
462
|
else:
|
|
437
463
|
_, tool_calls = self._handle_tool_calls_in_content(
|
|
438
464
|
accumulated_content, tools
|
|
@@ -476,6 +502,7 @@ class ClaudeCodeProvider:
|
|
|
476
502
|
if final_result.text:
|
|
477
503
|
yield ProviderChunk(content=final_result.text)
|
|
478
504
|
tool_calls = tool_parser.get_all_tool_calls()
|
|
505
|
+
tool_calls = filter_valid_tool_calls(tool_calls, tools)
|
|
479
506
|
else:
|
|
480
507
|
_, tool_calls = self._handle_tool_calls_in_content(
|
|
481
508
|
accumulated_content, tools
|
|
@@ -489,6 +516,7 @@ class ClaudeCodeProvider:
|
|
|
489
516
|
if final_result.text:
|
|
490
517
|
yield ProviderChunk(content=final_result.text)
|
|
491
518
|
tool_calls = tool_parser.get_all_tool_calls()
|
|
519
|
+
tool_calls = filter_valid_tool_calls(tool_calls, tools)
|
|
492
520
|
else:
|
|
493
521
|
_, tool_calls = self._handle_tool_calls_in_content(accumulated_content, tools)
|
|
494
522
|
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
5
|
import uuid
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from pathlib import Path
|
|
6
8
|
from typing import Any, Optional
|
|
7
9
|
|
|
8
10
|
from cli2api.constants import (
|
|
@@ -14,6 +16,13 @@ from cli2api.constants import (
|
|
|
14
16
|
from cli2api.schemas.openai import ChatMessage
|
|
15
17
|
|
|
16
18
|
|
|
19
|
+
@lru_cache(maxsize=4)
|
|
20
|
+
def _load_prompt_template(filename: str) -> str:
|
|
21
|
+
"""Load prompt template from cli2api/prompts/ directory."""
|
|
22
|
+
path = Path(__file__).parent.parent / "prompts" / filename
|
|
23
|
+
return path.read_text()
|
|
24
|
+
|
|
25
|
+
|
|
17
26
|
# ==================== Message Helpers ====================
|
|
18
27
|
|
|
19
28
|
|
|
@@ -86,9 +95,8 @@ class ToolHandler:
|
|
|
86
95
|
key=lambda t: t.get("function", {}).get("name", "")
|
|
87
96
|
)
|
|
88
97
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
]
|
|
98
|
+
tool_definitions = []
|
|
99
|
+
tool_examples = []
|
|
92
100
|
|
|
93
101
|
for tool in tools:
|
|
94
102
|
if tool.get("type") != "function":
|
|
@@ -98,14 +106,10 @@ class ToolHandler:
|
|
|
98
106
|
name = func.get("name", "unknown")
|
|
99
107
|
description = func.get("description", "No description")
|
|
100
108
|
params = func.get("parameters", {})
|
|
101
|
-
|
|
102
|
-
lines.append(f"## {name}")
|
|
103
|
-
lines.append(f"{description}\n")
|
|
104
|
-
|
|
105
|
-
# Format parameters
|
|
106
109
|
properties = params.get("properties", {})
|
|
107
110
|
required = params.get("required", [])
|
|
108
111
|
|
|
112
|
+
lines = [f"## {name}", f"{description}\n"]
|
|
109
113
|
if properties:
|
|
110
114
|
lines.append("Parameters:")
|
|
111
115
|
for pname, pinfo in properties.items():
|
|
@@ -114,45 +118,23 @@ class ToolHandler:
|
|
|
114
118
|
req = " (required)" if pname in required else " (optional)"
|
|
115
119
|
lines.append(f"- {pname} ({ptype}{req}): {pdesc}")
|
|
116
120
|
lines.append("")
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
lines.append('{"name": "read_file", "arguments": {"path": "file2.py"}}')
|
|
135
|
-
lines.append("</tool_call>")
|
|
136
|
-
lines.append("")
|
|
137
|
-
lines.append("CRITICAL RULES:")
|
|
138
|
-
lines.append("- ALWAYS wrap tool calls in <tool_call>...</tool_call> tags")
|
|
139
|
-
lines.append("- Each tool call must be in its own tag pair")
|
|
140
|
-
lines.append("- When reading multiple files, include multiple <tool_call> tags")
|
|
141
|
-
lines.append("- You CAN include text explanation before/after tags")
|
|
142
|
-
lines.append("- Do NOT execute tools yourself - just output the tags")
|
|
143
|
-
lines.append("- If user wants to read a file -> use read_file tool")
|
|
144
|
-
lines.append("- If user wants to list files -> use list_files tool")
|
|
145
|
-
lines.append("- ALWAYS include ALL required parameters for each tool")
|
|
146
|
-
lines.append("")
|
|
147
|
-
lines.append("ATTEMPT_COMPLETION USAGE:")
|
|
148
|
-
lines.append("When using attempt_completion, ALWAYS include the 'result' parameter:")
|
|
149
|
-
lines.append("<tool_call>")
|
|
150
|
-
lines.append('{"name": "attempt_completion", "arguments": {"result": "Your response text here"}}')
|
|
151
|
-
lines.append("</tool_call>")
|
|
152
|
-
lines.append("- The 'result' parameter is REQUIRED and must contain your final response")
|
|
153
|
-
lines.append("- Use attempt_completion ONLY after you have gathered all needed information")
|
|
154
|
-
|
|
155
|
-
return "\n".join(lines)
|
|
121
|
+
tool_definitions.append("\n".join(lines))
|
|
122
|
+
|
|
123
|
+
# Generate example call for tools with required parameters
|
|
124
|
+
if required:
|
|
125
|
+
example_args = {p: "<value>" for p in required}
|
|
126
|
+
example_json = json.dumps({"name": name, "arguments": example_args})
|
|
127
|
+
tool_examples.append(f"<tool_call>\n{example_json}\n</tool_call>")
|
|
128
|
+
|
|
129
|
+
template = _load_prompt_template("tools_instruction.txt")
|
|
130
|
+
examples_section = ""
|
|
131
|
+
if tool_examples:
|
|
132
|
+
examples_section = "Required parameter examples:\n" + "\n".join(tool_examples)
|
|
133
|
+
|
|
134
|
+
return template.format(
|
|
135
|
+
tool_definitions="\n".join(tool_definitions),
|
|
136
|
+
tool_examples=examples_section,
|
|
137
|
+
)
|
|
156
138
|
|
|
157
139
|
@staticmethod
|
|
158
140
|
def generate_tool_call_id() -> str:
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Tool call argument validation against OpenAI tool schemas."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from cli2api.utils.logging import get_logger
|
|
7
|
+
|
|
8
|
+
logger = get_logger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_required_params_index(tools: list[dict]) -> dict[str, list[str]]:
|
|
12
|
+
"""Build a lookup: tool_name -> list of required parameter names.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
tools: OpenAI-format tool definitions.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Dict mapping tool name to list of required param names.
|
|
19
|
+
"""
|
|
20
|
+
index = {}
|
|
21
|
+
for tool in tools:
|
|
22
|
+
if tool.get("type") != "function":
|
|
23
|
+
continue
|
|
24
|
+
func = tool.get("function", {})
|
|
25
|
+
name = func.get("name")
|
|
26
|
+
if not name:
|
|
27
|
+
continue
|
|
28
|
+
params = func.get("parameters", {})
|
|
29
|
+
index[name] = params.get("required", [])
|
|
30
|
+
return index
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def validate_tool_call(
|
|
34
|
+
tool_call: dict,
|
|
35
|
+
required_params_index: dict[str, list[str]],
|
|
36
|
+
) -> bool:
|
|
37
|
+
"""Validate a single tool call has all required arguments.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
tool_call: Parsed tool call in OpenAI format.
|
|
41
|
+
required_params_index: From build_required_params_index().
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
True if valid (all required params present), False otherwise.
|
|
45
|
+
"""
|
|
46
|
+
func = tool_call.get("function", {})
|
|
47
|
+
name = func.get("name", "")
|
|
48
|
+
arguments_str = func.get("arguments", "{}")
|
|
49
|
+
|
|
50
|
+
required = required_params_index.get(name)
|
|
51
|
+
if required is None:
|
|
52
|
+
# Unknown tool — can't validate, let it pass
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
if not required:
|
|
56
|
+
# Tool has no required params — always valid
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
arguments = json.loads(arguments_str)
|
|
61
|
+
except (json.JSONDecodeError, TypeError):
|
|
62
|
+
logger.warning(f"Tool call '{name}' has unparseable arguments: {arguments_str!r}")
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
if not isinstance(arguments, dict):
|
|
66
|
+
logger.warning(f"Tool call '{name}' arguments is not a dict: {type(arguments)}")
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
missing = [p for p in required if p not in arguments]
|
|
70
|
+
if missing:
|
|
71
|
+
logger.warning(
|
|
72
|
+
f"Tool call '{name}' missing required params: {missing}. "
|
|
73
|
+
f"Got: {list(arguments.keys())}"
|
|
74
|
+
)
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def filter_valid_tool_calls(
|
|
81
|
+
tool_calls: Optional[list[dict]],
|
|
82
|
+
tools: Optional[list[dict]],
|
|
83
|
+
) -> Optional[list[dict]]:
|
|
84
|
+
"""Filter tool calls, removing those with missing required params.
|
|
85
|
+
|
|
86
|
+
Invalid tool calls are dropped with a WARNING log.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
tool_calls: Parsed tool calls (may be None).
|
|
90
|
+
tools: Original tool definitions (may be None).
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Filtered list, or None if empty/input was None.
|
|
94
|
+
"""
|
|
95
|
+
if not tool_calls or not tools:
|
|
96
|
+
return tool_calls
|
|
97
|
+
|
|
98
|
+
index = build_required_params_index(tools)
|
|
99
|
+
valid = [tc for tc in tool_calls if validate_tool_call(tc, index)]
|
|
100
|
+
|
|
101
|
+
if len(valid) < len(tool_calls):
|
|
102
|
+
dropped = len(tool_calls) - len(valid)
|
|
103
|
+
logger.warning(f"Dropped {dropped} invalid tool call(s) with missing required params")
|
|
104
|
+
|
|
105
|
+
return valid if valid else None
|
|
File without changes
|