code-lm 0.1.4__tar.gz → 0.2.0__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.
- {code_lm-0.1.4/src/code_lm.egg-info → code_lm-0.2.0}/PKG-INFO +2 -2
- {code_lm-0.1.4 → code_lm-0.2.0}/pyproject.toml +2 -2
- {code_lm-0.1.4 → code_lm-0.2.0}/setup.py +1 -1
- {code_lm-0.1.4 → code_lm-0.2.0/src/code_lm.egg-info}/PKG-INFO +2 -2
- code_lm-0.2.0/src/gemini_cli/models/openrouter.py +567 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/tools/tree_tool.py +1 -2
- code_lm-0.1.4/src/gemini_cli/models/openrouter.py +0 -203
- {code_lm-0.1.4 → code_lm-0.2.0}/MANIFEST.in +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/README.md +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/setup.cfg +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/code_lm.egg-info/SOURCES.txt +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/code_lm.egg-info/dependency_links.txt +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/code_lm.egg-info/entry_points.txt +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/code_lm.egg-info/requires.txt +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/code_lm.egg-info/top_level.txt +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/__init__.py +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/config.py +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/main.py +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/models/__init__.py +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/models/gemini.py +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/tools/__init__.py +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/tools/base.py +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/tools/directory_tools.py +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/tools/file_tools.py +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/tools/quality_tools.py +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/tools/summarizer_tool.py +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/tools/system_tools.py +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/tools/task_complete_tool.py +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/tools/test_runner.py +0 -0
- {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/utils.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: code-lm
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: An AI coding assistant
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: An AI coding assistant using various LLM models.
|
|
5
5
|
Home-page: https://github.com/Panagiotis897/lm-code
|
|
6
6
|
Author: Panagiotis897
|
|
7
7
|
Author-email: Panagiotis897 <orion256business@gmail.com>
|
|
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "code-lm"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Panagiotis897", email="orion256business@gmail.com" }
|
|
10
10
|
]
|
|
11
|
-
description = "An AI coding assistant
|
|
11
|
+
description = "An AI coding assistant using various LLM models."
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.9"
|
|
14
14
|
license = { file = "LICENSE" }
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="code-lm",
|
|
5
|
-
version="0.
|
|
5
|
+
version="0.2.0",
|
|
6
6
|
description="A CLI for interacting with various LLM models using OpenRouter and other APIs.",
|
|
7
7
|
long_description=open("README.md").read(),
|
|
8
8
|
long_description_content_type="text/markdown",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: code-lm
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: An AI coding assistant
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: An AI coding assistant using various LLM models.
|
|
5
5
|
Home-page: https://github.com/Panagiotis897/lm-code
|
|
6
6
|
Author: Panagiotis897
|
|
7
7
|
Author-email: Panagiotis897 <orion256business@gmail.com>
|
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenRouter model integration for the CLI tool.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
from typing import Optional, Dict, List, Any, Union
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
import questionary
|
|
13
|
+
|
|
14
|
+
from ..utils import count_tokens
|
|
15
|
+
from ..tools import get_tool, AVAILABLE_TOOLS
|
|
16
|
+
|
|
17
|
+
# Setup logging
|
|
18
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s')
|
|
19
|
+
log = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
MAX_AGENT_ITERATIONS = 10
|
|
22
|
+
FALLBACK_MODEL = "qwen/qwen-2.5-coder-32b-instruct:free"
|
|
23
|
+
CONTEXT_TRUNCATION_THRESHOLD_TOKENS = 12000 # Approximate token limit for Qwen model
|
|
24
|
+
|
|
25
|
+
# Function definitions for tools
|
|
26
|
+
class FunctionDeclaration:
|
|
27
|
+
def __init__(self, name: str, description: str, parameters: Dict):
|
|
28
|
+
self.name = name
|
|
29
|
+
self.description = description
|
|
30
|
+
self.parameters = parameters
|
|
31
|
+
|
|
32
|
+
class OpenRouterTool:
|
|
33
|
+
def __init__(self, function_declarations):
|
|
34
|
+
self.function_declarations = function_declarations
|
|
35
|
+
|
|
36
|
+
def list_available_models(api_key):
|
|
37
|
+
try:
|
|
38
|
+
headers = {
|
|
39
|
+
"Authorization": f"Bearer {api_key}",
|
|
40
|
+
"Content-Type": "application/json"
|
|
41
|
+
}
|
|
42
|
+
response = requests.get("https://openrouter.ai/api/v1/models", headers=headers)
|
|
43
|
+
response.raise_for_status()
|
|
44
|
+
models_data = response.json()
|
|
45
|
+
|
|
46
|
+
openrouter_models = []
|
|
47
|
+
for model in models_data.get("data", []):
|
|
48
|
+
model_info = {
|
|
49
|
+
"name": model.get("id", ""),
|
|
50
|
+
"display_name": model.get("name", ""),
|
|
51
|
+
"description": model.get("description", ""),
|
|
52
|
+
"context_length": model.get("context_length", 0)
|
|
53
|
+
}
|
|
54
|
+
openrouter_models.append(model_info)
|
|
55
|
+
return openrouter_models
|
|
56
|
+
except Exception as e:
|
|
57
|
+
log.error(f"Error listing models: {str(e)}")
|
|
58
|
+
return [{"error": str(e)}]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class OpenRouterModel:
|
|
62
|
+
"""Interface for OpenRouter models with function calling support."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, api_key: str, console: Console, model_name: str ="qwen/qwen-2.5-coder-32b-instruct:free"):
|
|
65
|
+
"""Initialize the OpenRouter model interface."""
|
|
66
|
+
self.api_key = api_key
|
|
67
|
+
self.initial_model_name = model_name
|
|
68
|
+
self.current_model_name = model_name
|
|
69
|
+
self.console = console
|
|
70
|
+
self.base_url = "https://openrouter.ai/api/v1/chat/completions"
|
|
71
|
+
|
|
72
|
+
self.headers = {
|
|
73
|
+
"Authorization": f"Bearer {api_key}",
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
"HTTP-Referer": "https://github.com/Panagiotis897/lm-code", # Optional: site URL
|
|
76
|
+
"X-Title": "LM Code" # Optional: title of your application
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# --- Tool Definition ---
|
|
80
|
+
self.function_declarations = self._create_tool_definitions()
|
|
81
|
+
self.openrouter_tools = self._convert_to_openrouter_tools() if self.function_declarations else None
|
|
82
|
+
# ---
|
|
83
|
+
|
|
84
|
+
# --- System Prompt (Native Functions & Planning) ---
|
|
85
|
+
self.system_instruction = self._create_system_prompt()
|
|
86
|
+
# ---
|
|
87
|
+
|
|
88
|
+
# --- Initialize Persistent History ---
|
|
89
|
+
self.chat_history = [
|
|
90
|
+
{'role': 'system', 'content': self.system_instruction},
|
|
91
|
+
{'role': 'assistant', 'content': "Okay, I'm ready. Provide the directory context and your request."}
|
|
92
|
+
]
|
|
93
|
+
log.info("Initialized persistent chat history.")
|
|
94
|
+
# ---
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
# Test the connection to make sure the model is valid
|
|
98
|
+
self._test_model_connection()
|
|
99
|
+
log.info("OpenRouterModel initialized successfully (Native Function Calling Agent Loop).")
|
|
100
|
+
except Exception as e:
|
|
101
|
+
log.error(f"Fatal error initializing OpenRouter model '{self.current_model_name}': {str(e)}", exc_info=True)
|
|
102
|
+
raise Exception(f"Could not initialize OpenRouter model: {e}") from e
|
|
103
|
+
|
|
104
|
+
def _test_model_connection(self):
|
|
105
|
+
"""Test the connection to the model."""
|
|
106
|
+
log.info(f"Testing connection to model: {self.current_model_name}")
|
|
107
|
+
try:
|
|
108
|
+
# Simple test message
|
|
109
|
+
payload = {
|
|
110
|
+
"model": self.current_model_name,
|
|
111
|
+
"messages": [
|
|
112
|
+
{"role": "system", "content": "You are a helpful assistant."},
|
|
113
|
+
{"role": "user", "content": "Test connection"}
|
|
114
|
+
],
|
|
115
|
+
"max_tokens": 10
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
response = requests.post(
|
|
119
|
+
self.base_url,
|
|
120
|
+
headers=self.headers,
|
|
121
|
+
json=payload
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if response.status_code != 200:
|
|
125
|
+
log.error(f"API returned status code {response.status_code}: {response.text}")
|
|
126
|
+
raise Exception(f"API error: {response.status_code} - {response.text}")
|
|
127
|
+
|
|
128
|
+
log.info(f"Model connection test successful: {self.current_model_name}")
|
|
129
|
+
except Exception as e:
|
|
130
|
+
log.error(f"Connection test failed: {e}")
|
|
131
|
+
raise e
|
|
132
|
+
|
|
133
|
+
def get_available_models(self):
|
|
134
|
+
return list_available_models(self.api_key)
|
|
135
|
+
|
|
136
|
+
def _convert_to_openrouter_tools(self):
|
|
137
|
+
"""Convert function declarations to OpenRouter tools format."""
|
|
138
|
+
tools = []
|
|
139
|
+
for func_decl in self.function_declarations:
|
|
140
|
+
tools.append({
|
|
141
|
+
"type": "function",
|
|
142
|
+
"function": {
|
|
143
|
+
"name": func_decl.name,
|
|
144
|
+
"description": func_decl.description,
|
|
145
|
+
"parameters": func_decl.parameters
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
return tools
|
|
149
|
+
|
|
150
|
+
# --- Native Function Calling Agent Loop ---
|
|
151
|
+
def generate(self, prompt: str) -> str | None:
|
|
152
|
+
logging.info(f"Agent Loop - Processing prompt: '{prompt[:100]}...' using model '{self.current_model_name}'")
|
|
153
|
+
original_user_prompt = prompt
|
|
154
|
+
if prompt.startswith('/'):
|
|
155
|
+
command = prompt.split()[0].lower()
|
|
156
|
+
# Handle commands like /compact here eventually
|
|
157
|
+
if command in ['/exit', '/help']:
|
|
158
|
+
logging.info(f"Handled command: {command}")
|
|
159
|
+
return None # Or return specific help text
|
|
160
|
+
|
|
161
|
+
# === Step 1: Mandatory Orientation ===
|
|
162
|
+
orientation_context = ""
|
|
163
|
+
ls_result = None # Initialize to None
|
|
164
|
+
try:
|
|
165
|
+
logging.info("Performing mandatory orientation (ls).")
|
|
166
|
+
ls_tool = get_tool("ls")
|
|
167
|
+
if ls_tool:
|
|
168
|
+
# Clear args just in case, assuming ls takes none for basic root listing
|
|
169
|
+
ls_result = ls_tool.execute()
|
|
170
|
+
# === START DEBUG LOGGING ===
|
|
171
|
+
log.debug(f"LsTool raw result:\n---\n{ls_result}\n---")
|
|
172
|
+
# === END DEBUG LOGGING ===
|
|
173
|
+
log.info(f"Orientation ls result length: {len(ls_result) if ls_result else 0}")
|
|
174
|
+
self.console.print(f"[dim]Directory context acquired via 'ls'.[/dim]")
|
|
175
|
+
orientation_context = f"Current directory contents (from initial `ls`):\n```\n{ls_result}\n```\n"
|
|
176
|
+
else:
|
|
177
|
+
log.error("CRITICAL: Could not find 'ls' tool for mandatory orientation.")
|
|
178
|
+
# Stop execution if ls tool is missing - fundamental context is unavailable
|
|
179
|
+
return "Error: The essential 'ls' tool is missing. Cannot proceed."
|
|
180
|
+
|
|
181
|
+
except Exception as orient_error:
|
|
182
|
+
log.error(f"Error during mandatory orientation (ls): {orient_error}", exc_info=True)
|
|
183
|
+
error_message = f"Error during initial directory scan: {orient_error}"
|
|
184
|
+
orientation_context = f"{error_message}\n"
|
|
185
|
+
self.console.print(f"[bold red]Error getting initial directory listing: {orient_error}[/bold red]")
|
|
186
|
+
# Stop execution if initial ls fails - context is unreliable
|
|
187
|
+
return f"Error: Failed to get initial directory listing. Cannot reliably proceed. Details: {orient_error}"
|
|
188
|
+
|
|
189
|
+
# === Step 2: Prepare Initial User Turn ===
|
|
190
|
+
# Combine orientation with the actual user request
|
|
191
|
+
turn_input_prompt = f"{orientation_context}\nUser request: {original_user_prompt}"
|
|
192
|
+
|
|
193
|
+
# Add this combined input to the PERSISTENT history
|
|
194
|
+
self.chat_history.append({'role': 'user', 'content': turn_input_prompt})
|
|
195
|
+
# === START DEBUG LOGGING ===
|
|
196
|
+
log.debug(f"Prepared turn_input_prompt (sent to LLM):\n---\n{turn_input_prompt}\n---")
|
|
197
|
+
# === END DEBUG LOGGING ===
|
|
198
|
+
self._manage_context_window() # Truncate *before* sending the first request
|
|
199
|
+
|
|
200
|
+
iteration_count = 0
|
|
201
|
+
task_completed = False
|
|
202
|
+
final_summary = None
|
|
203
|
+
last_text_response = "No response generated." # Fallback text
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
while iteration_count < MAX_AGENT_ITERATIONS:
|
|
207
|
+
iteration_count += 1
|
|
208
|
+
logging.info(f"Agent Loop Iteration {iteration_count}/{MAX_AGENT_ITERATIONS}")
|
|
209
|
+
|
|
210
|
+
# === Call LLM with History and Tools ===
|
|
211
|
+
llm_response = None
|
|
212
|
+
try:
|
|
213
|
+
logging.info(f"Sending request to LLM ({self.current_model_name}). History length: {len(self.chat_history)} turns.")
|
|
214
|
+
# === ADD STATUS FOR LLM CALL ===
|
|
215
|
+
with self.console.status(f"[yellow]Assistant thinking ({self.current_model_name})...", spinner="dots"):
|
|
216
|
+
# Prepare the payload for the API request
|
|
217
|
+
payload = {
|
|
218
|
+
"model": self.current_model_name,
|
|
219
|
+
"messages": self.chat_history,
|
|
220
|
+
"temperature": 0.4,
|
|
221
|
+
"top_p": 0.95,
|
|
222
|
+
"max_tokens": 2000
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# Add tools if available
|
|
226
|
+
if self.openrouter_tools:
|
|
227
|
+
payload["tools"] = self.openrouter_tools
|
|
228
|
+
|
|
229
|
+
# Send the API request
|
|
230
|
+
response = requests.post(
|
|
231
|
+
self.base_url,
|
|
232
|
+
headers=self.headers,
|
|
233
|
+
json=payload
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Check for successful response
|
|
237
|
+
if response.status_code != 200:
|
|
238
|
+
log.error(f"API returned status code {response.status_code}: {response.text}")
|
|
239
|
+
raise Exception(f"API error: {response.status_code} - {response.text}")
|
|
240
|
+
|
|
241
|
+
# Parse the response
|
|
242
|
+
llm_response = response.json()
|
|
243
|
+
# === END STATUS ===
|
|
244
|
+
|
|
245
|
+
# === START DEBUG LOGGING ===
|
|
246
|
+
log.debug(f"RAW OpenRouter Response Object (Iter {iteration_count}): {llm_response}")
|
|
247
|
+
# === END DEBUG LOGGING ===
|
|
248
|
+
|
|
249
|
+
# Extract the response
|
|
250
|
+
if not llm_response.get("choices"):
|
|
251
|
+
log.error(f"LLM response had no choices. Response: {llm_response}")
|
|
252
|
+
last_text_response = "(Agent received response with no choices)"
|
|
253
|
+
task_completed = True
|
|
254
|
+
final_summary = last_text_response
|
|
255
|
+
break
|
|
256
|
+
|
|
257
|
+
response_message = llm_response["choices"][0]["message"]
|
|
258
|
+
|
|
259
|
+
# Handle function call responses
|
|
260
|
+
if "tool_calls" in response_message and response_message["tool_calls"]:
|
|
261
|
+
# Process the function call
|
|
262
|
+
function_call = response_message["tool_calls"][0]["function"]
|
|
263
|
+
tool_name = function_call["name"]
|
|
264
|
+
tool_args = json.loads(function_call["arguments"])
|
|
265
|
+
log.info(f"LLM requested Function Call: {tool_name} with args: {tool_args}")
|
|
266
|
+
|
|
267
|
+
# Add the function call to history
|
|
268
|
+
self.chat_history.append(response_message)
|
|
269
|
+
self._manage_context_window()
|
|
270
|
+
|
|
271
|
+
# Execute the tool
|
|
272
|
+
tool_result = ""
|
|
273
|
+
tool_error = False
|
|
274
|
+
user_rejected = False # Flag for user rejection
|
|
275
|
+
|
|
276
|
+
# --- HUMAN IN THE LOOP CONFIRMATION ---
|
|
277
|
+
if tool_name in ["edit", "create_file"]:
|
|
278
|
+
file_path = tool_args.get("file_path", "(unknown file)")
|
|
279
|
+
content = tool_args.get("content") # Get content, might be None
|
|
280
|
+
old_string = tool_args.get("old_string") # Get old_string
|
|
281
|
+
new_string = tool_args.get("new_string") # Get new_string
|
|
282
|
+
|
|
283
|
+
panel_content = f"[bold yellow]Proposed change:[/bold yellow]\n[cyan]Tool:[/cyan] {tool_name}\n[cyan]File:[/cyan] {file_path}\n"
|
|
284
|
+
|
|
285
|
+
if content is not None: # Case 1: Full content provided
|
|
286
|
+
# Prepare content preview (limit length?)
|
|
287
|
+
preview_lines = content.splitlines()
|
|
288
|
+
max_preview_lines = 30 # Limit preview for long content
|
|
289
|
+
if len(preview_lines) > max_preview_lines:
|
|
290
|
+
content_preview = "\n".join(preview_lines[:max_preview_lines]) + f"\n... ({len(preview_lines) - max_preview_lines} more lines)"
|
|
291
|
+
else:
|
|
292
|
+
content_preview = content
|
|
293
|
+
panel_content += f"\n[bold]Content Preview:[/bold]\n---\n{content_preview}\n---"
|
|
294
|
+
|
|
295
|
+
elif old_string is not None and new_string is not None: # Case 2: Replacement
|
|
296
|
+
max_snippet = 50 # Max chars to show for old/new strings
|
|
297
|
+
old_snippet = old_string[:max_snippet] + ('...' if len(old_string) > max_snippet else '')
|
|
298
|
+
new_snippet = new_string[:max_snippet] + ('...' if len(new_string) > max_snippet else '')
|
|
299
|
+
panel_content += f"\n[bold]Action:[/bold] Replace occurrence of:\n---\n{old_snippet}\n---\n[bold]With:[/bold]\n---\n{new_snippet}\n---"
|
|
300
|
+
else: # Case 3: Other/Unknown edit args
|
|
301
|
+
panel_content += "\n[italic](Preview not available for this edit type)"
|
|
302
|
+
|
|
303
|
+
# Use Rich Panel for better presentation
|
|
304
|
+
self.console.print(Panel(
|
|
305
|
+
panel_content, # Use the constructed content
|
|
306
|
+
title="Confirm File Modification",
|
|
307
|
+
border_style="red",
|
|
308
|
+
expand=False
|
|
309
|
+
))
|
|
310
|
+
|
|
311
|
+
# Use questionary for confirmation
|
|
312
|
+
confirmed = questionary.confirm(
|
|
313
|
+
"Apply this change?",
|
|
314
|
+
default=False, # Default to No
|
|
315
|
+
auto_enter=False # Require Enter key press
|
|
316
|
+
).ask()
|
|
317
|
+
|
|
318
|
+
# Handle case where user might Ctrl+C during prompt
|
|
319
|
+
if confirmed is None:
|
|
320
|
+
log.warning("User cancelled confirmation prompt.")
|
|
321
|
+
tool_result = f"User cancelled confirmation for {tool_name} on {file_path}."
|
|
322
|
+
user_rejected = True
|
|
323
|
+
elif not confirmed: # User explicitly selected No
|
|
324
|
+
log.warning(f"User rejected proposed action: {tool_name} on {file_path}")
|
|
325
|
+
tool_result = f"User rejected the proposed {tool_name} operation on {file_path}."
|
|
326
|
+
user_rejected = True # Set flag to skip execution
|
|
327
|
+
else: # User selected Yes
|
|
328
|
+
log.info(f"User confirmed action: {tool_name} on {file_path}")
|
|
329
|
+
# --- END CONFIRMATION ---
|
|
330
|
+
|
|
331
|
+
# Only execute if not rejected by user
|
|
332
|
+
if not user_rejected:
|
|
333
|
+
status_msg = f"Executing {tool_name}"
|
|
334
|
+
if tool_args:
|
|
335
|
+
status_msg += f" ({', '.join([f'{k}={str(v)[:30]}...' if len(str(v))>30 else f'{k}={v}' for k,v in tool_args.items()])})"
|
|
336
|
+
|
|
337
|
+
with self.console.status(f"[yellow]{status_msg}...", spinner="dots"):
|
|
338
|
+
try:
|
|
339
|
+
tool_instance = get_tool(tool_name)
|
|
340
|
+
if tool_instance:
|
|
341
|
+
log.debug(f"Executing tool '{tool_name}' with arguments: {tool_args}")
|
|
342
|
+
tool_result = tool_instance.execute(**tool_args)
|
|
343
|
+
log.info(f"Tool '{tool_name}' executed. Result length: {len(str(tool_result)) if tool_result else 0}")
|
|
344
|
+
log.debug(f"Tool '{tool_name}' result: {str(tool_result)[:500]}...")
|
|
345
|
+
else:
|
|
346
|
+
log.error(f"Tool '{tool_name}' not found.")
|
|
347
|
+
tool_result = f"Error: Tool '{tool_name}' is not available."
|
|
348
|
+
tool_error = True
|
|
349
|
+
except Exception as tool_exec_error:
|
|
350
|
+
log.error(f"Error executing tool '{tool_name}' with args {tool_args}: {tool_exec_error}", exc_info=True)
|
|
351
|
+
tool_result = f"Error executing tool {tool_name}: {str(tool_exec_error)}"
|
|
352
|
+
tool_error = True
|
|
353
|
+
|
|
354
|
+
# --- Print Executed/Error INSIDE the status block ---
|
|
355
|
+
if tool_error:
|
|
356
|
+
self.console.print(f"[red] -> Error executing {tool_name}: {str(tool_result)[:100]}...[/red]")
|
|
357
|
+
else:
|
|
358
|
+
self.console.print(f"[dim] -> Executed {tool_name}[/dim]")
|
|
359
|
+
# --- End Status Block ---
|
|
360
|
+
|
|
361
|
+
# === Check for Task Completion Signal via Tool Call ===
|
|
362
|
+
if tool_name == "task_complete":
|
|
363
|
+
log.info("Task completion signaled by 'task_complete' function call.")
|
|
364
|
+
task_completed = True
|
|
365
|
+
final_summary = tool_result # The result of task_complete IS the summary
|
|
366
|
+
# We break *after* adding the tool response below
|
|
367
|
+
|
|
368
|
+
# Add tool response to history
|
|
369
|
+
tool_response = {
|
|
370
|
+
"role": "tool",
|
|
371
|
+
"tool_call_id": response_message["tool_calls"][0]["id"],
|
|
372
|
+
"name": tool_name,
|
|
373
|
+
"content": str(tool_result)
|
|
374
|
+
}
|
|
375
|
+
self.chat_history.append(tool_response)
|
|
376
|
+
self._manage_context_window()
|
|
377
|
+
|
|
378
|
+
if task_completed:
|
|
379
|
+
break # Exit loop after task_complete result is in history
|
|
380
|
+
else:
|
|
381
|
+
continue # Continue loop to let LLM react to tool result
|
|
382
|
+
|
|
383
|
+
# Handle text responses
|
|
384
|
+
elif "content" in response_message and response_message["content"]:
|
|
385
|
+
llm_text = response_message["content"]
|
|
386
|
+
log.info(f"LLM returned text (Iter {iteration_count}): {llm_text[:100]}...")
|
|
387
|
+
|
|
388
|
+
# Add text response to history
|
|
389
|
+
self.chat_history.append(response_message)
|
|
390
|
+
self._manage_context_window()
|
|
391
|
+
|
|
392
|
+
last_text_response = llm_text.strip()
|
|
393
|
+
task_completed = True # Treat text response as completion
|
|
394
|
+
final_summary = last_text_response # Use the text as the summary
|
|
395
|
+
break # Exit the loop
|
|
396
|
+
|
|
397
|
+
else:
|
|
398
|
+
# No actionable content
|
|
399
|
+
log.warning("LLM response contained no actionable content.")
|
|
400
|
+
last_text_response = "(Agent received response with no actionable content)"
|
|
401
|
+
task_completed = True # Treat as completion to avoid loop errors
|
|
402
|
+
final_summary = last_text_response
|
|
403
|
+
break # Exit loop
|
|
404
|
+
|
|
405
|
+
except Exception as e:
|
|
406
|
+
# Handle other errors during the API call or response processing
|
|
407
|
+
log.error(f"Error during Agent Loop: {e}", exc_info=True)
|
|
408
|
+
# Clean history
|
|
409
|
+
if self.chat_history[-1]["role"] == "user":
|
|
410
|
+
self.chat_history.pop()
|
|
411
|
+
return f"Error during agent processing: {e}"
|
|
412
|
+
|
|
413
|
+
# === End Agent Loop ===
|
|
414
|
+
|
|
415
|
+
# === Handle Final Output ===
|
|
416
|
+
if task_completed and final_summary:
|
|
417
|
+
log.info("Agent loop finished. Returning final summary.")
|
|
418
|
+
return final_summary.strip() # Return the summary from task_complete or final text
|
|
419
|
+
elif iteration_count >= MAX_AGENT_ITERATIONS:
|
|
420
|
+
log.warning(f"Agent loop terminated after reaching max iterations ({MAX_AGENT_ITERATIONS}).")
|
|
421
|
+
# Try to get the last text response
|
|
422
|
+
last_model_response_text = self._find_last_model_text(self.chat_history)
|
|
423
|
+
timeout_message = f"(Task exceeded max iterations ({MAX_AGENT_ITERATIONS}). Last text from model was: {last_model_response_text})"
|
|
424
|
+
return timeout_message.strip()
|
|
425
|
+
else:
|
|
426
|
+
# This case should be less likely now
|
|
427
|
+
log.error("Agent loop exited unexpectedly.")
|
|
428
|
+
last_model_response_text = self._find_last_model_text(self.chat_history)
|
|
429
|
+
return f"(Agent loop finished unexpectedly. Last model text: {last_model_response_text})"
|
|
430
|
+
|
|
431
|
+
except Exception as e:
|
|
432
|
+
log.error(f"Error during Agent Loop: {str(e)}", exc_info=True)
|
|
433
|
+
return f"An unexpected error occurred during the agent process: {str(e)}"
|
|
434
|
+
|
|
435
|
+
# --- Context Management ---
|
|
436
|
+
def _manage_context_window(self):
|
|
437
|
+
"""Basic context window management."""
|
|
438
|
+
# Basic management based on turn count
|
|
439
|
+
MAX_HISTORY_TURNS = 20 # Keep ~N pairs of user/assistant turns + initial setup + tool calls/responses
|
|
440
|
+
# OpenRouter format uses 'role' of system, user, assistant, tool
|
|
441
|
+
if len(self.chat_history) > (MAX_HISTORY_TURNS * 2 + 1): # +1 for system message
|
|
442
|
+
log.warning(f"Chat history length ({len(self.chat_history)}) exceeded threshold. Truncating.")
|
|
443
|
+
# Keep system message (idx 0)
|
|
444
|
+
keep_count = MAX_HISTORY_TURNS * 2 # Keep N rounds (user+assistant or user+tool)
|
|
445
|
+
keep_from_index = len(self.chat_history) - keep_count
|
|
446
|
+
self.chat_history = [self.chat_history[0]] + self.chat_history[keep_from_index:]
|
|
447
|
+
log.info(f"History truncated to {len(self.chat_history)} items.")
|
|
448
|
+
|
|
449
|
+
# --- Tool Definition Helper ---
|
|
450
|
+
def _create_tool_definitions(self) -> list[FunctionDeclaration] | None:
|
|
451
|
+
"""Dynamically create FunctionDeclarations from AVAILABLE_TOOLS."""
|
|
452
|
+
declarations = []
|
|
453
|
+
for tool_name, tool_instance in AVAILABLE_TOOLS.items():
|
|
454
|
+
if hasattr(tool_instance, 'get_function_declaration'):
|
|
455
|
+
declaration = tool_instance.get_function_declaration()
|
|
456
|
+
if isinstance(declaration, dict): # Handle unexpected dictionary type
|
|
457
|
+
if "name" in declaration and "description" in declaration:
|
|
458
|
+
schema = {
|
|
459
|
+
"type": "object",
|
|
460
|
+
"properties": declaration.get("parameters", {}).get("properties", {}),
|
|
461
|
+
"required": declaration.get("parameters", {}).get("required", [])
|
|
462
|
+
}
|
|
463
|
+
declarations.append(FunctionDeclaration(
|
|
464
|
+
name=declaration["name"],
|
|
465
|
+
description=declaration["description"],
|
|
466
|
+
parameters=schema
|
|
467
|
+
))
|
|
468
|
+
log.debug(f"Generated FunctionDeclaration (dict) for tool: {tool_name}")
|
|
469
|
+
else:
|
|
470
|
+
log.warning(f"Unexpected dictionary format for tool declaration: {declaration}")
|
|
471
|
+
elif declaration: # Handle regular objects with attributes
|
|
472
|
+
schema = {
|
|
473
|
+
"type": "object",
|
|
474
|
+
"properties": {},
|
|
475
|
+
"required": []
|
|
476
|
+
}
|
|
477
|
+
# Convert declaration parameters to schema
|
|
478
|
+
if hasattr(declaration, 'parameters') and declaration.parameters:
|
|
479
|
+
if hasattr(declaration.parameters, 'properties'):
|
|
480
|
+
for prop_name, prop_details in declaration.parameters.properties.items():
|
|
481
|
+
schema["properties"][prop_name] = {
|
|
482
|
+
"type": getattr(prop_details, 'type', "string"),
|
|
483
|
+
"description": getattr(prop_details, 'description', "")
|
|
484
|
+
}
|
|
485
|
+
if hasattr(declaration.parameters, 'required') and declaration.parameters.required:
|
|
486
|
+
schema["required"] = declaration.parameters.required
|
|
487
|
+
|
|
488
|
+
declarations.append(FunctionDeclaration(
|
|
489
|
+
name=getattr(declaration, 'name', 'unknown'),
|
|
490
|
+
description=getattr(declaration, 'description', ''),
|
|
491
|
+
parameters=schema
|
|
492
|
+
))
|
|
493
|
+
log.debug(f"Generated FunctionDeclaration (object) for tool: {tool_name}")
|
|
494
|
+
else:
|
|
495
|
+
log.warning(f"Tool {tool_name} has 'get_function_declaration' but it returned None.")
|
|
496
|
+
else:
|
|
497
|
+
log.warning(f"Tool {tool_name} does not have a 'get_function_declaration' method. Skipping.")
|
|
498
|
+
|
|
499
|
+
log.info(f"Created {len(declarations)} function declarations for native tool use.")
|
|
500
|
+
return declarations if declarations else None
|
|
501
|
+
# --- System Prompt Helper ---
|
|
502
|
+
def _create_system_prompt(self) -> str:
|
|
503
|
+
"""Creates the system prompt, emphasizing native functions and planning."""
|
|
504
|
+
# Use docstrings from tools if possible for descriptions
|
|
505
|
+
tool_descriptions = []
|
|
506
|
+
if self.function_declarations:
|
|
507
|
+
for func_decl in self.function_declarations:
|
|
508
|
+
# Format the parameters
|
|
509
|
+
params_str = ""
|
|
510
|
+
if hasattr(func_decl, 'parameters') and func_decl.parameters and 'properties' in func_decl.parameters:
|
|
511
|
+
params_list = []
|
|
512
|
+
required_args = func_decl.parameters.get('required', [])
|
|
513
|
+
|
|
514
|
+
for prop, details in func_decl.parameters['properties'].items():
|
|
515
|
+
prop_type = details.get('type', 'UNKNOWN')
|
|
516
|
+
prop_desc = details.get('description', '')
|
|
517
|
+
|
|
518
|
+
suffix = "" if prop in required_args else "?" # Indicate optional args
|
|
519
|
+
|
|
520
|
+
params_list.append(f"{prop}: {prop_type}{suffix} # {prop_desc}")
|
|
521
|
+
|
|
522
|
+
params_str = ", ".join(params_list)
|
|
523
|
+
|
|
524
|
+
desc = func_decl.description if hasattr(func_decl, 'description') else "(No description provided)"
|
|
525
|
+
tool_descriptions.append(f"- `{func_decl.name}({params_str})`: {desc}")
|
|
526
|
+
else:
|
|
527
|
+
tool_descriptions.append(" - (No tools available with function declarations)")
|
|
528
|
+
|
|
529
|
+
tool_list_str = "\n".join(tool_descriptions)
|
|
530
|
+
|
|
531
|
+
# System prompt based on the original but with OpenRouter specifics
|
|
532
|
+
return f"""You are LM Code, an AI coding assistant running in a CLI environment.
|
|
533
|
+
Your goal is to help the user with their coding tasks by understanding their request, planning the necessary steps, and using the available tools via **native function calls**.
|
|
534
|
+
|
|
535
|
+
Available Tools (Use ONLY these via function calls):
|
|
536
|
+
{tool_list_str}
|
|
537
|
+
|
|
538
|
+
Workflow:
|
|
539
|
+
1. **Analyze & Plan:** Understand the user's request based on the provided directory context (`ls` output) and the request itself. For non-trivial tasks, **first outline a brief plan** of the steps needed.
|
|
540
|
+
2. **Execute:** If a plan is not needed or after outlining the plan, make the **first necessary function call** to execute the next step (e.g., `view` a file, `edit` a file, `grep` for text, `tree` for directory structure).
|
|
541
|
+
3. **Observe:** You will receive the result of the function call (or a message indicating user rejection). Use this result to inform your next step.
|
|
542
|
+
4. **Repeat:** Based on the result, make the next function call required to achieve the user's goal. Continue calling functions sequentially until the task is complete.
|
|
543
|
+
5. **Complete:** Once the *entire* task is finished, **you MUST call the `task_complete` function**, providing a concise summary of what was done in the `summary` argument.
|
|
544
|
+
* The `summary` argument MUST accurately reflect the final outcome (success, partial success, error, or what was done).
|
|
545
|
+
* Format the summary using **Markdown** for readability (e.g., use backticks for filenames `like_this.py` or commands `like this`).
|
|
546
|
+
* If code was generated or modified, the summary **MUST** contain the **actual, specific commands** needed to run or test the result (e.g., show `pip install Flask` and `python app.py`).
|
|
547
|
+
|
|
548
|
+
Important Rules:
|
|
549
|
+
* **Use Native Functions:** ONLY interact with tools by making function calls as defined above. Do NOT output tool calls as text.
|
|
550
|
+
* **Sequential Calls:** Call functions one at a time. You will get the result back before deciding the next step. Do not try to chain calls in one turn.
|
|
551
|
+
* **Initial Context Handling:** When the user asks a general question about the codebase contents, your **first step** should be to use tools like `ls`, `tree` or `find` to gather this information.
|
|
552
|
+
* **Accurate Context Reporting:** When asked about directory contents, accurately list or summarize **all** relevant files and directories shown in the `ls` or `tree` output.
|
|
553
|
+
* **Handling Explanations:** If the user asks *how* to do something or requests instructions, provide the explanation directly in a text response without calling a function.
|
|
554
|
+
* **Proactive Assistance:** When providing instructions that culminate in a specific execution command, offer to run it for the user.
|
|
555
|
+
* **Planning First:** For tasks requiring multiple steps, explain your plan briefly in text *before* the first function call.
|
|
556
|
+
* **Precise Edits:** When editing files, prefer viewing the relevant section first, then use exact `old_string`/`new_string` arguments if possible.
|
|
557
|
+
* **Task Completion Signal:** ALWAYS finish action-oriented tasks by calling `task_complete(summary=...)`.
|
|
558
|
+
|
|
559
|
+
The user's first message will contain initial directory context and their request."""
|
|
560
|
+
|
|
561
|
+
# --- Find Last Text Helper ---
|
|
562
|
+
def _find_last_model_text(self, history: list) -> str:
|
|
563
|
+
"""Finds the last text content sent by the assistant in the history."""
|
|
564
|
+
for i in range(len(history) - 1, -1, -1):
|
|
565
|
+
if history[i]['role'] == 'assistant' and 'content' in history[i] and history[i]['content']:
|
|
566
|
+
return history[i]['content'].strip()
|
|
567
|
+
return "(No previous text response found)"
|
|
@@ -3,7 +3,6 @@ Tool for displaying directory structure using the 'tree' command.
|
|
|
3
3
|
"""
|
|
4
4
|
import subprocess
|
|
5
5
|
import logging
|
|
6
|
-
from google.generativeai.types import FunctionDeclaration, Tool
|
|
7
6
|
|
|
8
7
|
from .base import BaseTool
|
|
9
8
|
|
|
@@ -89,4 +88,4 @@ class TreeTool(BaseTool):
|
|
|
89
88
|
return f"Error: Tree command timed out for path '{target_path}'. The directory might be too large or complex."
|
|
90
89
|
except Exception as e:
|
|
91
90
|
log.exception(f"An unexpected error occurred while executing tree command for path '{target_path}': {e}")
|
|
92
|
-
return f"An unexpected error occurred while executing tree: {str(e)}"
|
|
91
|
+
return f"An unexpected error occurred while executing tree: {str(e)}"
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
OpenRouter model integration for the CLI tool.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import requests
|
|
6
|
-
import json
|
|
7
|
-
import logging
|
|
8
|
-
import time
|
|
9
|
-
from typing import Optional, Dict, List, Any
|
|
10
|
-
from rich.console import Console
|
|
11
|
-
from rich.panel import Panel
|
|
12
|
-
|
|
13
|
-
from ..utils import count_tokens
|
|
14
|
-
from ..tools import get_tool, AVAILABLE_TOOLS
|
|
15
|
-
|
|
16
|
-
# Setup logging
|
|
17
|
-
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s')
|
|
18
|
-
log = logging.getLogger(__name__)
|
|
19
|
-
|
|
20
|
-
MAX_AGENT_ITERATIONS = 10
|
|
21
|
-
FALLBACK_MODEL = "qwen/qwen-2.5-coder-32b-instruct:free"
|
|
22
|
-
CONTEXT_TRUNCATION_THRESHOLD_TOKENS = 12000 # Approximate token limit for Qwen model
|
|
23
|
-
|
|
24
|
-
# Function definitions for tools
|
|
25
|
-
class FunctionDeclaration:
|
|
26
|
-
def __init__(self, name: str, description: str, parameters: Dict):
|
|
27
|
-
self.name = name
|
|
28
|
-
self.description = description
|
|
29
|
-
self.parameters = parameters
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class OpenRouterModel:
|
|
33
|
-
"""Interface for OpenRouter models with function calling support."""
|
|
34
|
-
|
|
35
|
-
def __init__(self, api_key: str, console: Console, model_name: str = FALLBACK_MODEL):
|
|
36
|
-
"""Initialize the OpenRouter model interface."""
|
|
37
|
-
self.api_key = api_key
|
|
38
|
-
self.initial_model_name = model_name
|
|
39
|
-
self.current_model_name = model_name
|
|
40
|
-
self.console = console
|
|
41
|
-
self.base_url = "https://openrouter.ai/api/v1/chat/completions"
|
|
42
|
-
|
|
43
|
-
self.headers = {
|
|
44
|
-
"Authorization": f"Bearer {api_key}",
|
|
45
|
-
"Content-Type": "application/json",
|
|
46
|
-
"HTTP-Referer": "https://github.com/Panagiotis897/lm-code", # Optional: site URL
|
|
47
|
-
"X-Title": "LM Code" # Optional: title of your application
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
# --- Tool Definition ---
|
|
51
|
-
self.function_declarations = self._create_tool_definitions()
|
|
52
|
-
self.openrouter_tools = self._convert_to_openrouter_tools() if self.function_declarations else None
|
|
53
|
-
# ---
|
|
54
|
-
|
|
55
|
-
# --- System Prompt (Native Functions & Planning) ---
|
|
56
|
-
self.system_instruction = self._create_system_prompt()
|
|
57
|
-
# ---
|
|
58
|
-
|
|
59
|
-
# --- Initialize Persistent History ---
|
|
60
|
-
self.chat_history = [
|
|
61
|
-
{'role': 'system', 'content': self.system_instruction},
|
|
62
|
-
{'role': 'assistant', 'content': "Okay, I'm ready. Provide the directory context and your request."}
|
|
63
|
-
]
|
|
64
|
-
log.info("Initialized persistent chat history.")
|
|
65
|
-
# ---
|
|
66
|
-
|
|
67
|
-
try:
|
|
68
|
-
# Test the connection to make sure the model is valid
|
|
69
|
-
self._test_model_connection()
|
|
70
|
-
log.info("OpenRouterModel initialized successfully (Native Function Calling Agent Loop).")
|
|
71
|
-
except Exception as e:
|
|
72
|
-
log.error(f"Fatal error initializing OpenRouter model '{self.current_model_name}': {str(e)}", exc_info=True)
|
|
73
|
-
raise Exception(f"Could not initialize OpenRouter model: {e}") from e
|
|
74
|
-
|
|
75
|
-
def _test_model_connection(self):
|
|
76
|
-
"""Test the connection to the model."""
|
|
77
|
-
log.info(f"Testing connection to model: {self.current_model_name}")
|
|
78
|
-
try:
|
|
79
|
-
# Simple test message
|
|
80
|
-
payload = {
|
|
81
|
-
"model": self.current_model_name,
|
|
82
|
-
"messages": [
|
|
83
|
-
{"role": "system", "content": "You are a helpful assistant."},
|
|
84
|
-
{"role": "user", "content": "Test connection"}
|
|
85
|
-
],
|
|
86
|
-
"max_tokens": 10
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
response = requests.post(
|
|
90
|
-
self.base_url,
|
|
91
|
-
headers=self.headers,
|
|
92
|
-
json=payload
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
if response.status_code != 200:
|
|
96
|
-
log.error(f"API returned status code {response.status_code}: {response.text}")
|
|
97
|
-
raise Exception(f"API error: {response.status_code} - {response.text}")
|
|
98
|
-
|
|
99
|
-
log.info(f"Model connection test successful: {self.current_model_name}")
|
|
100
|
-
except Exception as e:
|
|
101
|
-
log.error(f"Connection test failed: {e}")
|
|
102
|
-
raise e
|
|
103
|
-
|
|
104
|
-
def get_available_models(self):
|
|
105
|
-
"""List available models for the API key."""
|
|
106
|
-
try:
|
|
107
|
-
headers = {
|
|
108
|
-
"Authorization": f"Bearer {self.api_key}",
|
|
109
|
-
"Content-Type": "application/json"
|
|
110
|
-
}
|
|
111
|
-
response = requests.get("https://openrouter.ai/api/v1/models", headers=headers)
|
|
112
|
-
response.raise_for_status()
|
|
113
|
-
models_data = response.json()
|
|
114
|
-
|
|
115
|
-
openrouter_models = []
|
|
116
|
-
for model in models_data.get("data", []):
|
|
117
|
-
model_info = {
|
|
118
|
-
"name": model.get("id", ""),
|
|
119
|
-
"display_name": model.get("name", ""),
|
|
120
|
-
"description": model.get("description", ""),
|
|
121
|
-
"context_length": model.get("context_length", 0)
|
|
122
|
-
}
|
|
123
|
-
openrouter_models.append(model_info)
|
|
124
|
-
return openrouter_models
|
|
125
|
-
except Exception as e:
|
|
126
|
-
log.error(f"Error listing models: {str(e)}")
|
|
127
|
-
return [{"error": str(e)}]
|
|
128
|
-
|
|
129
|
-
def _convert_to_openrouter_tools(self):
|
|
130
|
-
"""Convert function declarations to OpenRouter tools format."""
|
|
131
|
-
tools = []
|
|
132
|
-
for func_decl in self.function_declarations:
|
|
133
|
-
tools.append({
|
|
134
|
-
"type": "function",
|
|
135
|
-
"function": {
|
|
136
|
-
"name": func_decl.name,
|
|
137
|
-
"description": func_decl.description,
|
|
138
|
-
"parameters": func_decl.parameters
|
|
139
|
-
}
|
|
140
|
-
})
|
|
141
|
-
return tools
|
|
142
|
-
|
|
143
|
-
def _create_tool_definitions(self) -> list[FunctionDeclaration] | None:
|
|
144
|
-
"""
|
|
145
|
-
Dynamically create FunctionDeclarations from AVAILABLE_TOOLS.
|
|
146
|
-
"""
|
|
147
|
-
declarations = []
|
|
148
|
-
for tool_name, tool_instance in AVAILABLE_TOOLS.items():
|
|
149
|
-
if hasattr(tool_instance, 'get_function_declaration'):
|
|
150
|
-
declaration_data = tool_instance.get_function_declaration()
|
|
151
|
-
if declaration_data:
|
|
152
|
-
try:
|
|
153
|
-
openrouter_declaration = FunctionDeclaration(
|
|
154
|
-
name=declaration_data['name'],
|
|
155
|
-
description=declaration_data.get('description', ''),
|
|
156
|
-
parameters=declaration_data.get('parameters', {})
|
|
157
|
-
)
|
|
158
|
-
declarations.append(openrouter_declaration)
|
|
159
|
-
log.debug(f"Generated FunctionDeclaration for tool: {tool_name}")
|
|
160
|
-
except Exception as e:
|
|
161
|
-
log.error(f"Error processing tool {tool_name}: {e}", exc_info=True)
|
|
162
|
-
else:
|
|
163
|
-
log.warning(f"Tool {tool_name} has 'get_function_declaration' but it returned None.")
|
|
164
|
-
else:
|
|
165
|
-
log.warning(f"Tool {tool_name} does not have a 'get_function_declaration' method. Skipping.")
|
|
166
|
-
|
|
167
|
-
log.info(f"Created {len(declarations)} function declarations for native tool use.")
|
|
168
|
-
return declarations if declarations else None
|
|
169
|
-
|
|
170
|
-
def _create_system_prompt(self) -> str:
|
|
171
|
-
"""Creates the system prompt, emphasizing native functions and planning."""
|
|
172
|
-
tool_descriptions = []
|
|
173
|
-
if self.function_declarations:
|
|
174
|
-
for func_decl in self.function_declarations:
|
|
175
|
-
params_str = ", ".join(
|
|
176
|
-
f"{name}: {details.get('type', 'unknown')} # {details.get('description', '')}"
|
|
177
|
-
for name, details in func_decl.parameters.get('properties', {}).items()
|
|
178
|
-
)
|
|
179
|
-
description = func_decl.description or "(No description provided)"
|
|
180
|
-
tool_descriptions.append(f"- `{func_decl.name}({params_str})`: {description}")
|
|
181
|
-
else:
|
|
182
|
-
tool_descriptions.append(" - (No tools available with function declarations)")
|
|
183
|
-
|
|
184
|
-
tool_list_str = "\n".join(tool_descriptions)
|
|
185
|
-
|
|
186
|
-
return f"""You are LM Code, an AI coding assistant running in a CLI environment.
|
|
187
|
-
Your goal is to help the user with their coding tasks by understanding their request, planning the necessary steps, and using the available tools via **native function calls**.
|
|
188
|
-
|
|
189
|
-
Available Tools (Use ONLY these via function calls):
|
|
190
|
-
{tool_list_str}
|
|
191
|
-
|
|
192
|
-
Workflow:
|
|
193
|
-
1. **Analyze & Plan:** Understand the user's request based on the provided directory context (`ls` output) and the request itself. For non-trivial tasks, **first outline a brief plan** of the steps needed.
|
|
194
|
-
2. **Execute:** If a plan is not needed or after outlining the plan, make the **first necessary function call** to execute the next step (e.g., `view` a file, `edit` a file, `grep` for text, `tree` for directory structure).
|
|
195
|
-
3. **Observe:** You will receive the result of the function call (or a message indicating user rejection). Use this result to inform your next step.
|
|
196
|
-
4. **Repeat:** Based on the result, make the next function call required to achieve the user's goal. Continue calling functions sequentially until the task is complete.
|
|
197
|
-
5. **Complete:** Once the *entire* task is finished, **you MUST call the `task_complete` function**, providing a concise summary of what was done in the `summary` argument.
|
|
198
|
-
|
|
199
|
-
Important Rules:
|
|
200
|
-
* **Use Native Functions:** ONLY interact with tools by making function calls as defined above. Do NOT output tool calls as text.
|
|
201
|
-
* **Sequential Calls:** Call functions one at a time. You will get the result back before deciding the next step. Do not try to chain calls in one turn.
|
|
202
|
-
* **Initial Context Handling:** When the user asks a general question about the codebase contents, your **first step** should be to use tools like `ls`, `tree` or `find` to gather this information.
|
|
203
|
-
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|