jarviscore-framework 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.
- examples/calculator_agent_example.py +77 -0
- examples/multi_agent_workflow.py +132 -0
- examples/research_agent_example.py +76 -0
- jarviscore/__init__.py +54 -0
- jarviscore/cli/__init__.py +7 -0
- jarviscore/cli/__main__.py +33 -0
- jarviscore/cli/check.py +404 -0
- jarviscore/cli/smoketest.py +371 -0
- jarviscore/config/__init__.py +7 -0
- jarviscore/config/settings.py +128 -0
- jarviscore/core/__init__.py +7 -0
- jarviscore/core/agent.py +163 -0
- jarviscore/core/mesh.py +463 -0
- jarviscore/core/profile.py +64 -0
- jarviscore/docs/API_REFERENCE.md +932 -0
- jarviscore/docs/CONFIGURATION.md +753 -0
- jarviscore/docs/GETTING_STARTED.md +600 -0
- jarviscore/docs/TROUBLESHOOTING.md +424 -0
- jarviscore/docs/USER_GUIDE.md +983 -0
- jarviscore/execution/__init__.py +94 -0
- jarviscore/execution/code_registry.py +298 -0
- jarviscore/execution/generator.py +268 -0
- jarviscore/execution/llm.py +430 -0
- jarviscore/execution/repair.py +283 -0
- jarviscore/execution/result_handler.py +332 -0
- jarviscore/execution/sandbox.py +555 -0
- jarviscore/execution/search.py +281 -0
- jarviscore/orchestration/__init__.py +18 -0
- jarviscore/orchestration/claimer.py +101 -0
- jarviscore/orchestration/dependency.py +143 -0
- jarviscore/orchestration/engine.py +292 -0
- jarviscore/orchestration/status.py +96 -0
- jarviscore/p2p/__init__.py +23 -0
- jarviscore/p2p/broadcaster.py +353 -0
- jarviscore/p2p/coordinator.py +364 -0
- jarviscore/p2p/keepalive.py +361 -0
- jarviscore/p2p/swim_manager.py +290 -0
- jarviscore/profiles/__init__.py +6 -0
- jarviscore/profiles/autoagent.py +264 -0
- jarviscore/profiles/customagent.py +137 -0
- jarviscore_framework-0.1.0.dist-info/METADATA +136 -0
- jarviscore_framework-0.1.0.dist-info/RECORD +55 -0
- jarviscore_framework-0.1.0.dist-info/WHEEL +5 -0
- jarviscore_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
- jarviscore_framework-0.1.0.dist-info/top_level.txt +3 -0
- tests/conftest.py +44 -0
- tests/test_agent.py +165 -0
- tests/test_autoagent.py +140 -0
- tests/test_autoagent_day4.py +186 -0
- tests/test_customagent.py +248 -0
- tests/test_integration.py +293 -0
- tests/test_llm_fallback.py +185 -0
- tests/test_mesh.py +356 -0
- tests/test_p2p_integration.py +375 -0
- tests/test_remote_sandbox.py +116 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Autonomous Repair - LLM-based automatic code fixing
|
|
3
|
+
Analyzes errors and generates corrected code
|
|
4
|
+
"""
|
|
5
|
+
import ast
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Dict, Any, Optional
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AutonomousRepair:
|
|
13
|
+
"""
|
|
14
|
+
Automatic code repair using LLM.
|
|
15
|
+
|
|
16
|
+
Philosophy:
|
|
17
|
+
- Code execution fails → analyze error
|
|
18
|
+
- LLM generates fixed version
|
|
19
|
+
- Validate and retry (max 3 attempts)
|
|
20
|
+
- Learn from previous failures
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
repair = AutonomousRepair(code_generator)
|
|
24
|
+
fixed_code = await repair.repair(
|
|
25
|
+
original_code="result = 1/0",
|
|
26
|
+
error=ZeroDivisionError("division by zero"),
|
|
27
|
+
task={"task": "Calculate result"}
|
|
28
|
+
)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, code_generator, max_attempts: int = 3):
|
|
32
|
+
"""
|
|
33
|
+
Initialize repair system.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
code_generator: CodeGenerator instance
|
|
37
|
+
max_attempts: Maximum repair attempts (default 3)
|
|
38
|
+
"""
|
|
39
|
+
self.codegen = code_generator
|
|
40
|
+
self.max_attempts = max_attempts
|
|
41
|
+
|
|
42
|
+
self.repair_template = self._load_repair_template()
|
|
43
|
+
|
|
44
|
+
def _load_repair_template(self) -> str:
|
|
45
|
+
"""Load prompt template for code repair."""
|
|
46
|
+
return """You are an expert Python debugger. The following code FAILED during execution.
|
|
47
|
+
|
|
48
|
+
ORIGINAL CODE:
|
|
49
|
+
```python
|
|
50
|
+
{original_code}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
ERROR DETAILS:
|
|
54
|
+
Type: {error_type}
|
|
55
|
+
Message: {error_message}
|
|
56
|
+
|
|
57
|
+
ORIGINAL TASK:
|
|
58
|
+
{task}
|
|
59
|
+
|
|
60
|
+
{previous_attempts}
|
|
61
|
+
|
|
62
|
+
INSTRUCTIONS:
|
|
63
|
+
1. Analyze the error carefully
|
|
64
|
+
2. Identify the root cause (syntax error, logic error, missing import, etc.)
|
|
65
|
+
3. Generate FIXED Python code that:
|
|
66
|
+
- Solves the original task
|
|
67
|
+
- Handles the error properly
|
|
68
|
+
- Stores result in 'result' variable
|
|
69
|
+
- Is complete and executable
|
|
70
|
+
|
|
71
|
+
CRITICAL:
|
|
72
|
+
- Write ONLY executable Python code (no explanations)
|
|
73
|
+
- DO NOT repeat the same mistake
|
|
74
|
+
- Add proper error handling (try/except)
|
|
75
|
+
- Test edge cases in your mind
|
|
76
|
+
|
|
77
|
+
Generate the FIXED code now (code only):
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
async def repair(
|
|
81
|
+
self,
|
|
82
|
+
code: str,
|
|
83
|
+
error: Exception,
|
|
84
|
+
task: Dict[str, Any],
|
|
85
|
+
system_prompt: str,
|
|
86
|
+
attempt: int = 1,
|
|
87
|
+
previous_attempts: Optional[list] = None
|
|
88
|
+
) -> str:
|
|
89
|
+
"""
|
|
90
|
+
Repair failed code using LLM.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
code: Original code that failed
|
|
94
|
+
error: Exception that occurred
|
|
95
|
+
task: Original task specification
|
|
96
|
+
system_prompt: Agent's system prompt
|
|
97
|
+
attempt: Current repair attempt number (1-3)
|
|
98
|
+
previous_attempts: List of previous failed attempts
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Fixed Python code
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
RuntimeError: If max attempts exceeded
|
|
105
|
+
ValueError: If fixed code has syntax errors
|
|
106
|
+
|
|
107
|
+
Example:
|
|
108
|
+
try:
|
|
109
|
+
result = await executor.execute(code)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
fixed_code = await repair.repair(code, e, task, system_prompt)
|
|
112
|
+
result = await executor.execute(fixed_code)
|
|
113
|
+
"""
|
|
114
|
+
if attempt > self.max_attempts:
|
|
115
|
+
raise RuntimeError(
|
|
116
|
+
f"Repair failed after {self.max_attempts} attempts. "
|
|
117
|
+
f"Last error: {error}"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
logger.info(f"Attempting code repair (attempt {attempt}/{self.max_attempts})")
|
|
121
|
+
logger.debug(f"Error: {type(error).__name__}: {error}")
|
|
122
|
+
|
|
123
|
+
# Format previous attempts info
|
|
124
|
+
attempts_info = ""
|
|
125
|
+
if previous_attempts:
|
|
126
|
+
attempts_info = "PREVIOUS FAILED ATTEMPTS:\n"
|
|
127
|
+
for i, prev in enumerate(previous_attempts, 1):
|
|
128
|
+
attempts_info += f"\nAttempt {i}:\n"
|
|
129
|
+
attempts_info += f"Error: {prev['error']}\n"
|
|
130
|
+
attempts_info += f"Code:\n```python\n{prev['code']}\n```\n"
|
|
131
|
+
|
|
132
|
+
# Build repair prompt
|
|
133
|
+
prompt = self.repair_template.format(
|
|
134
|
+
original_code=code,
|
|
135
|
+
error_type=type(error).__name__,
|
|
136
|
+
error_message=str(error),
|
|
137
|
+
task=task.get('task', ''),
|
|
138
|
+
previous_attempts=attempts_info
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Add system context
|
|
142
|
+
prompt = f"{system_prompt}\n\n{prompt}"
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
# Generate fixed code via LLM
|
|
146
|
+
response = await self.codegen.llm.generate(
|
|
147
|
+
prompt=prompt,
|
|
148
|
+
temperature=0.4, # Slightly higher for creative problem solving
|
|
149
|
+
max_tokens=4000
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
fixed_code = response['content']
|
|
153
|
+
|
|
154
|
+
# Clean up code
|
|
155
|
+
fixed_code = self.codegen._clean_code(fixed_code)
|
|
156
|
+
|
|
157
|
+
# Validate syntax
|
|
158
|
+
self._validate_fix(fixed_code)
|
|
159
|
+
|
|
160
|
+
logger.info(f"Code repair attempt {attempt} successful")
|
|
161
|
+
return fixed_code
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.error(f"Repair attempt {attempt} failed: {e}")
|
|
165
|
+
raise RuntimeError(f"Failed to repair code: {e}")
|
|
166
|
+
|
|
167
|
+
def _validate_fix(self, code: str):
|
|
168
|
+
"""
|
|
169
|
+
Validate that fixed code has correct syntax.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
code: Fixed Python code
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
ValueError: If code has syntax errors
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
ast.parse(code)
|
|
179
|
+
logger.debug("Fixed code syntax validation passed")
|
|
180
|
+
except SyntaxError as e:
|
|
181
|
+
logger.error(f"Fixed code still has syntax errors: {e}")
|
|
182
|
+
raise ValueError(f"Repaired code has syntax errors: {e}")
|
|
183
|
+
|
|
184
|
+
async def repair_with_retries(
|
|
185
|
+
self,
|
|
186
|
+
code: str,
|
|
187
|
+
error: Exception,
|
|
188
|
+
task: Dict[str, Any],
|
|
189
|
+
system_prompt: str,
|
|
190
|
+
executor
|
|
191
|
+
) -> Dict[str, Any]:
|
|
192
|
+
"""
|
|
193
|
+
Repair code with automatic retry on failure.
|
|
194
|
+
|
|
195
|
+
This is a high-level method that:
|
|
196
|
+
1. Attempts repair
|
|
197
|
+
2. Executes fixed code
|
|
198
|
+
3. If still fails, tries again (up to max_attempts)
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
code: Original failed code
|
|
202
|
+
error: Exception from execution
|
|
203
|
+
task: Task specification
|
|
204
|
+
system_prompt: Agent system prompt
|
|
205
|
+
executor: SandboxExecutor instance for testing fixes
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Final execution result (success or failure)
|
|
209
|
+
|
|
210
|
+
Example:
|
|
211
|
+
result = await repair.repair_with_retries(
|
|
212
|
+
code, error, task, system_prompt, executor
|
|
213
|
+
)
|
|
214
|
+
"""
|
|
215
|
+
previous_attempts = []
|
|
216
|
+
current_code = code
|
|
217
|
+
current_error = error
|
|
218
|
+
|
|
219
|
+
for attempt in range(1, self.max_attempts + 1):
|
|
220
|
+
try:
|
|
221
|
+
# Track this attempt
|
|
222
|
+
previous_attempts.append({
|
|
223
|
+
'code': current_code,
|
|
224
|
+
'error': str(current_error)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
# Generate fix
|
|
228
|
+
fixed_code = await self.repair(
|
|
229
|
+
code=current_code,
|
|
230
|
+
error=current_error,
|
|
231
|
+
task=task,
|
|
232
|
+
system_prompt=system_prompt,
|
|
233
|
+
attempt=attempt,
|
|
234
|
+
previous_attempts=previous_attempts if attempt > 1 else None
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Test the fix
|
|
238
|
+
logger.info(f"Testing repair attempt {attempt}...")
|
|
239
|
+
result = await executor.execute(fixed_code)
|
|
240
|
+
|
|
241
|
+
if result['status'] == 'success':
|
|
242
|
+
logger.info(f"✓ Repair succeeded on attempt {attempt}")
|
|
243
|
+
return result
|
|
244
|
+
else:
|
|
245
|
+
# Fix didn't work, prepare for next attempt
|
|
246
|
+
current_code = fixed_code
|
|
247
|
+
current_error = Exception(result.get('error', 'Unknown error'))
|
|
248
|
+
logger.warning(
|
|
249
|
+
f"Repair attempt {attempt} executed but failed: "
|
|
250
|
+
f"{result.get('error')}"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
except Exception as e:
|
|
254
|
+
logger.error(f"Repair attempt {attempt} failed with exception: {e}")
|
|
255
|
+
current_error = e
|
|
256
|
+
if attempt == self.max_attempts:
|
|
257
|
+
# Last attempt failed
|
|
258
|
+
return {
|
|
259
|
+
'status': 'failure',
|
|
260
|
+
'error': f'All {self.max_attempts} repair attempts exhausted',
|
|
261
|
+
'last_error': str(e)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
# Should not reach here, but just in case
|
|
265
|
+
return {
|
|
266
|
+
'status': 'failure',
|
|
267
|
+
'error': f'Repair failed after {self.max_attempts} attempts',
|
|
268
|
+
'attempts': previous_attempts
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def create_autonomous_repair(code_generator, max_attempts: int = 3) -> AutonomousRepair:
|
|
273
|
+
"""
|
|
274
|
+
Factory function to create autonomous repair system.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
code_generator: CodeGenerator instance
|
|
278
|
+
max_attempts: Max repair attempts (default 3)
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
AutonomousRepair instance
|
|
282
|
+
"""
|
|
283
|
+
return AutonomousRepair(code_generator, max_attempts)
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Result Handler - Process and store function execution results
|
|
3
|
+
Storage: File system + In-memory (no external dependencies)
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
import datetime
|
|
10
|
+
from typing import Dict, Any, Optional
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ResultStatus(Enum):
|
|
18
|
+
"""Execution result status."""
|
|
19
|
+
SUCCESS = "success"
|
|
20
|
+
FAILURE = "failure"
|
|
21
|
+
SYNTAX_ERROR = "syntax_error"
|
|
22
|
+
RUNTIME_ERROR = "runtime_error"
|
|
23
|
+
TIMEOUT = "timeout"
|
|
24
|
+
PARTIAL = "partial"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ErrorCategory(Enum):
|
|
28
|
+
"""Error classification categories."""
|
|
29
|
+
SYNTAX = "syntax"
|
|
30
|
+
RUNTIME = "runtime"
|
|
31
|
+
NETWORK = "network"
|
|
32
|
+
TIMEOUT = "timeout"
|
|
33
|
+
RESOURCE = "resource"
|
|
34
|
+
UNKNOWN = "unknown"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ResultHandler:
|
|
38
|
+
"""
|
|
39
|
+
Handles processing, formatting, and storage of execution results.
|
|
40
|
+
|
|
41
|
+
Storage:
|
|
42
|
+
- File system: ./logs/{agent_id}/{result_id}.json
|
|
43
|
+
- In-memory: LRU cache for fast access
|
|
44
|
+
|
|
45
|
+
Features:
|
|
46
|
+
- Stores generated code with results
|
|
47
|
+
- Sanitizes sensitive parameters
|
|
48
|
+
- Classifies errors automatically
|
|
49
|
+
- Provides retrieval by result_id
|
|
50
|
+
|
|
51
|
+
Zero-config: No external dependencies (no Redis, no DB)
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, log_directory: str = "./logs", max_cache_size: int = 100):
|
|
55
|
+
"""
|
|
56
|
+
Initialize result handler.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
log_directory: Base directory for storing results
|
|
60
|
+
max_cache_size: Maximum number of results to cache in memory
|
|
61
|
+
"""
|
|
62
|
+
self.log_directory = Path(log_directory)
|
|
63
|
+
self.log_directory.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
|
|
65
|
+
# In-memory cache (LRU)
|
|
66
|
+
self._cache: Dict[str, Dict[str, Any]] = {}
|
|
67
|
+
self._cache_order = [] # For LRU eviction
|
|
68
|
+
self.max_cache_size = max_cache_size
|
|
69
|
+
|
|
70
|
+
logger.info(f"ResultHandler initialized with directory: {self.log_directory}")
|
|
71
|
+
|
|
72
|
+
def process_result(
|
|
73
|
+
self,
|
|
74
|
+
agent_id: str,
|
|
75
|
+
task: str,
|
|
76
|
+
code: str,
|
|
77
|
+
output: Any,
|
|
78
|
+
status: str,
|
|
79
|
+
error: Optional[str] = None,
|
|
80
|
+
execution_time: Optional[float] = None,
|
|
81
|
+
tokens: Optional[Dict] = None,
|
|
82
|
+
cost_usd: Optional[float] = None,
|
|
83
|
+
repairs: int = 0,
|
|
84
|
+
metadata: Optional[Dict] = None
|
|
85
|
+
) -> Dict[str, Any]:
|
|
86
|
+
"""
|
|
87
|
+
Process and store execution result.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
agent_id: ID of the agent that executed
|
|
91
|
+
task: Original task description
|
|
92
|
+
code: Generated code that was executed
|
|
93
|
+
output: Execution output (result variable)
|
|
94
|
+
status: "success" or "failure"
|
|
95
|
+
error: Error message if failed
|
|
96
|
+
execution_time: Time taken in seconds
|
|
97
|
+
tokens: Token usage {input, output, total}
|
|
98
|
+
cost_usd: Estimated cost
|
|
99
|
+
repairs: Number of repair attempts
|
|
100
|
+
metadata: Additional metadata
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Processed result dictionary with storage metadata
|
|
104
|
+
"""
|
|
105
|
+
timestamp = datetime.datetime.now().isoformat()
|
|
106
|
+
result_id = self._generate_result_id(agent_id, timestamp)
|
|
107
|
+
|
|
108
|
+
# Classify error if present
|
|
109
|
+
error_category = None
|
|
110
|
+
if error:
|
|
111
|
+
error_category = self._classify_error(error)
|
|
112
|
+
|
|
113
|
+
# Determine detailed status
|
|
114
|
+
result_status = self._determine_status(status, error, error_category)
|
|
115
|
+
|
|
116
|
+
# Build result object
|
|
117
|
+
result_data = {
|
|
118
|
+
# Identity
|
|
119
|
+
"result_id": result_id,
|
|
120
|
+
"agent_id": agent_id,
|
|
121
|
+
"timestamp": timestamp,
|
|
122
|
+
|
|
123
|
+
# Execution details
|
|
124
|
+
"task": task,
|
|
125
|
+
"code": code,
|
|
126
|
+
"output": output,
|
|
127
|
+
|
|
128
|
+
# Status
|
|
129
|
+
"status": result_status.value,
|
|
130
|
+
"success": result_status == ResultStatus.SUCCESS,
|
|
131
|
+
|
|
132
|
+
# Error details
|
|
133
|
+
"error": error,
|
|
134
|
+
"error_category": error_category.value if error_category else None,
|
|
135
|
+
|
|
136
|
+
# Metrics
|
|
137
|
+
"execution_time": execution_time,
|
|
138
|
+
"tokens": tokens or {},
|
|
139
|
+
"cost_usd": cost_usd or 0.0,
|
|
140
|
+
"repairs": repairs,
|
|
141
|
+
|
|
142
|
+
# Metadata
|
|
143
|
+
"metadata": metadata or {}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# Store to file system
|
|
147
|
+
self._save_to_file(agent_id, result_id, result_data)
|
|
148
|
+
|
|
149
|
+
# Store in memory cache
|
|
150
|
+
self._cache_result(result_id, result_data)
|
|
151
|
+
|
|
152
|
+
# Log summary
|
|
153
|
+
if result_status == ResultStatus.SUCCESS:
|
|
154
|
+
time_str = f"{execution_time:.2f}s" if execution_time else "N/A"
|
|
155
|
+
logger.info(
|
|
156
|
+
f"Result {result_id}: SUCCESS in {time_str} "
|
|
157
|
+
f"(repairs: {repairs}, cost: ${cost_usd:.4f})"
|
|
158
|
+
)
|
|
159
|
+
else:
|
|
160
|
+
logger.error(
|
|
161
|
+
f"Result {result_id}: {result_status.value} - {error}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return result_data
|
|
165
|
+
|
|
166
|
+
def get_result(self, result_id: str) -> Optional[Dict[str, Any]]:
|
|
167
|
+
"""
|
|
168
|
+
Retrieve result by ID (checks cache first, then file system).
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
result_id: Result identifier
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Result dictionary or None if not found
|
|
175
|
+
"""
|
|
176
|
+
# Check in-memory cache first
|
|
177
|
+
if result_id in self._cache:
|
|
178
|
+
logger.debug(f"Result {result_id} retrieved from cache")
|
|
179
|
+
return self._cache[result_id]
|
|
180
|
+
|
|
181
|
+
# Search file system
|
|
182
|
+
result_data = self._load_from_file(result_id)
|
|
183
|
+
if result_data:
|
|
184
|
+
# Cache for future access
|
|
185
|
+
self._cache_result(result_id, result_data)
|
|
186
|
+
logger.debug(f"Result {result_id} loaded from file")
|
|
187
|
+
return result_data
|
|
188
|
+
|
|
189
|
+
logger.warning(f"Result {result_id} not found")
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
def get_agent_results(self, agent_id: str, limit: int = 10) -> list:
|
|
193
|
+
"""
|
|
194
|
+
Get recent results for a specific agent.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
agent_id: Agent identifier
|
|
198
|
+
limit: Maximum number of results to return
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
List of result dictionaries, most recent first
|
|
202
|
+
"""
|
|
203
|
+
agent_dir = self.log_directory / agent_id
|
|
204
|
+
if not agent_dir.exists():
|
|
205
|
+
return []
|
|
206
|
+
|
|
207
|
+
# Get all result files for this agent
|
|
208
|
+
result_files = sorted(
|
|
209
|
+
agent_dir.glob("*.json"),
|
|
210
|
+
key=lambda p: p.stat().st_mtime,
|
|
211
|
+
reverse=True
|
|
212
|
+
)[:limit]
|
|
213
|
+
|
|
214
|
+
results = []
|
|
215
|
+
for result_file in result_files:
|
|
216
|
+
try:
|
|
217
|
+
with open(result_file, 'r') as f:
|
|
218
|
+
results.append(json.load(f))
|
|
219
|
+
except Exception as e:
|
|
220
|
+
logger.warning(f"Failed to load {result_file}: {e}")
|
|
221
|
+
|
|
222
|
+
return results
|
|
223
|
+
|
|
224
|
+
def _generate_result_id(self, agent_id: str, timestamp: str) -> str:
|
|
225
|
+
"""Generate unique result ID."""
|
|
226
|
+
clean_timestamp = timestamp.replace(':', '-').replace('.', '_')
|
|
227
|
+
return f"{agent_id}_{clean_timestamp}"
|
|
228
|
+
|
|
229
|
+
def _determine_status(
|
|
230
|
+
self,
|
|
231
|
+
status: str,
|
|
232
|
+
error: Optional[str],
|
|
233
|
+
error_category: Optional[ErrorCategory]
|
|
234
|
+
) -> ResultStatus:
|
|
235
|
+
"""Determine detailed result status."""
|
|
236
|
+
if status == "success":
|
|
237
|
+
return ResultStatus.SUCCESS
|
|
238
|
+
|
|
239
|
+
if error_category == ErrorCategory.SYNTAX:
|
|
240
|
+
return ResultStatus.SYNTAX_ERROR
|
|
241
|
+
elif error_category == ErrorCategory.TIMEOUT:
|
|
242
|
+
return ResultStatus.TIMEOUT
|
|
243
|
+
elif error and "error" in error.lower():
|
|
244
|
+
return ResultStatus.RUNTIME_ERROR
|
|
245
|
+
|
|
246
|
+
return ResultStatus.FAILURE
|
|
247
|
+
|
|
248
|
+
def _classify_error(self, error: str) -> ErrorCategory:
|
|
249
|
+
"""Classify error into category."""
|
|
250
|
+
error_lower = error.lower()
|
|
251
|
+
|
|
252
|
+
# Syntax errors
|
|
253
|
+
syntax_keywords = ['syntaxerror', 'indentationerror', 'taberror', 'invalid syntax']
|
|
254
|
+
if any(kw in error_lower for kw in syntax_keywords):
|
|
255
|
+
return ErrorCategory.SYNTAX
|
|
256
|
+
|
|
257
|
+
# Timeout errors
|
|
258
|
+
timeout_keywords = ['timeout', 'timed out', 'time limit exceeded']
|
|
259
|
+
if any(kw in error_lower for kw in timeout_keywords):
|
|
260
|
+
return ErrorCategory.TIMEOUT
|
|
261
|
+
|
|
262
|
+
# Network errors
|
|
263
|
+
network_keywords = ['connection', 'network', 'httpx', 'aiohttp', 'socket']
|
|
264
|
+
if any(kw in error_lower for kw in network_keywords):
|
|
265
|
+
return ErrorCategory.NETWORK
|
|
266
|
+
|
|
267
|
+
# Resource errors
|
|
268
|
+
resource_keywords = ['memory', 'resource', 'quota', 'limit exceeded']
|
|
269
|
+
if any(kw in error_lower for kw in resource_keywords):
|
|
270
|
+
return ErrorCategory.RESOURCE
|
|
271
|
+
|
|
272
|
+
# Runtime errors (default for unknown errors)
|
|
273
|
+
return ErrorCategory.RUNTIME
|
|
274
|
+
|
|
275
|
+
def _save_to_file(self, agent_id: str, result_id: str, result_data: Dict):
|
|
276
|
+
"""Save result to file system."""
|
|
277
|
+
agent_dir = self.log_directory / agent_id
|
|
278
|
+
agent_dir.mkdir(parents=True, exist_ok=True)
|
|
279
|
+
|
|
280
|
+
result_file = agent_dir / f"{result_id}.json"
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
with open(result_file, 'w') as f:
|
|
284
|
+
json.dump(result_data, f, indent=2, default=str)
|
|
285
|
+
logger.debug(f"Saved result to {result_file}")
|
|
286
|
+
except Exception as e:
|
|
287
|
+
logger.error(f"Failed to save result to file: {e}")
|
|
288
|
+
|
|
289
|
+
def _load_from_file(self, result_id: str) -> Optional[Dict]:
|
|
290
|
+
"""Load result from file system by searching all agent directories."""
|
|
291
|
+
for agent_dir in self.log_directory.iterdir():
|
|
292
|
+
if not agent_dir.is_dir():
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
result_file = agent_dir / f"{result_id}.json"
|
|
296
|
+
if result_file.exists():
|
|
297
|
+
try:
|
|
298
|
+
with open(result_file, 'r') as f:
|
|
299
|
+
return json.load(f)
|
|
300
|
+
except Exception as e:
|
|
301
|
+
logger.warning(f"Failed to load {result_file}: {e}")
|
|
302
|
+
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
def _cache_result(self, result_id: str, result_data: Dict):
|
|
306
|
+
"""Cache result in memory with LRU eviction."""
|
|
307
|
+
# Add to cache
|
|
308
|
+
self._cache[result_id] = result_data
|
|
309
|
+
|
|
310
|
+
# Track access order
|
|
311
|
+
if result_id in self._cache_order:
|
|
312
|
+
self._cache_order.remove(result_id)
|
|
313
|
+
self._cache_order.append(result_id)
|
|
314
|
+
|
|
315
|
+
# Evict oldest if cache is full
|
|
316
|
+
while len(self._cache) > self.max_cache_size:
|
|
317
|
+
oldest_id = self._cache_order.pop(0)
|
|
318
|
+
del self._cache[oldest_id]
|
|
319
|
+
logger.debug(f"Evicted {oldest_id} from cache (LRU)")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def create_result_handler(log_directory: str = "./logs") -> ResultHandler:
|
|
323
|
+
"""
|
|
324
|
+
Factory function to create result handler.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
log_directory: Base directory for result storage
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
ResultHandler instance
|
|
331
|
+
"""
|
|
332
|
+
return ResultHandler(log_directory)
|