Skip to content

Commit a0552af

Browse files
authored
misc: better-error (#805)
# Motivation <!-- Why is this change necessary? --> # Content <!-- Please include a summary of the change --> # Testing <!-- How was the change tested? --> # Please check the following before marking your PR as ready for review - [ ] I have added tests for my changes - [ ] I have updated the documentation or added new documentation as needed --------- Co-authored-by: kopekC <[email protected]>
1 parent d7af589 commit a0552af

File tree

3 files changed

+272
-8
lines changed

3 files changed

+272
-8
lines changed

src/codegen/extensions/langchain/graph.py

Lines changed: 242 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,250 @@ def create(self, checkpointer: Optional[MemorySaver] = None, debug: bool = False
7171
jitter=True,
7272
)
7373

74+
# Custom error handler for tool validation errors
75+
def handle_tool_errors(exception):
76+
error_msg = str(exception)
77+
78+
# Extract tool name and input from the exception if possible
79+
tool_name = "unknown"
80+
tool_input = {}
81+
82+
# Helper function to get field descriptions from any tool
83+
def get_field_descriptions(tool_obj):
84+
field_descriptions = {}
85+
if not tool_obj or not hasattr(tool_obj, "args_schema"):
86+
return field_descriptions
87+
88+
try:
89+
schema_cls = tool_obj.args_schema
90+
91+
# Handle Pydantic v2
92+
if hasattr(schema_cls, "model_fields"):
93+
for field_name, field in schema_cls.model_fields.items():
94+
field_descriptions[field_name] = field.description or f"Required parameter for {tool_obj.name}"
95+
96+
# Handle Pydantic v1 with warning suppression
97+
elif hasattr(schema_cls, "__fields__"):
98+
import warnings
99+
100+
with warnings.catch_warnings():
101+
warnings.filterwarnings("ignore", category=DeprecationWarning)
102+
for field_name, field in schema_cls.__fields__.items():
103+
field_descriptions[field_name] = field.field_info.description or f"Required parameter for {tool_obj.name}"
104+
except Exception:
105+
pass
106+
107+
return field_descriptions
108+
109+
# Try to extract tool name and input from the exception
110+
import re
111+
112+
tool_match = re.search(r"for (\w+)Input", error_msg)
113+
if tool_match:
114+
# Get the extracted name but preserve original case by finding the matching tool
115+
extracted_name = tool_match.group(1).lower()
116+
for t in self.tools:
117+
if t.name.lower() == extracted_name:
118+
tool_name = t.name # Use the original case from the tool
119+
break
120+
121+
# Try to extract the input values
122+
input_match = re.search(r"input_value=(\{.*?\})", error_msg)
123+
if input_match:
124+
input_str = input_match.group(1)
125+
# Simple parsing of the dict-like string
126+
try:
127+
# Clean up the string to make it more parseable
128+
input_str = input_str.replace("'", '"')
129+
import json
130+
131+
tool_input = json.loads(input_str)
132+
except:
133+
pass
134+
135+
# Handle validation errors with more helpful messages
136+
if "validation error" in error_msg.lower():
137+
# Find the tool in our tools list to get its schema
138+
tool = next((t for t in self.tools if t.name == tool_name), None)
139+
140+
# If we couldn't find the tool by extracted name, try to find it by looking at all tools
141+
if tool is None:
142+
# Try to extract tool name from the error message
143+
for t in self.tools:
144+
if t.name.lower() in error_msg.lower():
145+
tool = t
146+
tool_name = t.name
147+
break
148+
149+
# If still not found, check if any tool's schema name matches
150+
if tool is None:
151+
for t in self.tools:
152+
if hasattr(t, "args_schema") and t.args_schema.__name__.lower() in error_msg.lower():
153+
tool = t
154+
tool_name = t.name
155+
break
156+
157+
# Check for type errors
158+
type_errors = []
159+
if "type_error" in error_msg.lower():
160+
import re
161+
162+
# Try to extract type error information
163+
type_error_matches = re.findall(r"'(\w+)'.*?type_error\.(.*?)(?:;|$)", error_msg, re.IGNORECASE)
164+
for field_name, error_type in type_error_matches:
165+
if "json" in error_type:
166+
type_errors.append(f"'{field_name}' must be a string, not a JSON object or dictionary")
167+
elif "str_type" in error_type:
168+
type_errors.append(f"'{field_name}' must be a string")
169+
elif "int_type" in error_type:
170+
type_errors.append(f"'{field_name}' must be an integer")
171+
elif "bool_type" in error_type:
172+
type_errors.append(f"'{field_name}' must be a boolean")
173+
elif "list_type" in error_type:
174+
type_errors.append(f"'{field_name}' must be a list")
175+
else:
176+
type_errors.append(f"'{field_name}' has an incorrect type")
177+
178+
if type_errors:
179+
errors_str = "\n- ".join(type_errors)
180+
return f"Error using {tool_name} tool: Parameter type errors:\n- {errors_str}\n\nYou provided: {tool_input}\n\nPlease try again with the correct parameter types."
181+
182+
# Get missing fields by comparing tool input with required fields
183+
missing_fields = []
184+
if tool and hasattr(tool, "args_schema"):
185+
try:
186+
# Get the schema class
187+
schema_cls = tool.args_schema
188+
189+
# Handle Pydantic v2 (preferred) or v1 with warning suppression
190+
if hasattr(schema_cls, "model_fields"): # Pydantic v2
191+
for field_name, field in schema_cls.model_fields.items():
192+
# Check if field is required and missing from input
193+
if field.is_required() and field_name not in tool_input:
194+
missing_fields.append(field_name)
195+
else: # Pydantic v1 with warning suppression
196+
import warnings
197+
198+
with warnings.catch_warnings():
199+
warnings.filterwarnings("ignore", category=DeprecationWarning)
200+
for field_name, field in schema_cls.__fields__.items():
201+
# Check if field is required and missing from input
202+
if field.required and field_name not in tool_input:
203+
missing_fields.append(field_name)
204+
except Exception as e:
205+
# If we can't extract schema info, we'll fall back to regex
206+
pass
207+
208+
# If we couldn't get missing fields from schema, try to extract from error message
209+
if not missing_fields:
210+
# Extract the missing field name if possible using regex
211+
import re
212+
213+
field_matches = re.findall(r"'(\w+)'(?:\s+|.*?)field required", error_msg, re.IGNORECASE)
214+
if field_matches:
215+
missing_fields = field_matches
216+
else:
217+
# Try another pattern
218+
field_match = re.search(r"(\w+)\s+Field required", error_msg)
219+
if field_match:
220+
missing_fields = [field_match.group(1)]
221+
222+
# If we have identified missing fields, create a helpful error message
223+
if missing_fields:
224+
fields_str = ", ".join([f"'{f}'" for f in missing_fields])
225+
226+
# Get tool documentation if available
227+
tool_docs = ""
228+
if tool:
229+
if hasattr(tool, "description") and tool.description:
230+
tool_docs = f"\nTool description: {tool.description}\n"
231+
232+
# Try to get parameter descriptions from the schema
233+
param_docs = []
234+
try:
235+
# Get all field descriptions from the tool
236+
field_descriptions = get_field_descriptions(tool)
237+
238+
# Add descriptions for missing fields
239+
for field_name in missing_fields:
240+
if field_name in field_descriptions:
241+
param_docs.append(f"- {field_name}: {field_descriptions[field_name]}")
242+
else:
243+
param_docs.append(f"- {field_name}: Required parameter")
244+
245+
if param_docs:
246+
tool_docs += "\nParameter descriptions:\n" + "\n".join(param_docs)
247+
except Exception:
248+
# Fallback to simple parameter list
249+
param_docs = [f"- {field}: Required parameter" for field in missing_fields]
250+
if param_docs:
251+
tool_docs += "\nMissing parameters:\n" + "\n".join(param_docs)
252+
253+
# Add usage examples for common tools
254+
example = ""
255+
if tool_name == "create_file":
256+
example = "\nExample: create_file(filepath='path/to/file.py', content='print(\"Hello world\")')"
257+
elif tool_name == "replace_edit":
258+
example = "\nExample: replace_edit(filepath='path/to/file.py', old_text='def old_function()', new_text='def new_function()')"
259+
elif tool_name == "view_file":
260+
example = "\nExample: view_file(filepath='path/to/file.py')"
261+
elif tool_name == "search":
262+
example = "\nExample: search(query='function_name', file_extensions=['.py'])"
263+
264+
return (
265+
f"Error using {tool_name} tool: Missing required parameter(s): {fields_str}\n\nYou provided: {tool_input}\n{tool_docs}{example}\nPlease try again with all required parameters."
266+
)
267+
268+
# Common error patterns for specific tools (as fallback)
269+
if tool_name == "create_file":
270+
if "content" not in tool_input:
271+
return (
272+
"Error: When using the create_file tool, you must provide both 'filepath' and 'content' parameters.\n"
273+
"The 'content' parameter is missing. Please try again with both parameters.\n\n"
274+
"Example: create_file(filepath='path/to/file.py', content='print(\"Hello world\")')"
275+
)
276+
elif "filepath" not in tool_input:
277+
return (
278+
"Error: When using the create_file tool, you must provide both 'filepath' and 'content' parameters.\n"
279+
"The 'filepath' parameter is missing. Please try again with both parameters.\n\n"
280+
"Example: create_file(filepath='path/to/file.py', content='print(\"Hello world\")')"
281+
)
282+
283+
elif tool_name == "replace_edit":
284+
if "filepath" not in tool_input:
285+
return (
286+
"Error: When using the replace_edit tool, you must provide 'filepath', 'old_text', and 'new_text' parameters.\n"
287+
"The 'filepath' parameter is missing. Please try again with all required parameters."
288+
)
289+
elif "old_text" not in tool_input:
290+
return (
291+
"Error: When using the replace_edit tool, you must provide 'filepath', 'old_text', and 'new_text' parameters.\n"
292+
"The 'old_text' parameter is missing. Please try again with all required parameters."
293+
)
294+
elif "new_text" not in tool_input:
295+
return (
296+
"Error: When using the replace_edit tool, you must provide 'filepath', 'old_text', and 'new_text' parameters.\n"
297+
"The 'new_text' parameter is missing. Please try again with all required parameters."
298+
)
299+
300+
# Generic validation error with better formatting
301+
if tool:
302+
return (
303+
f"Error using {tool_name} tool: {error_msg}\n\n"
304+
f"You provided these parameters: {tool_input}\n\n"
305+
f"Please check the tool's required parameters and try again with all required fields."
306+
)
307+
else:
308+
# If we couldn't identify the tool, list all available tools
309+
available_tools = "\n".join([f"- {t.name}" for t in self.tools])
310+
return f"Error: Could not identify the tool you're trying to use.\n\nAvailable tools:\n{available_tools}\n\nPlease use one of the available tools with the correct parameters."
311+
312+
# For other types of errors
313+
return f"Error executing tool: {error_msg}\n\nPlease check your tool usage and try again with the correct parameters."
314+
74315
# Add nodes
75316
builder.add_node("reasoner", self.reasoner, retry=retry_policy)
76-
builder.add_node("tools", ToolNode(self.tools), retry=retry_policy)
317+
builder.add_node("tools", ToolNode(self.tools, handle_tool_errors=handle_tool_errors), retry=retry_policy)
77318

78319
# Add edges
79320
builder.add_edge(START, "reasoner")

src/codegen/extensions/langchain/tools.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ class SearchTool(BaseTool):
136136
def __init__(self, codebase: Codebase) -> None:
137137
super().__init__(codebase=codebase)
138138

139-
def _run(self, query: str, target_directories: Optional[list[str]] = None, file_extensions: Optional[list[str]] = None, page: int = 1, files_per_page: int = 10, use_regex: bool = False) -> str:
139+
def _run(self, query: str, file_extensions: Optional[list[str]] = None, page: int = 1, files_per_page: int = 10, use_regex: bool = False) -> str:
140140
result = search(self.codebase, query, file_extensions=file_extensions, page=page, files_per_page=files_per_page, use_regex=use_regex)
141141
return result.render()
142142

@@ -171,7 +171,6 @@ class EditFileTool(BaseTool):
171171
1. Simple text: "function calculateTotal" (matches exactly, case-insensitive)
172172
2. Regex: "def.*calculate.*\(.*\)" (with use_regex=True)
173173
3. File-specific: "TODO" with file_extensions=[".py", ".ts"]
174-
4. Directory-specific: "api" with target_directories=["src/backend"]
175174
"""
176175
args_schema: ClassVar[type[BaseModel]] = EditFileInput
177176
codebase: Codebase = Field(exclude=True)
@@ -188,21 +187,45 @@ class CreateFileInput(BaseModel):
188187
"""Input for creating a file."""
189188

190189
filepath: str = Field(..., description="Path where to create the file")
191-
content: str = Field(default="", description="Initial file content")
190+
content: str = Field(
191+
...,
192+
description="""
193+
Content for the new file (REQUIRED).
194+
195+
⚠️ IMPORTANT: This parameter MUST be a STRING, not a dictionary, JSON object, or any other data type.
196+
Example: content="print('Hello world')"
197+
NOT: content={"code": "print('Hello world')"}
198+
""",
199+
)
192200

193201

194202
class CreateFileTool(BaseTool):
195203
"""Tool for creating files."""
196204

197205
name: ClassVar[str] = "create_file"
198-
description: ClassVar[str] = "Create a new file in the codebase"
206+
description: ClassVar[str] = """
207+
Create a new file in the codebase. Always provide content for the new file, even if minimal.
208+
209+
⚠️ CRITICAL WARNING ⚠️
210+
Both parameters MUST be provided as STRINGS:
211+
The content for the new file always needs to be provided.
212+
213+
1. filepath: The path where to create the file (as a string)
214+
2. content: The content for the new file (as a STRING, NOT as a dictionary or JSON object)
215+
216+
✅ CORRECT usage:
217+
create_file(filepath="path/to/file.py", content="print('Hello world')")
218+
219+
The content parameter is REQUIRED and MUST be a STRING. If you receive a validation error about
220+
missing content, you are likely trying to pass a dictionary instead of a string.
221+
"""
199222
args_schema: ClassVar[type[BaseModel]] = CreateFileInput
200223
codebase: Codebase = Field(exclude=True)
201224

202225
def __init__(self, codebase: Codebase) -> None:
203226
super().__init__(codebase=codebase)
204227

205-
def _run(self, filepath: str, content: str = "") -> str:
228+
def _run(self, filepath: str, content: str) -> str:
206229
result = create_file(self.codebase, filepath, content)
207230
return result.render()
208231

src/codegen/extensions/tools/create_file.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ class CreateFileObservation(Observation):
2323
str_template: ClassVar[str] = "Created file {filepath}"
2424

2525

26-
def create_file(codebase: Codebase, filepath: str, content: str = "") -> CreateFileObservation:
26+
def create_file(codebase: Codebase, filepath: str, content: str) -> CreateFileObservation:
2727
"""Create a new file.
2828
2929
Args:
3030
codebase: The codebase to operate on
3131
filepath: Path where to create the file
32-
content: Initial file content
32+
content: Content for the new file (required)
3333
3434
Returns:
3535
CreateFileObservation containing new file state, or error if file exists

0 commit comments

Comments
 (0)