roma-debug 0.1.0__py3-none-any.whl
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.
- roma_debug/__init__.py +3 -0
- roma_debug/config.py +79 -0
- roma_debug/core/__init__.py +5 -0
- roma_debug/core/engine.py +423 -0
- roma_debug/core/models.py +313 -0
- roma_debug/main.py +753 -0
- roma_debug/parsers/__init__.py +21 -0
- roma_debug/parsers/base.py +189 -0
- roma_debug/parsers/python_ast_parser.py +268 -0
- roma_debug/parsers/registry.py +196 -0
- roma_debug/parsers/traceback_patterns.py +314 -0
- roma_debug/parsers/treesitter_parser.py +598 -0
- roma_debug/prompts.py +153 -0
- roma_debug/server.py +247 -0
- roma_debug/tracing/__init__.py +28 -0
- roma_debug/tracing/call_chain.py +278 -0
- roma_debug/tracing/context_builder.py +672 -0
- roma_debug/tracing/dependency_graph.py +298 -0
- roma_debug/tracing/error_analyzer.py +399 -0
- roma_debug/tracing/import_resolver.py +315 -0
- roma_debug/tracing/project_scanner.py +569 -0
- roma_debug/utils/__init__.py +5 -0
- roma_debug/utils/context.py +422 -0
- roma_debug-0.1.0.dist-info/METADATA +34 -0
- roma_debug-0.1.0.dist-info/RECORD +36 -0
- roma_debug-0.1.0.dist-info/WHEEL +5 -0
- roma_debug-0.1.0.dist-info/entry_points.txt +2 -0
- roma_debug-0.1.0.dist-info/licenses/LICENSE +201 -0
- roma_debug-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/test_context.py +208 -0
- tests/test_engine.py +296 -0
- tests/test_parsers.py +534 -0
- tests/test_project_scanner.py +275 -0
- tests/test_traceback_patterns.py +222 -0
- tests/test_tracing.py +296 -0
roma_debug/__init__.py
ADDED
roma_debug/config.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Centralized configuration for ROMA Debug.
|
|
2
|
+
|
|
3
|
+
This module is the single source of truth for all configuration.
|
|
4
|
+
It loads the .env file once at import time and exposes validated settings.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _find_project_root() -> Path:
|
|
14
|
+
"""Find project root by searching for .env file.
|
|
15
|
+
|
|
16
|
+
Searches upward from this file's location until .env is found.
|
|
17
|
+
Falls back to the parent of roma_debug package.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Path to project root directory
|
|
21
|
+
"""
|
|
22
|
+
current = Path(__file__).resolve().parent
|
|
23
|
+
|
|
24
|
+
# Search upward for .env file (max 5 levels)
|
|
25
|
+
for _ in range(5):
|
|
26
|
+
if (current / ".env").exists():
|
|
27
|
+
return current
|
|
28
|
+
if current.parent == current:
|
|
29
|
+
break
|
|
30
|
+
current = current.parent
|
|
31
|
+
|
|
32
|
+
# Fallback: assume project root is parent of roma_debug/
|
|
33
|
+
return Path(__file__).resolve().parent.parent
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _load_config() -> str:
|
|
37
|
+
"""Load configuration from .env file.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
The Gemini API key
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
RuntimeError: If GEMINI_API_KEY is not set
|
|
44
|
+
"""
|
|
45
|
+
project_root = _find_project_root()
|
|
46
|
+
env_path = project_root / ".env"
|
|
47
|
+
|
|
48
|
+
# Load .env file
|
|
49
|
+
if env_path.exists():
|
|
50
|
+
load_dotenv(env_path, override=True)
|
|
51
|
+
else:
|
|
52
|
+
# Try loading from environment anyway
|
|
53
|
+
load_dotenv()
|
|
54
|
+
|
|
55
|
+
# Get API key from environment
|
|
56
|
+
api_key = os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY")
|
|
57
|
+
|
|
58
|
+
if not api_key:
|
|
59
|
+
raise RuntimeError(
|
|
60
|
+
f"GEMINI_API_KEY not found!\n"
|
|
61
|
+
f"Searched .env at: {env_path}\n"
|
|
62
|
+
f"Please set GEMINI_API_KEY in your .env file or environment."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return api_key
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Load configuration at import time
|
|
69
|
+
# This ensures consistent behavior across CLI and server
|
|
70
|
+
GEMINI_API_KEY: str = _load_config()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_api_key_status() -> str:
|
|
74
|
+
"""Get human-readable API key status for logging.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
'OK' if key is loaded, 'MISSING' otherwise
|
|
78
|
+
"""
|
|
79
|
+
return "OK" if GEMINI_API_KEY else "MISSING"
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"""Gemini Fixer Logic for ROMA Debug.
|
|
2
|
+
|
|
3
|
+
Returns structured JSON responses for machine-readable fixes.
|
|
4
|
+
Supports both V1 (simple) and V2 (deep debugging) modes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Optional, List
|
|
12
|
+
|
|
13
|
+
from google import genai
|
|
14
|
+
from google.genai import types
|
|
15
|
+
|
|
16
|
+
from roma_debug.config import GEMINI_API_KEY
|
|
17
|
+
from roma_debug.prompts import SYSTEM_PROMPT, SYSTEM_PROMPT_V2
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Model priority: try Lite first (higher quota), then Flash
|
|
21
|
+
PRIMARY_MODEL = "gemini-2.5-flash-lite"
|
|
22
|
+
FALLBACK_MODEL = "gemini-2.5-flash"
|
|
23
|
+
|
|
24
|
+
# Placeholder paths that indicate the AI couldn't determine the real path
|
|
25
|
+
INVALID_PATHS = {
|
|
26
|
+
"unknown",
|
|
27
|
+
"path/to/file.py",
|
|
28
|
+
"path/to/your/code.py",
|
|
29
|
+
"path/to/your/file.py",
|
|
30
|
+
"example.py",
|
|
31
|
+
"your_file.py",
|
|
32
|
+
"file.py",
|
|
33
|
+
"",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class FixResult:
|
|
39
|
+
"""Structured result from the AI engine."""
|
|
40
|
+
filepath: Optional[str] # None for general system errors
|
|
41
|
+
full_code_block: str
|
|
42
|
+
explanation: str
|
|
43
|
+
raw_response: str
|
|
44
|
+
model_used: str
|
|
45
|
+
|
|
46
|
+
def to_dict(self) -> dict:
|
|
47
|
+
"""Convert to dictionary."""
|
|
48
|
+
return {
|
|
49
|
+
"filepath": self.filepath,
|
|
50
|
+
"full_code_block": self.full_code_block,
|
|
51
|
+
"explanation": self.explanation,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class AdditionalFix:
|
|
57
|
+
"""An additional fix for another file."""
|
|
58
|
+
filepath: str
|
|
59
|
+
full_code_block: str
|
|
60
|
+
explanation: str
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class FixResultV2(FixResult):
|
|
65
|
+
"""V2 result with root cause analysis and multiple fixes."""
|
|
66
|
+
root_cause_file: Optional[str] = None
|
|
67
|
+
root_cause_explanation: Optional[str] = None
|
|
68
|
+
additional_fixes: List[AdditionalFix] = field(default_factory=list)
|
|
69
|
+
|
|
70
|
+
def to_dict(self) -> dict:
|
|
71
|
+
"""Convert to dictionary."""
|
|
72
|
+
result = super().to_dict()
|
|
73
|
+
result["root_cause_file"] = self.root_cause_file
|
|
74
|
+
result["root_cause_explanation"] = self.root_cause_explanation
|
|
75
|
+
result["additional_fixes"] = [
|
|
76
|
+
{
|
|
77
|
+
"filepath": fix.filepath,
|
|
78
|
+
"full_code_block": fix.full_code_block,
|
|
79
|
+
"explanation": fix.explanation,
|
|
80
|
+
}
|
|
81
|
+
for fix in self.additional_fixes
|
|
82
|
+
]
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def has_root_cause(self) -> bool:
|
|
87
|
+
"""Check if a root cause in a different file was identified."""
|
|
88
|
+
return bool(self.root_cause_file and self.root_cause_file != self.filepath)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def all_files_to_fix(self) -> List[str]:
|
|
92
|
+
"""Get list of all files that need fixes."""
|
|
93
|
+
files = []
|
|
94
|
+
if self.filepath:
|
|
95
|
+
files.append(self.filepath)
|
|
96
|
+
if self.root_cause_file and self.root_cause_file not in files:
|
|
97
|
+
files.append(self.root_cause_file)
|
|
98
|
+
for fix in self.additional_fixes:
|
|
99
|
+
if fix.filepath not in files:
|
|
100
|
+
files.append(fix.filepath)
|
|
101
|
+
return files
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _get_client() -> genai.Client:
|
|
105
|
+
"""Get configured Gemini client."""
|
|
106
|
+
return genai.Client(api_key=GEMINI_API_KEY)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _normalize_filepath(filepath: Optional[str]) -> Optional[str]:
|
|
110
|
+
"""Normalize filepath, returning None for invalid/placeholder paths.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
filepath: Raw filepath from AI response
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Validated filepath or None if invalid
|
|
117
|
+
"""
|
|
118
|
+
if filepath is None:
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
# Convert to string and strip whitespace
|
|
122
|
+
filepath = str(filepath).strip()
|
|
123
|
+
|
|
124
|
+
# Check against known invalid placeholders
|
|
125
|
+
if filepath.lower() in INVALID_PATHS:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
# Check for placeholder patterns
|
|
129
|
+
placeholder_patterns = [
|
|
130
|
+
r"^path/to/",
|
|
131
|
+
r"^your[_-]",
|
|
132
|
+
r"^example[_-]?",
|
|
133
|
+
r"<.*>", # <filename> style placeholders
|
|
134
|
+
]
|
|
135
|
+
for pattern in placeholder_patterns:
|
|
136
|
+
if re.match(pattern, filepath, re.IGNORECASE):
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
return filepath
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _parse_json_response(text: str) -> dict:
|
|
143
|
+
"""Parse JSON from response, handling markdown code blocks.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
text: Raw response text
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Parsed JSON dict
|
|
150
|
+
|
|
151
|
+
Raises:
|
|
152
|
+
ValueError: If JSON parsing fails
|
|
153
|
+
"""
|
|
154
|
+
# Try direct JSON parse first
|
|
155
|
+
try:
|
|
156
|
+
return json.loads(text)
|
|
157
|
+
except json.JSONDecodeError:
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
# Try extracting from markdown code block
|
|
161
|
+
json_match = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', text, re.DOTALL)
|
|
162
|
+
if json_match:
|
|
163
|
+
try:
|
|
164
|
+
return json.loads(json_match.group(1))
|
|
165
|
+
except json.JSONDecodeError:
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
# Try finding JSON object in text
|
|
169
|
+
json_match = re.search(r'\{[\s\S]*\}', text)
|
|
170
|
+
if json_match:
|
|
171
|
+
try:
|
|
172
|
+
return json.loads(json_match.group(0))
|
|
173
|
+
except json.JSONDecodeError:
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
raise ValueError(f"Could not parse JSON from response: {text[:200]}...")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def analyze_error(log: str, context: str, max_retries: int = 3) -> FixResult:
|
|
180
|
+
"""Analyze an error log and return a structured code fix.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
log: The error log or traceback
|
|
184
|
+
context: Source code context around the error
|
|
185
|
+
max_retries: Number of retries for rate limit errors
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
FixResult with structured fix data (filepath may be None for general errors)
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
Exception: If Gemini API call fails after retries
|
|
192
|
+
"""
|
|
193
|
+
client = _get_client()
|
|
194
|
+
|
|
195
|
+
user_prompt = _build_prompt(log, context)
|
|
196
|
+
full_prompt = f"{SYSTEM_PROMPT}\n\n{user_prompt}"
|
|
197
|
+
|
|
198
|
+
# Configure for JSON output
|
|
199
|
+
generation_config = types.GenerateContentConfig(
|
|
200
|
+
temperature=0,
|
|
201
|
+
response_mime_type="application/json",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
models_to_try = [PRIMARY_MODEL, FALLBACK_MODEL]
|
|
205
|
+
last_error = None
|
|
206
|
+
|
|
207
|
+
for model_name in models_to_try:
|
|
208
|
+
for attempt in range(max_retries):
|
|
209
|
+
try:
|
|
210
|
+
response = client.models.generate_content(
|
|
211
|
+
model=model_name,
|
|
212
|
+
contents=full_prompt,
|
|
213
|
+
config=generation_config,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
raw_text = response.text
|
|
217
|
+
|
|
218
|
+
# Parse JSON response
|
|
219
|
+
try:
|
|
220
|
+
parsed = _parse_json_response(raw_text)
|
|
221
|
+
except ValueError:
|
|
222
|
+
# If JSON parsing fails, create a basic structure
|
|
223
|
+
parsed = {
|
|
224
|
+
"filepath": None,
|
|
225
|
+
"full_code_block": raw_text,
|
|
226
|
+
"explanation": "AI returned non-JSON response. Raw output provided.",
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# Normalize and validate filepath
|
|
230
|
+
raw_filepath = parsed.get("filepath")
|
|
231
|
+
normalized_filepath = _normalize_filepath(raw_filepath)
|
|
232
|
+
|
|
233
|
+
return FixResult(
|
|
234
|
+
filepath=normalized_filepath,
|
|
235
|
+
full_code_block=parsed.get("full_code_block", ""),
|
|
236
|
+
explanation=parsed.get("explanation", ""),
|
|
237
|
+
raw_response=raw_text,
|
|
238
|
+
model_used=model_name,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
except Exception as e:
|
|
242
|
+
error_str = str(e).lower()
|
|
243
|
+
last_error = e
|
|
244
|
+
|
|
245
|
+
is_quota_error = any(x in error_str for x in [
|
|
246
|
+
"429", "quota", "rate limit", "resource exhausted"
|
|
247
|
+
])
|
|
248
|
+
|
|
249
|
+
if is_quota_error:
|
|
250
|
+
if model_name == PRIMARY_MODEL:
|
|
251
|
+
break # Try fallback model
|
|
252
|
+
|
|
253
|
+
wait_time = (2 ** attempt) * 5
|
|
254
|
+
if attempt < max_retries - 1:
|
|
255
|
+
time.sleep(wait_time)
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
raise
|
|
259
|
+
|
|
260
|
+
if model_name == PRIMARY_MODEL and last_error:
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
raise last_error
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def analyze_error_v2(
|
|
267
|
+
log: str,
|
|
268
|
+
context: str,
|
|
269
|
+
max_retries: int = 3,
|
|
270
|
+
include_upstream: bool = True,
|
|
271
|
+
) -> FixResultV2:
|
|
272
|
+
"""Analyze an error with V2 deep debugging (root cause analysis).
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
log: The error log or traceback
|
|
276
|
+
context: Source code context (should include upstream context for V2)
|
|
277
|
+
max_retries: Number of retries for rate limit errors
|
|
278
|
+
include_upstream: Whether upstream context was included
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
FixResultV2 with root cause analysis and potentially multiple fixes
|
|
282
|
+
|
|
283
|
+
Raises:
|
|
284
|
+
Exception: If Gemini API call fails after retries
|
|
285
|
+
"""
|
|
286
|
+
client = _get_client()
|
|
287
|
+
|
|
288
|
+
user_prompt = _build_prompt_v2(log, context)
|
|
289
|
+
full_prompt = f"{SYSTEM_PROMPT_V2}\n\n{user_prompt}"
|
|
290
|
+
|
|
291
|
+
# Configure for JSON output
|
|
292
|
+
generation_config = types.GenerateContentConfig(
|
|
293
|
+
temperature=0,
|
|
294
|
+
response_mime_type="application/json",
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
models_to_try = [PRIMARY_MODEL, FALLBACK_MODEL]
|
|
298
|
+
last_error = None
|
|
299
|
+
|
|
300
|
+
for model_name in models_to_try:
|
|
301
|
+
for attempt in range(max_retries):
|
|
302
|
+
try:
|
|
303
|
+
response = client.models.generate_content(
|
|
304
|
+
model=model_name,
|
|
305
|
+
contents=full_prompt,
|
|
306
|
+
config=generation_config,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
raw_text = response.text
|
|
310
|
+
|
|
311
|
+
# Parse JSON response
|
|
312
|
+
try:
|
|
313
|
+
parsed = _parse_json_response(raw_text)
|
|
314
|
+
except ValueError:
|
|
315
|
+
# If JSON parsing fails, create a basic structure
|
|
316
|
+
parsed = {
|
|
317
|
+
"filepath": None,
|
|
318
|
+
"full_code_block": raw_text,
|
|
319
|
+
"explanation": "AI returned non-JSON response. Raw output provided.",
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
# Normalize filepaths
|
|
323
|
+
normalized_filepath = _normalize_filepath(parsed.get("filepath"))
|
|
324
|
+
root_cause_file = _normalize_filepath(parsed.get("root_cause_file"))
|
|
325
|
+
|
|
326
|
+
# Parse additional fixes
|
|
327
|
+
additional_fixes = []
|
|
328
|
+
for fix_data in parsed.get("additional_fixes", []):
|
|
329
|
+
fix_path = _normalize_filepath(fix_data.get("filepath"))
|
|
330
|
+
if fix_path:
|
|
331
|
+
additional_fixes.append(AdditionalFix(
|
|
332
|
+
filepath=fix_path,
|
|
333
|
+
full_code_block=fix_data.get("full_code_block", ""),
|
|
334
|
+
explanation=fix_data.get("explanation", ""),
|
|
335
|
+
))
|
|
336
|
+
|
|
337
|
+
return FixResultV2(
|
|
338
|
+
filepath=normalized_filepath,
|
|
339
|
+
full_code_block=parsed.get("full_code_block", ""),
|
|
340
|
+
explanation=parsed.get("explanation", ""),
|
|
341
|
+
raw_response=raw_text,
|
|
342
|
+
model_used=model_name,
|
|
343
|
+
root_cause_file=root_cause_file,
|
|
344
|
+
root_cause_explanation=parsed.get("root_cause_explanation"),
|
|
345
|
+
additional_fixes=additional_fixes,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
except Exception as e:
|
|
349
|
+
error_str = str(e).lower()
|
|
350
|
+
last_error = e
|
|
351
|
+
|
|
352
|
+
is_quota_error = any(x in error_str for x in [
|
|
353
|
+
"429", "quota", "rate limit", "resource exhausted"
|
|
354
|
+
])
|
|
355
|
+
|
|
356
|
+
if is_quota_error:
|
|
357
|
+
if model_name == PRIMARY_MODEL:
|
|
358
|
+
break # Try fallback model
|
|
359
|
+
|
|
360
|
+
wait_time = (2 ** attempt) * 5
|
|
361
|
+
if attempt < max_retries - 1:
|
|
362
|
+
time.sleep(wait_time)
|
|
363
|
+
continue
|
|
364
|
+
|
|
365
|
+
raise
|
|
366
|
+
|
|
367
|
+
if model_name == PRIMARY_MODEL and last_error:
|
|
368
|
+
continue
|
|
369
|
+
|
|
370
|
+
raise last_error
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def analyze_error_simple(log: str, context: str, max_retries: int = 3) -> str:
|
|
374
|
+
"""Analyze error and return raw text (for backward compatibility).
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
log: The error log or traceback
|
|
378
|
+
context: Source code context around the error
|
|
379
|
+
max_retries: Number of retries for rate limit errors
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Raw text fix from Gemini
|
|
383
|
+
"""
|
|
384
|
+
result = analyze_error(log, context, max_retries)
|
|
385
|
+
return result.full_code_block or result.raw_response
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _build_prompt(log: str, context: str) -> str:
|
|
389
|
+
"""Build the user prompt from log and context."""
|
|
390
|
+
parts = ["## ERROR LOG", log]
|
|
391
|
+
|
|
392
|
+
if context:
|
|
393
|
+
parts.extend(["", "## SOURCE CONTEXT", context])
|
|
394
|
+
|
|
395
|
+
parts.extend([
|
|
396
|
+
"",
|
|
397
|
+
"## INSTRUCTIONS",
|
|
398
|
+
"Analyze the error above. Return the corrected code as JSON.",
|
|
399
|
+
"The full_code_block should contain the complete fixed code, ready to replace the original.",
|
|
400
|
+
"If no specific file path is in the error traceback, set filepath to null.",
|
|
401
|
+
])
|
|
402
|
+
|
|
403
|
+
return "\n".join(parts)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _build_prompt_v2(log: str, context: str) -> str:
|
|
407
|
+
"""Build V2 prompt with upstream context support."""
|
|
408
|
+
parts = ["## ERROR LOG / TRACEBACK", log]
|
|
409
|
+
|
|
410
|
+
if context:
|
|
411
|
+
parts.extend(["", context]) # Context already formatted by ContextBuilder
|
|
412
|
+
|
|
413
|
+
parts.extend([
|
|
414
|
+
"",
|
|
415
|
+
"## INSTRUCTIONS",
|
|
416
|
+
"Analyze the error above with deep project understanding.",
|
|
417
|
+
"Consider the full call chain and upstream context to identify root causes.",
|
|
418
|
+
"Return the corrected code as JSON with root cause analysis if applicable.",
|
|
419
|
+
"If the bug originates in a different file, set root_cause_file and provide fixes for both.",
|
|
420
|
+
"If no specific file path is in the traceback, set filepath to null.",
|
|
421
|
+
])
|
|
422
|
+
|
|
423
|
+
return "\n".join(parts)
|