code-lm 0.1.1__tar.gz → 0.1.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.
- {code_lm-0.1.1/src/code_lm.egg-info → code_lm-0.1.2}/PKG-INFO +1 -1
- {code_lm-0.1.1 → code_lm-0.1.2}/pyproject.toml +1 -1
- {code_lm-0.1.1 → code_lm-0.1.2}/setup.py +1 -1
- {code_lm-0.1.1 → code_lm-0.1.2/src/code_lm.egg-info}/PKG-INFO +1 -1
- code_lm-0.1.2/src/gemini_cli/models/gemini.py +43 -0
- code_lm-0.1.2/src/gemini_cli/tools/__init__.py +80 -0
- code_lm-0.1.1/src/gemini_cli/models/gemini.py +0 -552
- code_lm-0.1.1/src/gemini_cli/tools/__init__.py +0 -85
- {code_lm-0.1.1 → code_lm-0.1.2}/MANIFEST.in +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/README.md +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/setup.cfg +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/src/code_lm.egg-info/SOURCES.txt +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/src/code_lm.egg-info/dependency_links.txt +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/src/code_lm.egg-info/entry_points.txt +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/src/code_lm.egg-info/requires.txt +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/src/code_lm.egg-info/top_level.txt +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/src/gemini_cli/__init__.py +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/src/gemini_cli/config.py +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/src/gemini_cli/main.py +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/src/gemini_cli/models/__init__.py +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/src/gemini_cli/models/openrouter.py +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/src/gemini_cli/tools/base.py +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/src/gemini_cli/tools/directory_tools.py +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/src/gemini_cli/tools/file_tools.py +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/src/gemini_cli/tools/quality_tools.py +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/src/gemini_cli/tools/summarizer_tool.py +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/src/gemini_cli/tools/system_tools.py +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/src/gemini_cli/tools/task_complete_tool.py +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/src/gemini_cli/tools/test_runner.py +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/src/gemini_cli/tools/tree_tool.py +0 -0
- {code_lm-0.1.1 → code_lm-0.1.2}/src/gemini_cli/utils.py +0 -0
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="code-lm", # The name of your package
|
|
5
|
-
version="0.1.
|
|
5
|
+
version="0.1.2", # Package version
|
|
6
6
|
description="A CLI for interacting with various LLM models using OpenRouter and other APIs.",
|
|
7
7
|
long_description=open("README.md").read(), # Read from README.md
|
|
8
8
|
long_description_content_type="text/markdown", # Markdown format
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gemini model integration for the CLI tool.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
import questionary
|
|
10
|
+
|
|
11
|
+
from ..utils import count_tokens
|
|
12
|
+
from ..tools import get_tool, AVAILABLE_TOOLS
|
|
13
|
+
|
|
14
|
+
# Setup logging (basic config, consider moving to main.py)
|
|
15
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s')
|
|
16
|
+
log = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
MAX_AGENT_ITERATIONS = 10
|
|
19
|
+
CONTEXT_TRUNCATION_THRESHOLD_TOKENS = 800000 # Example token limit
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GeminiModel:
|
|
23
|
+
"""Interface for Gemini models using native function calling agentic loop."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, console: Console):
|
|
26
|
+
"""Initialize the Gemini model interface."""
|
|
27
|
+
self.console = console
|
|
28
|
+
|
|
29
|
+
# --- Tool Definition ---
|
|
30
|
+
self.function_declarations = None # Tools have been removed
|
|
31
|
+
# ---
|
|
32
|
+
|
|
33
|
+
# --- System Prompt (Native Functions & Planning) ---
|
|
34
|
+
self.system_instruction = "Initialize system prompt."
|
|
35
|
+
# ---
|
|
36
|
+
|
|
37
|
+
# --- Initialize Persistent History ---
|
|
38
|
+
self.chat_history = [
|
|
39
|
+
{'role': 'user', 'parts': [self.system_instruction]},
|
|
40
|
+
{'role': 'model', 'parts': ["Okay, I'm ready. Provide the directory context and your request."]}
|
|
41
|
+
]
|
|
42
|
+
log.info("Initialized persistent chat history.")
|
|
43
|
+
# ---
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tools module initialization. Registers all available tools.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from .base import BaseTool
|
|
7
|
+
from .file_tools import ViewTool, EditTool, GrepTool, GlobTool
|
|
8
|
+
from .directory_tools import LsTool
|
|
9
|
+
|
|
10
|
+
# --- Tool Imports ---
|
|
11
|
+
try:
|
|
12
|
+
from .system_tools import BashTool
|
|
13
|
+
bash_tool_available = True
|
|
14
|
+
except ImportError:
|
|
15
|
+
logging.warning("system_tools.BashTool not found. Disabled.")
|
|
16
|
+
bash_tool_available = False
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from .task_complete_tool import TaskCompleteTool
|
|
20
|
+
task_complete_available = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
logging.warning("task_complete_tool.TaskCompleteTool not found. Disabled.")
|
|
23
|
+
task_complete_available = False
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
from .directory_tools import CreateDirectoryTool
|
|
27
|
+
create_dir_available = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
logging.warning("directory_tools.CreateDirectoryTool not found. Disabled.")
|
|
30
|
+
create_dir_available = False
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from .quality_tools import LinterCheckerTool, FormatterTool
|
|
34
|
+
quality_tools_available = True
|
|
35
|
+
except ImportError:
|
|
36
|
+
logging.warning("quality_tools not found or missing classes. Disabled.")
|
|
37
|
+
quality_tools_available = False
|
|
38
|
+
|
|
39
|
+
# End Tool Imports
|
|
40
|
+
|
|
41
|
+
from .tree_tool import TreeTool
|
|
42
|
+
|
|
43
|
+
# AVAILABLE_TOOLS maps tool names (strings) to the actual tool classes.
|
|
44
|
+
# Start with core, guaranteed tools
|
|
45
|
+
AVAILABLE_TOOLS = {
|
|
46
|
+
"view": ViewTool,
|
|
47
|
+
"edit": EditTool,
|
|
48
|
+
"ls": LsTool,
|
|
49
|
+
"grep": GrepTool,
|
|
50
|
+
"glob": GlobTool,
|
|
51
|
+
"create_directory": CreateDirectoryTool,
|
|
52
|
+
"task_complete": TaskCompleteTool,
|
|
53
|
+
"tree": TreeTool,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Conditionally add tools based on successful imports
|
|
57
|
+
if bash_tool_available:
|
|
58
|
+
AVAILABLE_TOOLS["bash"] = BashTool
|
|
59
|
+
if quality_tools_available:
|
|
60
|
+
AVAILABLE_TOOLS["linter_checker"] = LinterCheckerTool
|
|
61
|
+
AVAILABLE_TOOLS["formatter"] = FormatterTool
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_tool(name: str) -> BaseTool | None:
|
|
65
|
+
"""
|
|
66
|
+
Retrieves an *instance* of the tool class based on its name.
|
|
67
|
+
"""
|
|
68
|
+
tool_class = AVAILABLE_TOOLS.get(name)
|
|
69
|
+
if tool_class:
|
|
70
|
+
try:
|
|
71
|
+
return tool_class()
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logging.error(f"Error instantiating tool '{name}': {e}", exc_info=True)
|
|
74
|
+
return None
|
|
75
|
+
else:
|
|
76
|
+
logging.warning(f"Tool '{name}' not found in AVAILABLE_TOOLS.")
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
logging.info(f"Tools initialized. Available: {list(AVAILABLE_TOOLS.keys())}")
|
|
@@ -1,552 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Gemini model integration for the CLI tool.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import google.generativeai as genai
|
|
6
|
-
from google.generativeai import protos
|
|
7
|
-
from google.generativeai.types import FunctionDeclaration, Tool
|
|
8
|
-
import logging
|
|
9
|
-
import time
|
|
10
|
-
from rich.console import Console
|
|
11
|
-
from rich.panel import Panel
|
|
12
|
-
import questionary
|
|
13
|
-
|
|
14
|
-
# Import exceptions for specific error handling if needed later
|
|
15
|
-
from google.api_core.exceptions import ResourceExhausted
|
|
16
|
-
|
|
17
|
-
from ..utils import count_tokens
|
|
18
|
-
from ..tools import get_tool, AVAILABLE_TOOLS
|
|
19
|
-
|
|
20
|
-
# Setup logging (basic config, consider moving to main.py)
|
|
21
|
-
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s')
|
|
22
|
-
log = logging.getLogger(__name__)
|
|
23
|
-
|
|
24
|
-
MAX_AGENT_ITERATIONS = 10
|
|
25
|
-
FALLBACK_MODEL = "gemini-1.5-pro-latest"
|
|
26
|
-
CONTEXT_TRUNCATION_THRESHOLD_TOKENS = 800000 # Example token limit
|
|
27
|
-
|
|
28
|
-
def list_available_models(api_key):
|
|
29
|
-
try:
|
|
30
|
-
genai.configure(api_key=api_key)
|
|
31
|
-
models = genai.list_models()
|
|
32
|
-
gemini_models = []
|
|
33
|
-
for model in models:
|
|
34
|
-
# Filter for models supporting generateContent to avoid chat-only models if needed
|
|
35
|
-
if 'generateContent' in model.supported_generation_methods:
|
|
36
|
-
model_info = { "name": model.name, "display_name": model.display_name, "description": model.description, "supported_generation_methods": model.supported_generation_methods }
|
|
37
|
-
gemini_models.append(model_info)
|
|
38
|
-
return gemini_models
|
|
39
|
-
except Exception as e:
|
|
40
|
-
log.error(f"Error listing models: {str(e)}")
|
|
41
|
-
return [{"error": str(e)}]
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class GeminiModel:
|
|
45
|
-
"""Interface for Gemini models using native function calling agentic loop."""
|
|
46
|
-
|
|
47
|
-
def __init__(self, api_key: str, console: Console, model_name: str ="gemini-2.5-pro-exp-03-25"):
|
|
48
|
-
"""Initialize the Gemini model interface."""
|
|
49
|
-
self.api_key = api_key
|
|
50
|
-
self.initial_model_name = model_name
|
|
51
|
-
self.current_model_name = model_name
|
|
52
|
-
self.console = console
|
|
53
|
-
genai.configure(api_key=api_key)
|
|
54
|
-
|
|
55
|
-
self.generation_config = genai.types.GenerationConfig(temperature=0.4, top_p=0.95, top_k=40)
|
|
56
|
-
self.safety_settings = { "HARASSMENT": "BLOCK_MEDIUM_AND_ABOVE", "HATE": "BLOCK_MEDIUM_AND_ABOVE", "SEXUAL": "BLOCK_MEDIUM_AND_ABOVE", "DANGEROUS": "BLOCK_MEDIUM_AND_ABOVE" }
|
|
57
|
-
|
|
58
|
-
# --- Tool Definition ---
|
|
59
|
-
self.function_declarations = self._create_tool_definitions()
|
|
60
|
-
self.gemini_tools = Tool(function_declarations=self.function_declarations) if self.function_declarations else None
|
|
61
|
-
# ---
|
|
62
|
-
|
|
63
|
-
# --- System Prompt (Native Functions & Planning) ---
|
|
64
|
-
self.system_instruction = self._create_system_prompt()
|
|
65
|
-
# ---
|
|
66
|
-
|
|
67
|
-
# --- Initialize Persistent History ---
|
|
68
|
-
self.chat_history = [
|
|
69
|
-
{'role': 'user', 'parts': [self.system_instruction]},
|
|
70
|
-
{'role': 'model', 'parts': ["Okay, I'm ready. Provide the directory context and your request."]}
|
|
71
|
-
]
|
|
72
|
-
log.info("Initialized persistent chat history.")
|
|
73
|
-
# ---
|
|
74
|
-
|
|
75
|
-
try:
|
|
76
|
-
self._initialize_model_instance() # Creates self.model
|
|
77
|
-
log.info("GeminiModel initialized successfully (Native Function Calling Agent Loop).")
|
|
78
|
-
except Exception as e:
|
|
79
|
-
log.error(f"Fatal error initializing Gemini model '{self.current_model_name}': {str(e)}", exc_info=True)
|
|
80
|
-
raise Exception(f"Could not initialize Gemini model: {e}") from e
|
|
81
|
-
|
|
82
|
-
def _initialize_model_instance(self):
|
|
83
|
-
"""Helper to create the GenerativeModel instance."""
|
|
84
|
-
log.info(f"Initializing model instance: {self.current_model_name}")
|
|
85
|
-
try:
|
|
86
|
-
# Pass system instruction here, tools are passed during generate_content
|
|
87
|
-
self.model = genai.GenerativeModel(
|
|
88
|
-
model_name=self.current_model_name,
|
|
89
|
-
generation_config=self.generation_config,
|
|
90
|
-
safety_settings=self.safety_settings,
|
|
91
|
-
system_instruction=self.system_instruction
|
|
92
|
-
)
|
|
93
|
-
log.info(f"Model instance '{self.current_model_name}' created successfully.")
|
|
94
|
-
except Exception as init_err:
|
|
95
|
-
log.error(f"Failed to create model instance for '{self.current_model_name}': {init_err}", exc_info=True)
|
|
96
|
-
raise init_err
|
|
97
|
-
|
|
98
|
-
def get_available_models(self):
|
|
99
|
-
return list_available_models(self.api_key)
|
|
100
|
-
|
|
101
|
-
# --- Native Function Calling Agent Loop ---
|
|
102
|
-
def generate(self, prompt: str) -> str | None:
|
|
103
|
-
logging.info(f"Agent Loop - Processing prompt: '{prompt[:100]}...' using model '{self.current_model_name}'")
|
|
104
|
-
original_user_prompt = prompt
|
|
105
|
-
if prompt.startswith('/'):
|
|
106
|
-
command = prompt.split()[0].lower()
|
|
107
|
-
# Handle commands like /compact here eventually
|
|
108
|
-
if command in ['/exit', '/help']:
|
|
109
|
-
logging.info(f"Handled command: {command}")
|
|
110
|
-
return None # Or return specific help text
|
|
111
|
-
|
|
112
|
-
# === Step 1: Mandatory Orientation ===
|
|
113
|
-
orientation_context = ""
|
|
114
|
-
ls_result = None # Initialize to None
|
|
115
|
-
try:
|
|
116
|
-
logging.info("Performing mandatory orientation (ls).")
|
|
117
|
-
ls_tool = get_tool("ls")
|
|
118
|
-
if ls_tool:
|
|
119
|
-
# Clear args just in case, assuming ls takes none for basic root listing
|
|
120
|
-
ls_result = ls_tool.execute()
|
|
121
|
-
# === START DEBUG LOGGING ===
|
|
122
|
-
log.debug(f"LsTool raw result:\n---\n{ls_result}\n---")
|
|
123
|
-
# === END DEBUG LOGGING ===
|
|
124
|
-
log.info(f"Orientation ls result length: {len(ls_result) if ls_result else 0}") # Changed from logging full result
|
|
125
|
-
self.console.print(f"[dim]Directory context acquired via 'ls'.[/dim]")
|
|
126
|
-
orientation_context = f"Current directory contents (from initial `ls`):\n```\n{ls_result}\n```\n"
|
|
127
|
-
else:
|
|
128
|
-
log.error("CRITICAL: Could not find 'ls' tool for mandatory orientation.")
|
|
129
|
-
# Stop execution if ls tool is missing - fundamental context is unavailable
|
|
130
|
-
return "Error: The essential 'ls' tool is missing. Cannot proceed."
|
|
131
|
-
|
|
132
|
-
except Exception as orient_error:
|
|
133
|
-
log.error(f"Error during mandatory orientation (ls): {orient_error}", exc_info=True)
|
|
134
|
-
error_message = f"Error during initial directory scan: {orient_error}"
|
|
135
|
-
orientation_context = f"{error_message}\n"
|
|
136
|
-
self.console.print(f"[bold red]Error getting initial directory listing: {orient_error}[/bold red]")
|
|
137
|
-
# Stop execution if initial ls fails - context is unreliable
|
|
138
|
-
return f"Error: Failed to get initial directory listing. Cannot reliably proceed. Details: {orient_error}"
|
|
139
|
-
|
|
140
|
-
# === Step 2: Prepare Initial User Turn ===
|
|
141
|
-
# Combine orientation with the actual user request
|
|
142
|
-
turn_input_prompt = f"{orientation_context}\nUser request: {original_user_prompt}"
|
|
143
|
-
|
|
144
|
-
# Add this combined input to the PERSISTENT history
|
|
145
|
-
self.chat_history.append({'role': 'user', 'parts': [turn_input_prompt]})
|
|
146
|
-
# === START DEBUG LOGGING ===
|
|
147
|
-
log.debug(f"Prepared turn_input_prompt (sent to LLM):\n---\n{turn_input_prompt}\n---")
|
|
148
|
-
# === END DEBUG LOGGING ===
|
|
149
|
-
self._manage_context_window() # Truncate *before* sending the first request
|
|
150
|
-
|
|
151
|
-
iteration_count = 0
|
|
152
|
-
task_completed = False
|
|
153
|
-
final_summary = None
|
|
154
|
-
last_text_response = "No response generated." # Fallback text
|
|
155
|
-
|
|
156
|
-
try:
|
|
157
|
-
while iteration_count < MAX_AGENT_ITERATIONS:
|
|
158
|
-
iteration_count += 1
|
|
159
|
-
logging.info(f"Agent Loop Iteration {iteration_count}/{MAX_AGENT_ITERATIONS}")
|
|
160
|
-
|
|
161
|
-
# === Call LLM with History and Tools ===
|
|
162
|
-
llm_response = None
|
|
163
|
-
try:
|
|
164
|
-
logging.info(f"Sending request to LLM ({self.current_model_name}). History length: {len(self.chat_history)} turns.")
|
|
165
|
-
# === ADD STATUS FOR LLM CALL ===
|
|
166
|
-
with self.console.status(f"[yellow]Assistant thinking ({self.current_model_name})...", spinner="dots"):
|
|
167
|
-
# Pass the available tools to the generate_content call
|
|
168
|
-
llm_response = self.model.generate_content(
|
|
169
|
-
self.chat_history,
|
|
170
|
-
generation_config=self.generation_config,
|
|
171
|
-
tools=[self.gemini_tools] if self.gemini_tools else None
|
|
172
|
-
)
|
|
173
|
-
# === END STATUS ===
|
|
174
|
-
|
|
175
|
-
# === START DEBUG LOGGING ===
|
|
176
|
-
log.debug(f"RAW Gemini Response Object (Iter {iteration_count}): {llm_response}")
|
|
177
|
-
# === END DEBUG LOGGING ===
|
|
178
|
-
|
|
179
|
-
# Extract the response part (candidate)
|
|
180
|
-
# Add checks for empty candidates or parts
|
|
181
|
-
if not llm_response.candidates:
|
|
182
|
-
log.error(f"LLM response had no candidates. Response: {llm_response}")
|
|
183
|
-
last_text_response = "(Agent received response with no candidates)"
|
|
184
|
-
task_completed = True; final_summary = last_text_response; break
|
|
185
|
-
|
|
186
|
-
response_candidate = llm_response.candidates[0]
|
|
187
|
-
if not response_candidate.content or not response_candidate.content.parts:
|
|
188
|
-
log.error(f"LLM response candidate had no content or parts. Candidate: {response_candidate}")
|
|
189
|
-
last_text_response = "(Agent received response candidate with no content/parts)"
|
|
190
|
-
task_completed = True; final_summary = last_text_response; break
|
|
191
|
-
|
|
192
|
-
# --- REVISED LOOP LOGIC FOR MULTI-PART HANDLING ---
|
|
193
|
-
function_call_part_to_execute = None
|
|
194
|
-
text_response_buffer = ""
|
|
195
|
-
processed_function_call_in_turn = False # Flag to ensure only one function call is processed per turn
|
|
196
|
-
|
|
197
|
-
# Iterate through all parts in the response
|
|
198
|
-
for part in response_candidate.content.parts:
|
|
199
|
-
if hasattr(part, 'function_call') and part.function_call and not processed_function_call_in_turn:
|
|
200
|
-
function_call = part.function_call
|
|
201
|
-
tool_name = function_call.name
|
|
202
|
-
tool_args = dict(function_call.args) if function_call.args else {}
|
|
203
|
-
log.info(f"LLM requested Function Call: {tool_name} with args: {tool_args}")
|
|
204
|
-
|
|
205
|
-
# Add the function *call* part to history immediately
|
|
206
|
-
self.chat_history.append({'role': 'model', 'parts': [part]})
|
|
207
|
-
self._manage_context_window()
|
|
208
|
-
|
|
209
|
-
# Store details for execution after processing all parts
|
|
210
|
-
function_call_part_to_execute = part
|
|
211
|
-
processed_function_call_in_turn = True # Mark that we found and will process a function call
|
|
212
|
-
# Don't break here yet, process other parts (like text) first for history/logging
|
|
213
|
-
|
|
214
|
-
elif hasattr(part, 'text') and part.text:
|
|
215
|
-
llm_text = part.text
|
|
216
|
-
log.info(f"LLM returned text part (Iter {iteration_count}): {llm_text[:100]}...")
|
|
217
|
-
text_response_buffer += llm_text + "\n" # Append text parts
|
|
218
|
-
# Add the text response part to history
|
|
219
|
-
self.chat_history.append({'role': 'model', 'parts': [part]})
|
|
220
|
-
self._manage_context_window()
|
|
221
|
-
|
|
222
|
-
else:
|
|
223
|
-
log.warning(f"LLM returned unexpected response part (Iter {iteration_count}): {part}")
|
|
224
|
-
# Add it to history anyway?
|
|
225
|
-
self.chat_history.append({'role': 'model', 'parts': [part]})
|
|
226
|
-
self._manage_context_window()
|
|
227
|
-
|
|
228
|
-
# --- Now, decide action based on processed parts ---
|
|
229
|
-
if function_call_part_to_execute:
|
|
230
|
-
# === Execute the Tool === (Using stored details)
|
|
231
|
-
function_call = function_call_part_to_execute.function_call # Get the stored call
|
|
232
|
-
tool_name = function_call.name
|
|
233
|
-
tool_args = dict(function_call.args) if function_call.args else {}
|
|
234
|
-
|
|
235
|
-
tool_result = ""
|
|
236
|
-
tool_error = False
|
|
237
|
-
user_rejected = False # Flag for user rejection
|
|
238
|
-
|
|
239
|
-
# --- HUMAN IN THE LOOP CONFIRMATION ---
|
|
240
|
-
if tool_name in ["edit", "create_file"]:
|
|
241
|
-
file_path = tool_args.get("file_path", "(unknown file)")
|
|
242
|
-
content = tool_args.get("content") # Get content, might be None
|
|
243
|
-
old_string = tool_args.get("old_string") # Get old_string
|
|
244
|
-
new_string = tool_args.get("new_string") # Get new_string
|
|
245
|
-
|
|
246
|
-
panel_content = f"[bold yellow]Proposed change:[/bold yellow]\n[cyan]Tool:[/cyan] {tool_name}\n[cyan]File:[/cyan] {file_path}\n"
|
|
247
|
-
|
|
248
|
-
if content is not None: # Case 1: Full content provided
|
|
249
|
-
# Prepare content preview (limit length?)
|
|
250
|
-
preview_lines = content.splitlines()
|
|
251
|
-
max_preview_lines = 30 # Limit preview for long content
|
|
252
|
-
if len(preview_lines) > max_preview_lines:
|
|
253
|
-
content_preview = "\n".join(preview_lines[:max_preview_lines]) + f"\n... ({len(preview_lines) - max_preview_lines} more lines)"
|
|
254
|
-
else:
|
|
255
|
-
content_preview = content
|
|
256
|
-
panel_content += f"\n[bold]Content Preview:[/bold]\n---\n{content_preview}\n---"
|
|
257
|
-
|
|
258
|
-
elif old_string is not None and new_string is not None: # Case 2: Replacement
|
|
259
|
-
max_snippet = 50 # Max chars to show for old/new strings
|
|
260
|
-
old_snippet = old_string[:max_snippet] + ('...' if len(old_string) > max_snippet else '')
|
|
261
|
-
new_snippet = new_string[:max_snippet] + ('...' if len(new_string) > max_snippet else '')
|
|
262
|
-
panel_content += f"\n[bold]Action:[/bold] Replace occurrence of:\n---\n{old_snippet}\n---\n[bold]With:[/bold]\n---\n{new_snippet}\n---"
|
|
263
|
-
else: # Case 3: Other/Unknown edit args
|
|
264
|
-
panel_content += "\n[italic](Preview not available for this edit type)"
|
|
265
|
-
|
|
266
|
-
# Use Rich Panel for better presentation
|
|
267
|
-
self.console.print(Panel(
|
|
268
|
-
panel_content, # Use the constructed content
|
|
269
|
-
title="Confirm File Modification",
|
|
270
|
-
border_style="red",
|
|
271
|
-
expand=False
|
|
272
|
-
))
|
|
273
|
-
|
|
274
|
-
# Use questionary for confirmation
|
|
275
|
-
confirmed = questionary.confirm(
|
|
276
|
-
"Apply this change?",
|
|
277
|
-
default=False, # Default to No
|
|
278
|
-
auto_enter=False # Require Enter key press
|
|
279
|
-
).ask()
|
|
280
|
-
|
|
281
|
-
# Handle case where user might Ctrl+C during prompt
|
|
282
|
-
if confirmed is None:
|
|
283
|
-
log.warning("User cancelled confirmation prompt.")
|
|
284
|
-
tool_result = f"User cancelled confirmation for {tool_name} on {file_path}."
|
|
285
|
-
user_rejected = True
|
|
286
|
-
elif not confirmed: # User explicitly selected No
|
|
287
|
-
log.warning(f"User rejected proposed action: {tool_name} on {file_path}")
|
|
288
|
-
tool_result = f"User rejected the proposed {tool_name} operation on {file_path}."
|
|
289
|
-
user_rejected = True # Set flag to skip execution
|
|
290
|
-
else: # User selected Yes
|
|
291
|
-
log.info(f"User confirmed action: {tool_name} on {file_path}")
|
|
292
|
-
# --- END CONFIRMATION ---
|
|
293
|
-
|
|
294
|
-
# Only execute if not rejected by user
|
|
295
|
-
if not user_rejected:
|
|
296
|
-
status_msg = f"Executing {tool_name}"
|
|
297
|
-
if tool_args: 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()])})"
|
|
298
|
-
|
|
299
|
-
with self.console.status(f"[yellow]{status_msg}...", spinner="dots"):
|
|
300
|
-
try:
|
|
301
|
-
tool_instance = get_tool(tool_name)
|
|
302
|
-
if tool_instance:
|
|
303
|
-
log.debug(f"Executing tool '{tool_name}' with arguments: {tool_args}")
|
|
304
|
-
tool_result = tool_instance.execute(**tool_args)
|
|
305
|
-
log.info(f"Tool '{tool_name}' executed. Result length: {len(str(tool_result)) if tool_result else 0}")
|
|
306
|
-
log.debug(f"Tool '{tool_name}' result: {str(tool_result)[:500]}...")
|
|
307
|
-
else:
|
|
308
|
-
log.error(f"Tool '{tool_name}' not found.")
|
|
309
|
-
tool_result = f"Error: Tool '{tool_name}' is not available."
|
|
310
|
-
tool_error = True
|
|
311
|
-
except Exception as tool_exec_error:
|
|
312
|
-
log.error(f"Error executing tool '{tool_name}' with args {tool_args}: {tool_exec_error}", exc_info=True)
|
|
313
|
-
tool_result = f"Error executing tool {tool_name}: {str(tool_exec_error)}"
|
|
314
|
-
tool_error = True
|
|
315
|
-
|
|
316
|
-
# --- Print Executed/Error INSIDE the status block ---
|
|
317
|
-
if tool_error:
|
|
318
|
-
self.console.print(f"[red] -> Error executing {tool_name}: {str(tool_result)[:100]}...[/red]")
|
|
319
|
-
else:
|
|
320
|
-
self.console.print(f"[dim] -> Executed {tool_name}[/dim]")
|
|
321
|
-
# --- End Status Block ---
|
|
322
|
-
|
|
323
|
-
# === Check for Task Completion Signal via Tool Call ===
|
|
324
|
-
if tool_name == "task_complete":
|
|
325
|
-
log.info("Task completion signaled by 'task_complete' function call.")
|
|
326
|
-
task_completed = True
|
|
327
|
-
final_summary = tool_result # The result of task_complete IS the summary
|
|
328
|
-
# We break *after* adding the function response below
|
|
329
|
-
|
|
330
|
-
# === Add Function Response to History ===
|
|
331
|
-
# Create the FunctionResponse proto
|
|
332
|
-
function_response_proto = protos.FunctionResponse(
|
|
333
|
-
name=tool_name,
|
|
334
|
-
response={"result": tool_result} # API expects dict
|
|
335
|
-
)
|
|
336
|
-
# Wrap it in a Part proto
|
|
337
|
-
response_part_proto = protos.Part(function_response=function_response_proto)
|
|
338
|
-
|
|
339
|
-
# Append to history
|
|
340
|
-
self.chat_history.append({'role': 'user', # Function response acts as a 'user' turn providing data
|
|
341
|
-
'parts': [response_part_proto]})
|
|
342
|
-
self._manage_context_window()
|
|
343
|
-
|
|
344
|
-
if task_completed:
|
|
345
|
-
break # Exit loop NOW that task_complete result is in history
|
|
346
|
-
else:
|
|
347
|
-
continue # IMPORTANT: Continue loop to let LLM react to function result
|
|
348
|
-
|
|
349
|
-
elif text_response_buffer:
|
|
350
|
-
# === Only Text Returned ===
|
|
351
|
-
log.info("LLM returned only text response(s). Assuming task completion or explanation provided.")
|
|
352
|
-
last_text_response = text_response_buffer.strip()
|
|
353
|
-
task_completed = True # Treat text response as completion
|
|
354
|
-
final_summary = last_text_response # Use the text as the summary
|
|
355
|
-
break # Exit the loop
|
|
356
|
-
|
|
357
|
-
else:
|
|
358
|
-
# === No actionable parts found ===
|
|
359
|
-
log.warning("LLM response contained no actionable parts (text or function call).")
|
|
360
|
-
last_text_response = "(Agent received response with no actionable parts)"
|
|
361
|
-
task_completed = True # Treat as completion to avoid loop errors
|
|
362
|
-
final_summary = last_text_response
|
|
363
|
-
break # Exit loop
|
|
364
|
-
|
|
365
|
-
except ResourceExhausted as quota_error:
|
|
366
|
-
log.warning(f"Quota exceeded for model '{self.current_model_name}': {quota_error}")
|
|
367
|
-
# Check if we are already using the fallback
|
|
368
|
-
if self.current_model_name == self.FALLBACK_MODEL:
|
|
369
|
-
log.error("Quota exceeded even for the fallback model. Cannot proceed.")
|
|
370
|
-
self.console.print(f"[bold red]API quota exceeded for primary and fallback models. Please check your plan/billing.[/bold red]")
|
|
371
|
-
# Clean history before returning
|
|
372
|
-
if self.chat_history[-1]['role'] == 'user': self.chat_history.pop()
|
|
373
|
-
return f"Error: API quota exceeded for primary and fallback models."
|
|
374
|
-
else:
|
|
375
|
-
log.info(f"Switching to fallback model: {self.FALLBACK_MODEL}")
|
|
376
|
-
self.console.print(f"[bold yellow]Quota limit reached for {self.current_model_name}. Switching to fallback model ({self.FALLBACK_MODEL})...[/bold yellow]")
|
|
377
|
-
self.current_model_name = self.FALLBACK_MODEL
|
|
378
|
-
try:
|
|
379
|
-
self._initialize_model_instance() # Recreate model instance with fallback name
|
|
380
|
-
log.info(f"Successfully switched to and initialized fallback model: {self.current_model_name}")
|
|
381
|
-
# Important: Clear the last model response (which caused the error) before retrying
|
|
382
|
-
if self.chat_history[-1]['role'] == 'model':
|
|
383
|
-
last_part = self.chat_history[-1]['parts'][0]
|
|
384
|
-
# Only pop if it was a failed function call attempt or empty text response leading to error
|
|
385
|
-
if hasattr(last_part, 'function_call') or not hasattr(last_part, 'text') or not last_part.text:
|
|
386
|
-
self.chat_history.pop()
|
|
387
|
-
log.debug("Removed last model part before retrying with fallback.")
|
|
388
|
-
continue # Retry the current loop iteration with the new model
|
|
389
|
-
except Exception as fallback_init_error:
|
|
390
|
-
log.error(f"Failed to initialize fallback model '{self.FALLBACK_MODEL}': {fallback_init_error}", exc_info=True)
|
|
391
|
-
self.console.print(f"[bold red]Error switching to fallback model: {fallback_init_error}[/bold red]")
|
|
392
|
-
if self.chat_history[-1]['role'] == 'user': self.chat_history.pop()
|
|
393
|
-
return f"Error: Failed to initialize fallback model after quota error."
|
|
394
|
-
|
|
395
|
-
except Exception as generation_error:
|
|
396
|
-
# This handles other errors during the generate_content call or loop logic
|
|
397
|
-
log.error(f"Error during Agent Loop: {generation_error}", exc_info=True)
|
|
398
|
-
# Clean history
|
|
399
|
-
if self.chat_history[-1]['role'] == 'user': self.chat_history.pop()
|
|
400
|
-
return f"Error during agent processing: {generation_error}"
|
|
401
|
-
|
|
402
|
-
# === End Agent Loop ===
|
|
403
|
-
|
|
404
|
-
# === Handle Final Output ===
|
|
405
|
-
if task_completed and final_summary:
|
|
406
|
-
log.info("Agent loop finished. Returning final summary.")
|
|
407
|
-
# Cleanup internal tags if needed (using a hypothetical method)
|
|
408
|
-
# cleaned_summary = self._cleanup_internal_tags(final_summary)
|
|
409
|
-
return final_summary.strip() # Return the summary from task_complete or final text
|
|
410
|
-
elif iteration_count >= MAX_AGENT_ITERATIONS:
|
|
411
|
-
log.warning(f"Agent loop terminated after reaching max iterations ({MAX_AGENT_ITERATIONS}).")
|
|
412
|
-
# Try to get the last *text* response the model generated, even if it wanted to call a function after
|
|
413
|
-
last_model_response_text = self._find_last_model_text(self.chat_history)
|
|
414
|
-
timeout_message = f"(Task exceeded max iterations ({MAX_AGENT_ITERATIONS}). Last text from model was: {last_model_response_text})"
|
|
415
|
-
return timeout_message.strip()
|
|
416
|
-
else:
|
|
417
|
-
# This case should be less likely now
|
|
418
|
-
log.error("Agent loop exited unexpectedly.")
|
|
419
|
-
last_model_response_text = self._find_last_model_text(self.chat_history)
|
|
420
|
-
return f"(Agent loop finished unexpectedly. Last model text: {last_model_response_text})"
|
|
421
|
-
|
|
422
|
-
except Exception as e:
|
|
423
|
-
log.error(f"Error during Agent Loop: {str(e)}", exc_info=True)
|
|
424
|
-
return f"An unexpected error occurred during the agent process: {str(e)}"
|
|
425
|
-
|
|
426
|
-
# --- Context Management (Consider Token Counting) ---
|
|
427
|
-
def _manage_context_window(self):
|
|
428
|
-
"""Basic context window management based on turn count."""
|
|
429
|
-
# Placeholder - Enhance with token counting
|
|
430
|
-
MAX_HISTORY_TURNS = 20 # Keep ~N pairs of user/model turns + initial setup + tool calls/responses
|
|
431
|
-
# Each full LLM round (request + function_call + function_response) adds 3 items
|
|
432
|
-
if len(self.chat_history) > (MAX_HISTORY_TURNS * 3 + 2):
|
|
433
|
-
log.warning(f"Chat history length ({len(self.chat_history)}) exceeded threshold. Truncating.")
|
|
434
|
-
# Keep system prompt (idx 0), initial model ack (idx 1)
|
|
435
|
-
keep_count = MAX_HISTORY_TURNS * 3 # Keep N rounds
|
|
436
|
-
keep_from_index = len(self.chat_history) - keep_count
|
|
437
|
-
self.chat_history = self.chat_history[:2] + self.chat_history[keep_from_index:]
|
|
438
|
-
log.info(f"History truncated to {len(self.chat_history)} items.")
|
|
439
|
-
|
|
440
|
-
# --- Tool Definition Helper ---
|
|
441
|
-
def _create_tool_definitions(self) -> list[FunctionDeclaration] | None:
|
|
442
|
-
"""Dynamically create FunctionDeclarations from AVAILABLE_TOOLS."""
|
|
443
|
-
declarations = []
|
|
444
|
-
for tool_name, tool_instance in AVAILABLE_TOOLS.items():
|
|
445
|
-
if hasattr(tool_instance, 'get_function_declaration'):
|
|
446
|
-
declaration = tool_instance.get_function_declaration()
|
|
447
|
-
if declaration:
|
|
448
|
-
declarations.append(declaration)
|
|
449
|
-
log.debug(f"Generated FunctionDeclaration for tool: {tool_name}")
|
|
450
|
-
else:
|
|
451
|
-
log.warning(f"Tool {tool_name} has 'get_function_declaration' but it returned None.")
|
|
452
|
-
else:
|
|
453
|
-
# Fallback or skip tools without the method? For now, log warning.
|
|
454
|
-
log.warning(f"Tool {tool_name} does not have a 'get_function_declaration' method. Skipping.")
|
|
455
|
-
|
|
456
|
-
log.info(f"Created {len(declarations)} function declarations for native tool use.")
|
|
457
|
-
return declarations if declarations else None
|
|
458
|
-
|
|
459
|
-
# --- System Prompt Helper ---
|
|
460
|
-
def _create_system_prompt(self) -> str:
|
|
461
|
-
"""Creates the system prompt, emphasizing native functions and planning."""
|
|
462
|
-
# Use docstrings from tools if possible for descriptions
|
|
463
|
-
tool_descriptions = []
|
|
464
|
-
if self.function_declarations:
|
|
465
|
-
for func_decl in self.function_declarations:
|
|
466
|
-
# Simple representation: name(args) - description
|
|
467
|
-
# Ensure parameters exist before trying to access properties
|
|
468
|
-
args_str = ""
|
|
469
|
-
if func_decl.parameters and func_decl.parameters.properties:
|
|
470
|
-
args_list = []
|
|
471
|
-
required_args = func_decl.parameters.required or []
|
|
472
|
-
for prop, details in func_decl.parameters.properties.items():
|
|
473
|
-
# Access attributes directly from the Schema object
|
|
474
|
-
prop_type = details.type if hasattr(details, 'type') else 'UNKNOWN'
|
|
475
|
-
prop_desc = details.description if hasattr(details, 'description') else ''
|
|
476
|
-
|
|
477
|
-
suffix = "" if prop in required_args else "?" # Indicate optional args
|
|
478
|
-
|
|
479
|
-
# Include parameter description in the string for clarity in the system prompt
|
|
480
|
-
args_list.append(f"{prop}: {prop_type}{suffix} # {prop_desc}")
|
|
481
|
-
|
|
482
|
-
args_str = ", ".join(args_list)
|
|
483
|
-
|
|
484
|
-
desc = func_decl.description or "(No description provided)" # Overall func desc
|
|
485
|
-
tool_descriptions.append(f"- `{func_decl.name}({args_str})`: {desc}")
|
|
486
|
-
else:
|
|
487
|
-
tool_descriptions.append(" - (No tools available with function declarations)")
|
|
488
|
-
|
|
489
|
-
tool_list_str = "\n".join(tool_descriptions)
|
|
490
|
-
|
|
491
|
-
# Prompt v13.1 - Native Functions, Planning, Accurate Context
|
|
492
|
-
return f"""You are Gemini Code, an AI coding assistant running in a CLI environment.
|
|
493
|
-
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**.
|
|
494
|
-
|
|
495
|
-
Available Tools (Use ONLY these via function calls):
|
|
496
|
-
{tool_list_str}
|
|
497
|
-
|
|
498
|
-
Workflow:
|
|
499
|
-
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 and tools you will use in a text response. **Note:** Actions that modify files (`edit`, `create_file`) will require user confirmation before execution.
|
|
500
|
-
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 structure).
|
|
501
|
-
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.
|
|
502
|
-
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.
|
|
503
|
-
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.
|
|
504
|
-
* The `summary` argument MUST accurately reflect the final outcome (success, partial success, error, or what was done).
|
|
505
|
-
* Format the summary using **Markdown** for readability (e.g., use backticks for filenames `like_this.py` or commands `like this`).
|
|
506
|
-
* 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`, not just say "instructions provided"). Use Markdown code blocks for commands.
|
|
507
|
-
|
|
508
|
-
Important Rules:
|
|
509
|
-
* **Use Native Functions:** ONLY interact with tools by making function calls as defined above. Do NOT output tool calls as text (e.g., `cli_tools.ls(...)`).
|
|
510
|
-
* **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.
|
|
511
|
-
* **Initial Context Handling:** When the user asks a general question about the codebase contents (e.g., "what's in this directory?", "show me the files", "whats in this codebase?"), your **first** response MUST be a summary or list of **ALL** files and directories provided in the initial context (`ls` or `tree` output). Do **NOT** filter this initial list or make assumptions (e.g., about virtual environments). Only after presenting the full initial context should you suggest further actions or use other tools if necessary.
|
|
512
|
-
* **Accurate Context Reporting:** When asked about directory contents (like "whats in this codebase?"), accurately list or summarize **all** relevant files and directories shown in the `ls` or `tree` output, including common web files (`.html`, `.js`, `.css`), documentation (`.md`), configuration files, build artifacts, etc., not just specific source code types. Do not ignore files just because virtual environments are also present. Use `tree` for a hierarchical view if needed.
|
|
513
|
-
* **Handling Explanations:**
|
|
514
|
-
* If the user asks *how* to do something, asks for an explanation, or requests instructions (like "how do I run this?"), **provide the explanation or instructions directly in a text response** using clear Markdown formatting.
|
|
515
|
-
* **Proactive Assistance:** When providing instructions that culminate in a specific execution command (like `python file.py`, `npm start`, `git status | cat`, etc.), first give the full explanation, then **explicitly ask the user if they want you to run that final command** using the `execute_command` tool.
|
|
516
|
-
* Example: After explaining how to run `calculator.py`, you should ask: "Would you like me to run `python calculator.py | cat` for you using the `execute_command` tool?" (Append `| cat` for commands that might page).
|
|
517
|
-
* Do *not* use `task_complete` just for providing information; only use it when the *underlying task* (e.g., file creation, modification) is fully finished.
|
|
518
|
-
* **Planning First:** For tasks requiring multiple steps (e.g., read file, modify content, write file), explain your plan briefly in text *before* the first function call.
|
|
519
|
-
* **Precise Edits:** When editing files (`edit` tool), prefer viewing the relevant section first (`view` tool with offset/limit), then use exact `old_string`/`new_string` arguments if possible. Only use the `content` argument for creating new files or complete overwrites.
|
|
520
|
-
* **Task Completion Signal:** ALWAYS finish action-oriented tasks by calling `task_complete(summary=...)`.
|
|
521
|
-
* The `summary` argument MUST accurately reflect the final outcome (success, partial success, error, or what was done).
|
|
522
|
-
* Format the summary using **Markdown** for readability (e.g., use backticks for filenames `like_this.py` or commands `like this`).
|
|
523
|
-
* 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`, not just say "instructions provided"). Use Markdown code blocks for commands.
|
|
524
|
-
|
|
525
|
-
The user's first message will contain initial directory context and their request."""
|
|
526
|
-
|
|
527
|
-
# --- Text Extraction Helper (if needed for final output) ---
|
|
528
|
-
def _extract_text_from_response(self, response) -> str | None:
|
|
529
|
-
"""Safely extracts text from a Gemini response object."""
|
|
530
|
-
try:
|
|
531
|
-
if response and response.candidates:
|
|
532
|
-
# Handle potential multi-part responses if ever needed, for now assume text is in the first part
|
|
533
|
-
if response.candidates[0].content and response.candidates[0].content.parts:
|
|
534
|
-
text_parts = [part.text for part in response.candidates[0].content.parts if hasattr(part, 'text')]
|
|
535
|
-
return "\n".join(text_parts).strip() if text_parts else None
|
|
536
|
-
return None
|
|
537
|
-
except (AttributeError, IndexError) as e:
|
|
538
|
-
log.warning(f"Could not extract text from response: {e} - Response: {response}")
|
|
539
|
-
return None
|
|
540
|
-
|
|
541
|
-
# --- Find Last Text Helper ---
|
|
542
|
-
def _find_last_model_text(self, history: list) -> str:
|
|
543
|
-
"""Finds the last text part sent by the model in the history."""
|
|
544
|
-
for i in range(len(history) - 1, -1, -1):
|
|
545
|
-
if history[i]['role'] == 'model':
|
|
546
|
-
try:
|
|
547
|
-
# Check if parts exists and has content
|
|
548
|
-
if history[i]['parts'] and hasattr(history[i]['parts'][0], 'text'):
|
|
549
|
-
return history[i]['parts'][0].text.strip()
|
|
550
|
-
except (AttributeError, IndexError):
|
|
551
|
-
continue # Ignore malformed history entries
|
|
552
|
-
return "(No previous text response found)"
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tools module initialization. Registers all available tools.
|
|
3
|
-
Includes Summarizer Tool.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import logging
|
|
7
|
-
from .base import BaseTool
|
|
8
|
-
from .file_tools import ViewTool, EditTool, GrepTool, GlobTool
|
|
9
|
-
from .directory_tools import LsTool
|
|
10
|
-
|
|
11
|
-
# --- Tool Imports ---
|
|
12
|
-
try: from .system_tools import BashTool; bash_tool_available = True
|
|
13
|
-
except ImportError: logging.warning("system_tools.BashTool not found. Disabled."); bash_tool_available = False
|
|
14
|
-
|
|
15
|
-
try: from .task_complete_tool import TaskCompleteTool; task_complete_available = True
|
|
16
|
-
except ImportError: logging.warning("task_complete_tool.TaskCompleteTool not found. Disabled."); task_complete_available = False
|
|
17
|
-
|
|
18
|
-
try: from .directory_tools import CreateDirectoryTool; create_dir_available = True
|
|
19
|
-
except ImportError: logging.warning("directory_tools.CreateDirectoryTool not found. Disabled."); create_dir_available = False
|
|
20
|
-
|
|
21
|
-
try: from .quality_tools import LinterCheckerTool, FormatterTool; quality_tools_available = True
|
|
22
|
-
except ImportError: logging.warning("quality_tools not found or missing classes. Disabled."); quality_tools_available = False
|
|
23
|
-
|
|
24
|
-
# Import the new summarizer tool
|
|
25
|
-
try: from .summarizer_tool import SummarizeCodeTool; summarizer_available = True
|
|
26
|
-
except ImportError: logging.warning("summarizer_tool.SummarizeCodeTool not found. Disabled."); summarizer_available = False
|
|
27
|
-
|
|
28
|
-
# Assuming test_runner exists from previous steps
|
|
29
|
-
test_runner_available = True
|
|
30
|
-
if test_runner_available:
|
|
31
|
-
try: from .test_runner import TestRunnerTool
|
|
32
|
-
except ImportError: logging.warning("test_runner.py exists but failed import?"); test_runner_available=False
|
|
33
|
-
# --- End Tool Imports ---
|
|
34
|
-
|
|
35
|
-
from .tree_tool import TreeTool
|
|
36
|
-
|
|
37
|
-
# AVAILABLE_TOOLS maps tool names (strings) to the actual tool classes.
|
|
38
|
-
# Start with core, guaranteed tools
|
|
39
|
-
AVAILABLE_TOOLS = {
|
|
40
|
-
"view": ViewTool,
|
|
41
|
-
"edit": EditTool,
|
|
42
|
-
"ls": LsTool,
|
|
43
|
-
"grep": GrepTool,
|
|
44
|
-
"glob": GlobTool,
|
|
45
|
-
"create_directory": CreateDirectoryTool,
|
|
46
|
-
"task_complete": TaskCompleteTool,
|
|
47
|
-
"tree": TreeTool,
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
# Conditionally add tools based on successful imports
|
|
51
|
-
if bash_tool_available: AVAILABLE_TOOLS["bash"] = BashTool
|
|
52
|
-
# task_complete is core, already added
|
|
53
|
-
# create_directory is core, already added
|
|
54
|
-
if quality_tools_available:
|
|
55
|
-
AVAILABLE_TOOLS["linter_checker"] = LinterCheckerTool
|
|
56
|
-
AVAILABLE_TOOLS["formatter"] = FormatterTool
|
|
57
|
-
# Summarizer tool is not added by default
|
|
58
|
-
if test_runner_available: AVAILABLE_TOOLS["test_runner"] = TestRunnerTool
|
|
59
|
-
# tree is core, already added
|
|
60
|
-
|
|
61
|
-
def get_tool(name: str) -> BaseTool | None:
|
|
62
|
-
"""
|
|
63
|
-
Retrieves an *instance* of the tool class based on its name.
|
|
64
|
-
NOTE: Does NOT handle special constructors (like SummarizeCodeTool needing the model).
|
|
65
|
-
That specific instantiation happens in the GeminiModel class now.
|
|
66
|
-
"""
|
|
67
|
-
tool_class = AVAILABLE_TOOLS.get(name)
|
|
68
|
-
if tool_class:
|
|
69
|
-
try:
|
|
70
|
-
# For most tools, simple instantiation works
|
|
71
|
-
if name != "summarize_code": # Exclude the special case
|
|
72
|
-
return tool_class()
|
|
73
|
-
else:
|
|
74
|
-
# Raise error or return None if called for summarize_code,
|
|
75
|
-
# as it needs special handling elsewhere.
|
|
76
|
-
logging.error(f"get_tool() called for '{name}', which requires special instantiation with model instance.")
|
|
77
|
-
return None
|
|
78
|
-
except Exception as e:
|
|
79
|
-
logging.error(f"Error instantiating tool '{name}': {e}", exc_info=True)
|
|
80
|
-
return None
|
|
81
|
-
else:
|
|
82
|
-
logging.warning(f"Tool '{name}' not found in AVAILABLE_TOOLS.")
|
|
83
|
-
return None
|
|
84
|
-
|
|
85
|
-
logging.info(f"Tools initialized. Available: {list(AVAILABLE_TOOLS.keys())}")
|
|
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
|