amd-gaia 0.14.3__py3-none-any.whl → 0.15.1__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.
- {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/METADATA +223 -223
- amd_gaia-0.15.1.dist-info/RECORD +178 -0
- {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/entry_points.txt +1 -0
- {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/licenses/LICENSE.md +20 -20
- gaia/__init__.py +29 -29
- gaia/agents/__init__.py +19 -19
- gaia/agents/base/__init__.py +9 -9
- gaia/agents/base/agent.py +2177 -2177
- gaia/agents/base/api_agent.py +120 -120
- gaia/agents/base/console.py +1841 -1841
- gaia/agents/base/errors.py +237 -237
- gaia/agents/base/mcp_agent.py +86 -86
- gaia/agents/base/tools.py +83 -83
- gaia/agents/blender/agent.py +556 -556
- gaia/agents/blender/agent_simple.py +133 -135
- gaia/agents/blender/app.py +211 -211
- gaia/agents/blender/app_simple.py +41 -41
- gaia/agents/blender/core/__init__.py +16 -16
- gaia/agents/blender/core/materials.py +506 -506
- gaia/agents/blender/core/objects.py +316 -316
- gaia/agents/blender/core/rendering.py +225 -225
- gaia/agents/blender/core/scene.py +220 -220
- gaia/agents/blender/core/view.py +146 -146
- gaia/agents/chat/__init__.py +9 -9
- gaia/agents/chat/agent.py +835 -835
- gaia/agents/chat/app.py +1058 -1058
- gaia/agents/chat/session.py +508 -508
- gaia/agents/chat/tools/__init__.py +15 -15
- gaia/agents/chat/tools/file_tools.py +96 -96
- gaia/agents/chat/tools/rag_tools.py +1729 -1729
- gaia/agents/chat/tools/shell_tools.py +436 -436
- gaia/agents/code/__init__.py +7 -7
- gaia/agents/code/agent.py +549 -549
- gaia/agents/code/cli.py +377 -0
- gaia/agents/code/models.py +135 -135
- gaia/agents/code/orchestration/__init__.py +24 -24
- gaia/agents/code/orchestration/checklist_executor.py +1763 -1763
- gaia/agents/code/orchestration/checklist_generator.py +713 -713
- gaia/agents/code/orchestration/factories/__init__.py +9 -9
- gaia/agents/code/orchestration/factories/base.py +63 -63
- gaia/agents/code/orchestration/factories/nextjs_factory.py +118 -118
- gaia/agents/code/orchestration/factories/python_factory.py +106 -106
- gaia/agents/code/orchestration/orchestrator.py +841 -841
- gaia/agents/code/orchestration/project_analyzer.py +391 -391
- gaia/agents/code/orchestration/steps/__init__.py +67 -67
- gaia/agents/code/orchestration/steps/base.py +188 -188
- gaia/agents/code/orchestration/steps/error_handler.py +314 -314
- gaia/agents/code/orchestration/steps/nextjs.py +828 -828
- gaia/agents/code/orchestration/steps/python.py +307 -307
- gaia/agents/code/orchestration/template_catalog.py +469 -469
- gaia/agents/code/orchestration/workflows/__init__.py +14 -14
- gaia/agents/code/orchestration/workflows/base.py +80 -80
- gaia/agents/code/orchestration/workflows/nextjs.py +186 -186
- gaia/agents/code/orchestration/workflows/python.py +94 -94
- gaia/agents/code/prompts/__init__.py +11 -11
- gaia/agents/code/prompts/base_prompt.py +77 -77
- gaia/agents/code/prompts/code_patterns.py +2036 -2036
- gaia/agents/code/prompts/nextjs_prompt.py +40 -40
- gaia/agents/code/prompts/python_prompt.py +109 -109
- gaia/agents/code/schema_inference.py +365 -365
- gaia/agents/code/system_prompt.py +41 -41
- gaia/agents/code/tools/__init__.py +42 -42
- gaia/agents/code/tools/cli_tools.py +1138 -1138
- gaia/agents/code/tools/code_formatting.py +319 -319
- gaia/agents/code/tools/code_tools.py +769 -769
- gaia/agents/code/tools/error_fixing.py +1347 -1347
- gaia/agents/code/tools/external_tools.py +180 -180
- gaia/agents/code/tools/file_io.py +845 -845
- gaia/agents/code/tools/prisma_tools.py +190 -190
- gaia/agents/code/tools/project_management.py +1016 -1016
- gaia/agents/code/tools/testing.py +321 -321
- gaia/agents/code/tools/typescript_tools.py +122 -122
- gaia/agents/code/tools/validation_parsing.py +461 -461
- gaia/agents/code/tools/validation_tools.py +806 -806
- gaia/agents/code/tools/web_dev_tools.py +1758 -1758
- gaia/agents/code/validators/__init__.py +16 -16
- gaia/agents/code/validators/antipattern_checker.py +241 -241
- gaia/agents/code/validators/ast_analyzer.py +197 -197
- gaia/agents/code/validators/requirements_validator.py +145 -145
- gaia/agents/code/validators/syntax_validator.py +171 -171
- gaia/agents/docker/__init__.py +7 -7
- gaia/agents/docker/agent.py +642 -642
- gaia/agents/emr/__init__.py +8 -8
- gaia/agents/emr/agent.py +1506 -1506
- gaia/agents/emr/cli.py +1322 -1322
- gaia/agents/emr/constants.py +475 -475
- gaia/agents/emr/dashboard/__init__.py +4 -4
- gaia/agents/emr/dashboard/server.py +1974 -1974
- gaia/agents/jira/__init__.py +11 -11
- gaia/agents/jira/agent.py +894 -894
- gaia/agents/jira/jql_templates.py +299 -299
- gaia/agents/routing/__init__.py +7 -7
- gaia/agents/routing/agent.py +567 -570
- gaia/agents/routing/system_prompt.py +75 -75
- gaia/agents/summarize/__init__.py +11 -0
- gaia/agents/summarize/agent.py +885 -0
- gaia/agents/summarize/prompts.py +129 -0
- gaia/api/__init__.py +23 -23
- gaia/api/agent_registry.py +238 -238
- gaia/api/app.py +305 -305
- gaia/api/openai_server.py +575 -575
- gaia/api/schemas.py +186 -186
- gaia/api/sse_handler.py +373 -373
- gaia/apps/__init__.py +4 -4
- gaia/apps/llm/__init__.py +6 -6
- gaia/apps/llm/app.py +173 -169
- gaia/apps/summarize/app.py +116 -633
- gaia/apps/summarize/html_viewer.py +133 -133
- gaia/apps/summarize/pdf_formatter.py +284 -284
- gaia/audio/__init__.py +2 -2
- gaia/audio/audio_client.py +439 -439
- gaia/audio/audio_recorder.py +269 -269
- gaia/audio/kokoro_tts.py +599 -599
- gaia/audio/whisper_asr.py +432 -432
- gaia/chat/__init__.py +16 -16
- gaia/chat/app.py +430 -430
- gaia/chat/prompts.py +522 -522
- gaia/chat/sdk.py +1228 -1225
- gaia/cli.py +5481 -5621
- gaia/database/__init__.py +10 -10
- gaia/database/agent.py +176 -176
- gaia/database/mixin.py +290 -290
- gaia/database/testing.py +64 -64
- gaia/eval/batch_experiment.py +2332 -2332
- gaia/eval/claude.py +542 -542
- gaia/eval/config.py +37 -37
- gaia/eval/email_generator.py +512 -512
- gaia/eval/eval.py +3179 -3179
- gaia/eval/groundtruth.py +1130 -1130
- gaia/eval/transcript_generator.py +582 -582
- gaia/eval/webapp/README.md +167 -167
- gaia/eval/webapp/package-lock.json +875 -875
- gaia/eval/webapp/package.json +20 -20
- gaia/eval/webapp/public/app.js +3402 -3402
- gaia/eval/webapp/public/index.html +87 -87
- gaia/eval/webapp/public/styles.css +3661 -3661
- gaia/eval/webapp/server.js +415 -415
- gaia/eval/webapp/test-setup.js +72 -72
- gaia/llm/__init__.py +9 -2
- gaia/llm/base_client.py +60 -0
- gaia/llm/exceptions.py +12 -0
- gaia/llm/factory.py +70 -0
- gaia/llm/lemonade_client.py +3236 -3221
- gaia/llm/lemonade_manager.py +294 -294
- gaia/llm/providers/__init__.py +9 -0
- gaia/llm/providers/claude.py +108 -0
- gaia/llm/providers/lemonade.py +120 -0
- gaia/llm/providers/openai_provider.py +79 -0
- gaia/llm/vlm_client.py +382 -382
- gaia/logger.py +189 -189
- gaia/mcp/agent_mcp_server.py +245 -245
- gaia/mcp/blender_mcp_client.py +138 -138
- gaia/mcp/blender_mcp_server.py +648 -648
- gaia/mcp/context7_cache.py +332 -332
- gaia/mcp/external_services.py +518 -518
- gaia/mcp/mcp_bridge.py +811 -550
- gaia/mcp/servers/__init__.py +6 -6
- gaia/mcp/servers/docker_mcp.py +83 -83
- gaia/perf_analysis.py +361 -0
- gaia/rag/__init__.py +10 -10
- gaia/rag/app.py +293 -293
- gaia/rag/demo.py +304 -304
- gaia/rag/pdf_utils.py +235 -235
- gaia/rag/sdk.py +2194 -2194
- gaia/security.py +163 -163
- gaia/talk/app.py +289 -289
- gaia/talk/sdk.py +538 -538
- gaia/testing/__init__.py +87 -87
- gaia/testing/assertions.py +330 -330
- gaia/testing/fixtures.py +333 -333
- gaia/testing/mocks.py +493 -493
- gaia/util.py +46 -46
- gaia/utils/__init__.py +33 -33
- gaia/utils/file_watcher.py +675 -675
- gaia/utils/parsing.py +223 -223
- gaia/version.py +100 -100
- amd_gaia-0.14.3.dist-info/RECORD +0 -168
- gaia/agents/code/app.py +0 -266
- gaia/llm/llm_client.py +0 -729
- {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/WHEEL +0 -0
- {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/top_level.txt +0 -0
|
@@ -1,314 +1,314 @@
|
|
|
1
|
-
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
3
|
-
"""
|
|
4
|
-
Three-tier error recovery for orchestration workflows.
|
|
5
|
-
|
|
6
|
-
Provides intelligent error handling with:
|
|
7
|
-
- RETRY: For transient errors (network, resources)
|
|
8
|
-
- FIX_AND_RETRY: For known fixable errors (missing deps, compilation)
|
|
9
|
-
- ESCALATE: For complex errors requiring LLM intervention
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import logging
|
|
13
|
-
import re
|
|
14
|
-
import time
|
|
15
|
-
from dataclasses import dataclass
|
|
16
|
-
from enum import Enum, auto
|
|
17
|
-
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
18
|
-
|
|
19
|
-
from .base import ErrorCategory
|
|
20
|
-
|
|
21
|
-
logger = logging.getLogger(__name__)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class RecoveryAction(Enum):
|
|
25
|
-
"""Actions the error handler can take."""
|
|
26
|
-
|
|
27
|
-
RETRY = auto() # Wait and retry same operation
|
|
28
|
-
FIX_AND_RETRY = auto() # Execute fix, then retry
|
|
29
|
-
ESCALATE = auto() # Use LLM to diagnose and fix
|
|
30
|
-
ABORT = auto() # Give up, unrecoverable
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@dataclass
|
|
34
|
-
class ErrorPattern:
|
|
35
|
-
"""Pattern for matching and handling specific errors."""
|
|
36
|
-
|
|
37
|
-
pattern: str # Regex pattern to match
|
|
38
|
-
category: ErrorCategory
|
|
39
|
-
action: RecoveryAction
|
|
40
|
-
fix_command: Optional[str] = None # Command to run for FIX_AND_RETRY
|
|
41
|
-
max_retries: int = 2
|
|
42
|
-
delay_seconds: float = 1.0
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
# Error patterns based on existing cli_tools.py ERROR_PATTERNS
|
|
46
|
-
ERROR_PATTERNS: List[ErrorPattern] = [
|
|
47
|
-
# Network errors - retry
|
|
48
|
-
ErrorPattern(
|
|
49
|
-
pattern=r"ECONNREFUSED|ETIMEDOUT|ENOTFOUND|network\s+error",
|
|
50
|
-
category=ErrorCategory.NETWORK,
|
|
51
|
-
action=RecoveryAction.RETRY,
|
|
52
|
-
max_retries=3,
|
|
53
|
-
delay_seconds=2.0,
|
|
54
|
-
),
|
|
55
|
-
# Resource errors - retry with delay
|
|
56
|
-
ErrorPattern(
|
|
57
|
-
pattern=r"ENOSPC|ENOMEM|out\s+of\s+memory",
|
|
58
|
-
category=ErrorCategory.RESOURCE,
|
|
59
|
-
action=RecoveryAction.RETRY,
|
|
60
|
-
max_retries=2,
|
|
61
|
-
delay_seconds=5.0,
|
|
62
|
-
),
|
|
63
|
-
# Missing dependencies - fix and retry
|
|
64
|
-
ErrorPattern(
|
|
65
|
-
pattern=r"Cannot find module '([^']+)'|Module not found.*'([^']+)'",
|
|
66
|
-
category=ErrorCategory.DEPENDENCY,
|
|
67
|
-
action=RecoveryAction.FIX_AND_RETRY,
|
|
68
|
-
fix_command="npm install {module}",
|
|
69
|
-
max_retries=2,
|
|
70
|
-
),
|
|
71
|
-
ErrorPattern(
|
|
72
|
-
pattern=r"ModuleNotFoundError:\s+No module named '([^']+)'",
|
|
73
|
-
category=ErrorCategory.DEPENDENCY,
|
|
74
|
-
action=RecoveryAction.FIX_AND_RETRY,
|
|
75
|
-
fix_command="uv pip install {module}",
|
|
76
|
-
max_retries=2,
|
|
77
|
-
),
|
|
78
|
-
# TypeScript compilation - escalate to LLM
|
|
79
|
-
ErrorPattern(
|
|
80
|
-
pattern=r"TS\d+:|error TS\d+|Type '.*' is not assignable",
|
|
81
|
-
category=ErrorCategory.COMPILATION,
|
|
82
|
-
action=RecoveryAction.ESCALATE,
|
|
83
|
-
max_retries=1,
|
|
84
|
-
),
|
|
85
|
-
# Prisma errors - fix and retry
|
|
86
|
-
ErrorPattern(
|
|
87
|
-
pattern=r"prisma generate|@prisma/client.*not.*generated",
|
|
88
|
-
category=ErrorCategory.DEPENDENCY,
|
|
89
|
-
action=RecoveryAction.FIX_AND_RETRY,
|
|
90
|
-
fix_command="npx prisma generate",
|
|
91
|
-
max_retries=2,
|
|
92
|
-
),
|
|
93
|
-
# Syntax errors - escalate to LLM
|
|
94
|
-
ErrorPattern(
|
|
95
|
-
pattern=r"SyntaxError:|Unexpected token|Parse error",
|
|
96
|
-
category=ErrorCategory.SYNTAX,
|
|
97
|
-
action=RecoveryAction.ESCALATE,
|
|
98
|
-
max_retries=1,
|
|
99
|
-
),
|
|
100
|
-
# Lint errors - escalate to LLM for fix
|
|
101
|
-
ErrorPattern(
|
|
102
|
-
pattern=r"eslint.*error|warning.*react-hooks|unused variable",
|
|
103
|
-
category=ErrorCategory.VALIDATION,
|
|
104
|
-
action=RecoveryAction.ESCALATE,
|
|
105
|
-
max_retries=1,
|
|
106
|
-
),
|
|
107
|
-
# CSS content type errors (Issue #1002) - escalate to LLM
|
|
108
|
-
# Catches TypeScript/JavaScript code in CSS files
|
|
109
|
-
ErrorPattern(
|
|
110
|
-
pattern=r"CSS file contains.*TypeScript|CRITICAL.*CSS file|globals\.css contains",
|
|
111
|
-
category=ErrorCategory.VALIDATION,
|
|
112
|
-
action=RecoveryAction.ESCALATE,
|
|
113
|
-
max_retries=2, # Allow retries - LLM can regenerate correct CSS
|
|
114
|
-
),
|
|
115
|
-
]
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
class ErrorHandler:
|
|
119
|
-
"""Handles errors with three-tier recovery strategy."""
|
|
120
|
-
|
|
121
|
-
def __init__(
|
|
122
|
-
self,
|
|
123
|
-
command_executor: Optional[Callable[[str], Tuple[int, str]]] = None,
|
|
124
|
-
llm_fixer: Optional[Callable[[str, str], Optional[str]]] = None,
|
|
125
|
-
):
|
|
126
|
-
"""Initialize error handler.
|
|
127
|
-
|
|
128
|
-
Args:
|
|
129
|
-
command_executor: Function to run shell commands (returns exit_code, output)
|
|
130
|
-
llm_fixer: Function to fix code using LLM (takes error, code, returns fixed code)
|
|
131
|
-
"""
|
|
132
|
-
self.command_executor = command_executor
|
|
133
|
-
self.llm_fixer = llm_fixer
|
|
134
|
-
self.retry_counts: Dict[str, int] = {}
|
|
135
|
-
|
|
136
|
-
def categorize_error(self, error_text: str) -> Tuple[ErrorCategory, ErrorPattern]:
|
|
137
|
-
"""Categorize an error and find matching pattern.
|
|
138
|
-
|
|
139
|
-
Args:
|
|
140
|
-
error_text: The error message to categorize
|
|
141
|
-
|
|
142
|
-
Returns:
|
|
143
|
-
Tuple of (ErrorCategory, matching ErrorPattern or default)
|
|
144
|
-
"""
|
|
145
|
-
for pattern in ERROR_PATTERNS:
|
|
146
|
-
if re.search(pattern.pattern, error_text, re.IGNORECASE):
|
|
147
|
-
return pattern.category, pattern
|
|
148
|
-
|
|
149
|
-
# Default pattern for unknown errors
|
|
150
|
-
return ErrorCategory.UNKNOWN, ErrorPattern(
|
|
151
|
-
pattern=".*",
|
|
152
|
-
category=ErrorCategory.UNKNOWN,
|
|
153
|
-
action=RecoveryAction.ESCALATE,
|
|
154
|
-
max_retries=1,
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
def handle_error(
|
|
158
|
-
self,
|
|
159
|
-
step_name: str,
|
|
160
|
-
error_text: str,
|
|
161
|
-
context: Optional[Dict[str, Any]] = None,
|
|
162
|
-
) -> Tuple[RecoveryAction, Optional[str]]:
|
|
163
|
-
"""Handle an error with appropriate recovery action.
|
|
164
|
-
|
|
165
|
-
Args:
|
|
166
|
-
step_name: Name of the step that failed
|
|
167
|
-
error_text: The error message
|
|
168
|
-
context: Optional context (e.g., code being executed)
|
|
169
|
-
|
|
170
|
-
Returns:
|
|
171
|
-
Tuple of (action to take, optional fix result/message)
|
|
172
|
-
"""
|
|
173
|
-
category, pattern = self.categorize_error(error_text)
|
|
174
|
-
retry_key = f"{step_name}:{category.name}"
|
|
175
|
-
|
|
176
|
-
# Check retry count
|
|
177
|
-
current_retries = self.retry_counts.get(retry_key, 0)
|
|
178
|
-
if current_retries >= pattern.max_retries:
|
|
179
|
-
logger.warning(
|
|
180
|
-
f"Max retries ({pattern.max_retries}) exceeded for {step_name}"
|
|
181
|
-
)
|
|
182
|
-
return RecoveryAction.ABORT, f"Max retries exceeded: {error_text}"
|
|
183
|
-
|
|
184
|
-
self.retry_counts[retry_key] = current_retries + 1
|
|
185
|
-
|
|
186
|
-
logger.info(
|
|
187
|
-
f"Error recovery: {category.name} -> {pattern.action.name} "
|
|
188
|
-
f"(attempt {current_retries + 1}/{pattern.max_retries})"
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
if pattern.action == RecoveryAction.RETRY:
|
|
192
|
-
return self._handle_retry(pattern)
|
|
193
|
-
|
|
194
|
-
if pattern.action == RecoveryAction.FIX_AND_RETRY:
|
|
195
|
-
return self._handle_fix_and_retry(pattern, error_text)
|
|
196
|
-
|
|
197
|
-
if pattern.action == RecoveryAction.ESCALATE:
|
|
198
|
-
return self._handle_escalate(error_text, context)
|
|
199
|
-
|
|
200
|
-
return RecoveryAction.ABORT, error_text
|
|
201
|
-
|
|
202
|
-
def _handle_retry(
|
|
203
|
-
self, pattern: ErrorPattern
|
|
204
|
-
) -> Tuple[RecoveryAction, Optional[str]]:
|
|
205
|
-
"""Handle retry action with delay."""
|
|
206
|
-
if pattern.delay_seconds > 0:
|
|
207
|
-
logger.info(f"Waiting {pattern.delay_seconds}s before retry...")
|
|
208
|
-
time.sleep(pattern.delay_seconds)
|
|
209
|
-
return RecoveryAction.RETRY, None
|
|
210
|
-
|
|
211
|
-
def _handle_fix_and_retry(
|
|
212
|
-
self, pattern: ErrorPattern, error_text: str
|
|
213
|
-
) -> Tuple[RecoveryAction, Optional[str]]:
|
|
214
|
-
"""Handle fix-and-retry action."""
|
|
215
|
-
if not pattern.fix_command or not self.command_executor:
|
|
216
|
-
return RecoveryAction.ESCALATE, "No fix command available"
|
|
217
|
-
|
|
218
|
-
# Extract module name from error if applicable
|
|
219
|
-
fix_cmd = pattern.fix_command
|
|
220
|
-
match = re.search(pattern.pattern, error_text, re.IGNORECASE)
|
|
221
|
-
if match and match.groups():
|
|
222
|
-
# Use first captured group as module name
|
|
223
|
-
module = (
|
|
224
|
-
match.group(1) or match.group(2)
|
|
225
|
-
if len(match.groups()) > 1
|
|
226
|
-
else match.group(1)
|
|
227
|
-
)
|
|
228
|
-
if module:
|
|
229
|
-
fix_cmd = fix_cmd.format(module=module)
|
|
230
|
-
|
|
231
|
-
logger.info(f"Executing fix command: {fix_cmd}")
|
|
232
|
-
exit_code, output = self.command_executor(fix_cmd)
|
|
233
|
-
|
|
234
|
-
if exit_code == 0:
|
|
235
|
-
return RecoveryAction.RETRY, f"Fix applied: {fix_cmd}"
|
|
236
|
-
return RecoveryAction.ESCALATE, f"Fix failed: {output}"
|
|
237
|
-
|
|
238
|
-
def _handle_escalate(
|
|
239
|
-
self, error_text: str, context: Optional[Dict[str, Any]] = None
|
|
240
|
-
) -> Tuple[RecoveryAction, Optional[str]]:
|
|
241
|
-
"""Handle escalation to LLM.
|
|
242
|
-
|
|
243
|
-
For TypeScript errors, parses file path from error and reads content.
|
|
244
|
-
"""
|
|
245
|
-
from pathlib import Path
|
|
246
|
-
|
|
247
|
-
if not self.llm_fixer:
|
|
248
|
-
return RecoveryAction.ABORT, "No LLM fixer available"
|
|
249
|
-
|
|
250
|
-
code = context.get("code", "") if context else ""
|
|
251
|
-
project_dir = context.get("project_dir", "") if context else ""
|
|
252
|
-
error_file_path = None
|
|
253
|
-
|
|
254
|
-
# If no code provided, try to extract file path from TypeScript error
|
|
255
|
-
# Format: filename.ts(line,col): error TSxxxx: message
|
|
256
|
-
if not code and project_dir:
|
|
257
|
-
ts_error_match = re.search(
|
|
258
|
-
r"^([^\s(]+\.tsx?)\(\d+,\d+\):", error_text, re.MULTILINE
|
|
259
|
-
)
|
|
260
|
-
if ts_error_match:
|
|
261
|
-
error_file_path = ts_error_match.group(1)
|
|
262
|
-
full_path = Path(project_dir) / error_file_path
|
|
263
|
-
if full_path.exists():
|
|
264
|
-
try:
|
|
265
|
-
code = full_path.read_text(encoding="utf-8")
|
|
266
|
-
logger.info(f"Read file for LLM fix: {error_file_path}")
|
|
267
|
-
except Exception as e:
|
|
268
|
-
logger.warning(f"Could not read {error_file_path}: {e}")
|
|
269
|
-
|
|
270
|
-
fixed_code = self.llm_fixer(error_text, code)
|
|
271
|
-
|
|
272
|
-
if fixed_code:
|
|
273
|
-
# If we read a file, write the fix back
|
|
274
|
-
if error_file_path and project_dir:
|
|
275
|
-
full_path = Path(project_dir) / error_file_path
|
|
276
|
-
try:
|
|
277
|
-
full_path.write_text(fixed_code, encoding="utf-8")
|
|
278
|
-
logger.info(f"Wrote LLM fix to: {error_file_path}")
|
|
279
|
-
except Exception as e:
|
|
280
|
-
logger.warning(f"Could not write fix to {error_file_path}: {e}")
|
|
281
|
-
|
|
282
|
-
return RecoveryAction.RETRY, fixed_code
|
|
283
|
-
return RecoveryAction.ABORT, "LLM could not fix the error"
|
|
284
|
-
|
|
285
|
-
def reset_retry_count(self, step_name: str) -> None:
|
|
286
|
-
"""Reset retry count for a step after success."""
|
|
287
|
-
keys_to_remove = [k for k in self.retry_counts if k.startswith(f"{step_name}:")]
|
|
288
|
-
for key in keys_to_remove:
|
|
289
|
-
del self.retry_counts[key]
|
|
290
|
-
|
|
291
|
-
def get_recovery_suggestion(self, error_text: str) -> str:
|
|
292
|
-
"""Get a human-readable recovery suggestion for an error.
|
|
293
|
-
|
|
294
|
-
Args:
|
|
295
|
-
error_text: The error message
|
|
296
|
-
|
|
297
|
-
Returns:
|
|
298
|
-
Suggestion string for how to fix the error
|
|
299
|
-
"""
|
|
300
|
-
category, pattern = self.categorize_error(error_text)
|
|
301
|
-
|
|
302
|
-
suggestions = {
|
|
303
|
-
ErrorCategory.NETWORK: "Check network connection and retry",
|
|
304
|
-
ErrorCategory.RESOURCE: "Free up system resources (disk/memory) and retry",
|
|
305
|
-
ErrorCategory.DEPENDENCY: f"Install missing dependency: {pattern.fix_command or 'check error message'}",
|
|
306
|
-
ErrorCategory.COMPILATION: "Fix TypeScript/compilation errors in the code",
|
|
307
|
-
ErrorCategory.SYNTAX: "Fix syntax errors in the code",
|
|
308
|
-
ErrorCategory.RUNTIME: "Debug runtime error and fix logic",
|
|
309
|
-
ErrorCategory.VALIDATION: "Fix linting/validation warnings",
|
|
310
|
-
ErrorCategory.CONFIGURATION: "Check configuration files for errors",
|
|
311
|
-
ErrorCategory.UNKNOWN: "Review error message and investigate",
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return suggestions.get(category, "Unknown error - investigate")
|
|
1
|
+
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""
|
|
4
|
+
Three-tier error recovery for orchestration workflows.
|
|
5
|
+
|
|
6
|
+
Provides intelligent error handling with:
|
|
7
|
+
- RETRY: For transient errors (network, resources)
|
|
8
|
+
- FIX_AND_RETRY: For known fixable errors (missing deps, compilation)
|
|
9
|
+
- ESCALATE: For complex errors requiring LLM intervention
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import re
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from enum import Enum, auto
|
|
17
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
from .base import ErrorCategory
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RecoveryAction(Enum):
|
|
25
|
+
"""Actions the error handler can take."""
|
|
26
|
+
|
|
27
|
+
RETRY = auto() # Wait and retry same operation
|
|
28
|
+
FIX_AND_RETRY = auto() # Execute fix, then retry
|
|
29
|
+
ESCALATE = auto() # Use LLM to diagnose and fix
|
|
30
|
+
ABORT = auto() # Give up, unrecoverable
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ErrorPattern:
|
|
35
|
+
"""Pattern for matching and handling specific errors."""
|
|
36
|
+
|
|
37
|
+
pattern: str # Regex pattern to match
|
|
38
|
+
category: ErrorCategory
|
|
39
|
+
action: RecoveryAction
|
|
40
|
+
fix_command: Optional[str] = None # Command to run for FIX_AND_RETRY
|
|
41
|
+
max_retries: int = 2
|
|
42
|
+
delay_seconds: float = 1.0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Error patterns based on existing cli_tools.py ERROR_PATTERNS
|
|
46
|
+
ERROR_PATTERNS: List[ErrorPattern] = [
|
|
47
|
+
# Network errors - retry
|
|
48
|
+
ErrorPattern(
|
|
49
|
+
pattern=r"ECONNREFUSED|ETIMEDOUT|ENOTFOUND|network\s+error",
|
|
50
|
+
category=ErrorCategory.NETWORK,
|
|
51
|
+
action=RecoveryAction.RETRY,
|
|
52
|
+
max_retries=3,
|
|
53
|
+
delay_seconds=2.0,
|
|
54
|
+
),
|
|
55
|
+
# Resource errors - retry with delay
|
|
56
|
+
ErrorPattern(
|
|
57
|
+
pattern=r"ENOSPC|ENOMEM|out\s+of\s+memory",
|
|
58
|
+
category=ErrorCategory.RESOURCE,
|
|
59
|
+
action=RecoveryAction.RETRY,
|
|
60
|
+
max_retries=2,
|
|
61
|
+
delay_seconds=5.0,
|
|
62
|
+
),
|
|
63
|
+
# Missing dependencies - fix and retry
|
|
64
|
+
ErrorPattern(
|
|
65
|
+
pattern=r"Cannot find module '([^']+)'|Module not found.*'([^']+)'",
|
|
66
|
+
category=ErrorCategory.DEPENDENCY,
|
|
67
|
+
action=RecoveryAction.FIX_AND_RETRY,
|
|
68
|
+
fix_command="npm install {module}",
|
|
69
|
+
max_retries=2,
|
|
70
|
+
),
|
|
71
|
+
ErrorPattern(
|
|
72
|
+
pattern=r"ModuleNotFoundError:\s+No module named '([^']+)'",
|
|
73
|
+
category=ErrorCategory.DEPENDENCY,
|
|
74
|
+
action=RecoveryAction.FIX_AND_RETRY,
|
|
75
|
+
fix_command="uv pip install {module}",
|
|
76
|
+
max_retries=2,
|
|
77
|
+
),
|
|
78
|
+
# TypeScript compilation - escalate to LLM
|
|
79
|
+
ErrorPattern(
|
|
80
|
+
pattern=r"TS\d+:|error TS\d+|Type '.*' is not assignable",
|
|
81
|
+
category=ErrorCategory.COMPILATION,
|
|
82
|
+
action=RecoveryAction.ESCALATE,
|
|
83
|
+
max_retries=1,
|
|
84
|
+
),
|
|
85
|
+
# Prisma errors - fix and retry
|
|
86
|
+
ErrorPattern(
|
|
87
|
+
pattern=r"prisma generate|@prisma/client.*not.*generated",
|
|
88
|
+
category=ErrorCategory.DEPENDENCY,
|
|
89
|
+
action=RecoveryAction.FIX_AND_RETRY,
|
|
90
|
+
fix_command="npx prisma generate",
|
|
91
|
+
max_retries=2,
|
|
92
|
+
),
|
|
93
|
+
# Syntax errors - escalate to LLM
|
|
94
|
+
ErrorPattern(
|
|
95
|
+
pattern=r"SyntaxError:|Unexpected token|Parse error",
|
|
96
|
+
category=ErrorCategory.SYNTAX,
|
|
97
|
+
action=RecoveryAction.ESCALATE,
|
|
98
|
+
max_retries=1,
|
|
99
|
+
),
|
|
100
|
+
# Lint errors - escalate to LLM for fix
|
|
101
|
+
ErrorPattern(
|
|
102
|
+
pattern=r"eslint.*error|warning.*react-hooks|unused variable",
|
|
103
|
+
category=ErrorCategory.VALIDATION,
|
|
104
|
+
action=RecoveryAction.ESCALATE,
|
|
105
|
+
max_retries=1,
|
|
106
|
+
),
|
|
107
|
+
# CSS content type errors (Issue #1002) - escalate to LLM
|
|
108
|
+
# Catches TypeScript/JavaScript code in CSS files
|
|
109
|
+
ErrorPattern(
|
|
110
|
+
pattern=r"CSS file contains.*TypeScript|CRITICAL.*CSS file|globals\.css contains",
|
|
111
|
+
category=ErrorCategory.VALIDATION,
|
|
112
|
+
action=RecoveryAction.ESCALATE,
|
|
113
|
+
max_retries=2, # Allow retries - LLM can regenerate correct CSS
|
|
114
|
+
),
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ErrorHandler:
|
|
119
|
+
"""Handles errors with three-tier recovery strategy."""
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
command_executor: Optional[Callable[[str], Tuple[int, str]]] = None,
|
|
124
|
+
llm_fixer: Optional[Callable[[str, str], Optional[str]]] = None,
|
|
125
|
+
):
|
|
126
|
+
"""Initialize error handler.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
command_executor: Function to run shell commands (returns exit_code, output)
|
|
130
|
+
llm_fixer: Function to fix code using LLM (takes error, code, returns fixed code)
|
|
131
|
+
"""
|
|
132
|
+
self.command_executor = command_executor
|
|
133
|
+
self.llm_fixer = llm_fixer
|
|
134
|
+
self.retry_counts: Dict[str, int] = {}
|
|
135
|
+
|
|
136
|
+
def categorize_error(self, error_text: str) -> Tuple[ErrorCategory, ErrorPattern]:
|
|
137
|
+
"""Categorize an error and find matching pattern.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
error_text: The error message to categorize
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Tuple of (ErrorCategory, matching ErrorPattern or default)
|
|
144
|
+
"""
|
|
145
|
+
for pattern in ERROR_PATTERNS:
|
|
146
|
+
if re.search(pattern.pattern, error_text, re.IGNORECASE):
|
|
147
|
+
return pattern.category, pattern
|
|
148
|
+
|
|
149
|
+
# Default pattern for unknown errors
|
|
150
|
+
return ErrorCategory.UNKNOWN, ErrorPattern(
|
|
151
|
+
pattern=".*",
|
|
152
|
+
category=ErrorCategory.UNKNOWN,
|
|
153
|
+
action=RecoveryAction.ESCALATE,
|
|
154
|
+
max_retries=1,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def handle_error(
|
|
158
|
+
self,
|
|
159
|
+
step_name: str,
|
|
160
|
+
error_text: str,
|
|
161
|
+
context: Optional[Dict[str, Any]] = None,
|
|
162
|
+
) -> Tuple[RecoveryAction, Optional[str]]:
|
|
163
|
+
"""Handle an error with appropriate recovery action.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
step_name: Name of the step that failed
|
|
167
|
+
error_text: The error message
|
|
168
|
+
context: Optional context (e.g., code being executed)
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Tuple of (action to take, optional fix result/message)
|
|
172
|
+
"""
|
|
173
|
+
category, pattern = self.categorize_error(error_text)
|
|
174
|
+
retry_key = f"{step_name}:{category.name}"
|
|
175
|
+
|
|
176
|
+
# Check retry count
|
|
177
|
+
current_retries = self.retry_counts.get(retry_key, 0)
|
|
178
|
+
if current_retries >= pattern.max_retries:
|
|
179
|
+
logger.warning(
|
|
180
|
+
f"Max retries ({pattern.max_retries}) exceeded for {step_name}"
|
|
181
|
+
)
|
|
182
|
+
return RecoveryAction.ABORT, f"Max retries exceeded: {error_text}"
|
|
183
|
+
|
|
184
|
+
self.retry_counts[retry_key] = current_retries + 1
|
|
185
|
+
|
|
186
|
+
logger.info(
|
|
187
|
+
f"Error recovery: {category.name} -> {pattern.action.name} "
|
|
188
|
+
f"(attempt {current_retries + 1}/{pattern.max_retries})"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if pattern.action == RecoveryAction.RETRY:
|
|
192
|
+
return self._handle_retry(pattern)
|
|
193
|
+
|
|
194
|
+
if pattern.action == RecoveryAction.FIX_AND_RETRY:
|
|
195
|
+
return self._handle_fix_and_retry(pattern, error_text)
|
|
196
|
+
|
|
197
|
+
if pattern.action == RecoveryAction.ESCALATE:
|
|
198
|
+
return self._handle_escalate(error_text, context)
|
|
199
|
+
|
|
200
|
+
return RecoveryAction.ABORT, error_text
|
|
201
|
+
|
|
202
|
+
def _handle_retry(
|
|
203
|
+
self, pattern: ErrorPattern
|
|
204
|
+
) -> Tuple[RecoveryAction, Optional[str]]:
|
|
205
|
+
"""Handle retry action with delay."""
|
|
206
|
+
if pattern.delay_seconds > 0:
|
|
207
|
+
logger.info(f"Waiting {pattern.delay_seconds}s before retry...")
|
|
208
|
+
time.sleep(pattern.delay_seconds)
|
|
209
|
+
return RecoveryAction.RETRY, None
|
|
210
|
+
|
|
211
|
+
def _handle_fix_and_retry(
|
|
212
|
+
self, pattern: ErrorPattern, error_text: str
|
|
213
|
+
) -> Tuple[RecoveryAction, Optional[str]]:
|
|
214
|
+
"""Handle fix-and-retry action."""
|
|
215
|
+
if not pattern.fix_command or not self.command_executor:
|
|
216
|
+
return RecoveryAction.ESCALATE, "No fix command available"
|
|
217
|
+
|
|
218
|
+
# Extract module name from error if applicable
|
|
219
|
+
fix_cmd = pattern.fix_command
|
|
220
|
+
match = re.search(pattern.pattern, error_text, re.IGNORECASE)
|
|
221
|
+
if match and match.groups():
|
|
222
|
+
# Use first captured group as module name
|
|
223
|
+
module = (
|
|
224
|
+
match.group(1) or match.group(2)
|
|
225
|
+
if len(match.groups()) > 1
|
|
226
|
+
else match.group(1)
|
|
227
|
+
)
|
|
228
|
+
if module:
|
|
229
|
+
fix_cmd = fix_cmd.format(module=module)
|
|
230
|
+
|
|
231
|
+
logger.info(f"Executing fix command: {fix_cmd}")
|
|
232
|
+
exit_code, output = self.command_executor(fix_cmd)
|
|
233
|
+
|
|
234
|
+
if exit_code == 0:
|
|
235
|
+
return RecoveryAction.RETRY, f"Fix applied: {fix_cmd}"
|
|
236
|
+
return RecoveryAction.ESCALATE, f"Fix failed: {output}"
|
|
237
|
+
|
|
238
|
+
def _handle_escalate(
|
|
239
|
+
self, error_text: str, context: Optional[Dict[str, Any]] = None
|
|
240
|
+
) -> Tuple[RecoveryAction, Optional[str]]:
|
|
241
|
+
"""Handle escalation to LLM.
|
|
242
|
+
|
|
243
|
+
For TypeScript errors, parses file path from error and reads content.
|
|
244
|
+
"""
|
|
245
|
+
from pathlib import Path
|
|
246
|
+
|
|
247
|
+
if not self.llm_fixer:
|
|
248
|
+
return RecoveryAction.ABORT, "No LLM fixer available"
|
|
249
|
+
|
|
250
|
+
code = context.get("code", "") if context else ""
|
|
251
|
+
project_dir = context.get("project_dir", "") if context else ""
|
|
252
|
+
error_file_path = None
|
|
253
|
+
|
|
254
|
+
# If no code provided, try to extract file path from TypeScript error
|
|
255
|
+
# Format: filename.ts(line,col): error TSxxxx: message
|
|
256
|
+
if not code and project_dir:
|
|
257
|
+
ts_error_match = re.search(
|
|
258
|
+
r"^([^\s(]+\.tsx?)\(\d+,\d+\):", error_text, re.MULTILINE
|
|
259
|
+
)
|
|
260
|
+
if ts_error_match:
|
|
261
|
+
error_file_path = ts_error_match.group(1)
|
|
262
|
+
full_path = Path(project_dir) / error_file_path
|
|
263
|
+
if full_path.exists():
|
|
264
|
+
try:
|
|
265
|
+
code = full_path.read_text(encoding="utf-8")
|
|
266
|
+
logger.info(f"Read file for LLM fix: {error_file_path}")
|
|
267
|
+
except Exception as e:
|
|
268
|
+
logger.warning(f"Could not read {error_file_path}: {e}")
|
|
269
|
+
|
|
270
|
+
fixed_code = self.llm_fixer(error_text, code)
|
|
271
|
+
|
|
272
|
+
if fixed_code:
|
|
273
|
+
# If we read a file, write the fix back
|
|
274
|
+
if error_file_path and project_dir:
|
|
275
|
+
full_path = Path(project_dir) / error_file_path
|
|
276
|
+
try:
|
|
277
|
+
full_path.write_text(fixed_code, encoding="utf-8")
|
|
278
|
+
logger.info(f"Wrote LLM fix to: {error_file_path}")
|
|
279
|
+
except Exception as e:
|
|
280
|
+
logger.warning(f"Could not write fix to {error_file_path}: {e}")
|
|
281
|
+
|
|
282
|
+
return RecoveryAction.RETRY, fixed_code
|
|
283
|
+
return RecoveryAction.ABORT, "LLM could not fix the error"
|
|
284
|
+
|
|
285
|
+
def reset_retry_count(self, step_name: str) -> None:
|
|
286
|
+
"""Reset retry count for a step after success."""
|
|
287
|
+
keys_to_remove = [k for k in self.retry_counts if k.startswith(f"{step_name}:")]
|
|
288
|
+
for key in keys_to_remove:
|
|
289
|
+
del self.retry_counts[key]
|
|
290
|
+
|
|
291
|
+
def get_recovery_suggestion(self, error_text: str) -> str:
|
|
292
|
+
"""Get a human-readable recovery suggestion for an error.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
error_text: The error message
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Suggestion string for how to fix the error
|
|
299
|
+
"""
|
|
300
|
+
category, pattern = self.categorize_error(error_text)
|
|
301
|
+
|
|
302
|
+
suggestions = {
|
|
303
|
+
ErrorCategory.NETWORK: "Check network connection and retry",
|
|
304
|
+
ErrorCategory.RESOURCE: "Free up system resources (disk/memory) and retry",
|
|
305
|
+
ErrorCategory.DEPENDENCY: f"Install missing dependency: {pattern.fix_command or 'check error message'}",
|
|
306
|
+
ErrorCategory.COMPILATION: "Fix TypeScript/compilation errors in the code",
|
|
307
|
+
ErrorCategory.SYNTAX: "Fix syntax errors in the code",
|
|
308
|
+
ErrorCategory.RUNTIME: "Debug runtime error and fix logic",
|
|
309
|
+
ErrorCategory.VALIDATION: "Fix linting/validation warnings",
|
|
310
|
+
ErrorCategory.CONFIGURATION: "Check configuration files for errors",
|
|
311
|
+
ErrorCategory.UNKNOWN: "Review error message and investigate",
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return suggestions.get(category, "Unknown error - investigate")
|