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.
Files changed (30) hide show
  1. {code_lm-0.1.4/src/code_lm.egg-info → code_lm-0.2.0}/PKG-INFO +2 -2
  2. {code_lm-0.1.4 → code_lm-0.2.0}/pyproject.toml +2 -2
  3. {code_lm-0.1.4 → code_lm-0.2.0}/setup.py +1 -1
  4. {code_lm-0.1.4 → code_lm-0.2.0/src/code_lm.egg-info}/PKG-INFO +2 -2
  5. code_lm-0.2.0/src/gemini_cli/models/openrouter.py +567 -0
  6. {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/tools/tree_tool.py +1 -2
  7. code_lm-0.1.4/src/gemini_cli/models/openrouter.py +0 -203
  8. {code_lm-0.1.4 → code_lm-0.2.0}/MANIFEST.in +0 -0
  9. {code_lm-0.1.4 → code_lm-0.2.0}/README.md +0 -0
  10. {code_lm-0.1.4 → code_lm-0.2.0}/setup.cfg +0 -0
  11. {code_lm-0.1.4 → code_lm-0.2.0}/src/code_lm.egg-info/SOURCES.txt +0 -0
  12. {code_lm-0.1.4 → code_lm-0.2.0}/src/code_lm.egg-info/dependency_links.txt +0 -0
  13. {code_lm-0.1.4 → code_lm-0.2.0}/src/code_lm.egg-info/entry_points.txt +0 -0
  14. {code_lm-0.1.4 → code_lm-0.2.0}/src/code_lm.egg-info/requires.txt +0 -0
  15. {code_lm-0.1.4 → code_lm-0.2.0}/src/code_lm.egg-info/top_level.txt +0 -0
  16. {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/__init__.py +0 -0
  17. {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/config.py +0 -0
  18. {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/main.py +0 -0
  19. {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/models/__init__.py +0 -0
  20. {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/models/gemini.py +0 -0
  21. {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/tools/__init__.py +0 -0
  22. {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/tools/base.py +0 -0
  23. {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/tools/directory_tools.py +0 -0
  24. {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/tools/file_tools.py +0 -0
  25. {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/tools/quality_tools.py +0 -0
  26. {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/tools/summarizer_tool.py +0 -0
  27. {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/tools/system_tools.py +0 -0
  28. {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/tools/task_complete_tool.py +0 -0
  29. {code_lm-0.1.4 → code_lm-0.2.0}/src/gemini_cli/tools/test_runner.py +0 -0
  30. {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.1.4
4
- Summary: An AI coding assistant CLI using OpenRouter and various LLM models.
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.1.4"
7
+ version = "0.2.0"
8
8
  authors = [
9
9
  { name="Panagiotis897", email="orion256business@gmail.com" }
10
10
  ]
11
- description = "An AI coding assistant CLI using OpenRouter and various LLM models."
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.1.4",
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.1.4
4
- Summary: An AI coding assistant CLI using OpenRouter and various LLM models.
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