codeframe-ai 0.9.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.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
"""Diagnostics and run logging for CodeFRAME v2.
|
|
2
|
+
|
|
3
|
+
This module provides:
|
|
4
|
+
- RunLogger: Structured logging for agent runs
|
|
5
|
+
- DiagnosticReport: Analysis results for failed runs
|
|
6
|
+
- RemediationAction: Suggested actions to fix issues
|
|
7
|
+
|
|
8
|
+
This module is headless - no FastAPI or HTTP dependencies.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import uuid
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Any, Optional
|
|
17
|
+
|
|
18
|
+
from codeframe.core.workspace import Workspace, get_db_connection
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _utc_now() -> datetime:
|
|
22
|
+
"""Get current UTC time as timezone-aware datetime."""
|
|
23
|
+
return datetime.now(timezone.utc)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# Enums
|
|
28
|
+
# =============================================================================
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LogLevel(str, Enum):
|
|
32
|
+
"""Log level for run log entries."""
|
|
33
|
+
|
|
34
|
+
DEBUG = "DEBUG"
|
|
35
|
+
INFO = "INFO"
|
|
36
|
+
WARNING = "WARNING"
|
|
37
|
+
ERROR = "ERROR"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class LogCategory(str, Enum):
|
|
41
|
+
"""Category of log entry."""
|
|
42
|
+
|
|
43
|
+
AGENT_ACTION = "agent_action"
|
|
44
|
+
LLM_CALL = "llm_call"
|
|
45
|
+
ERROR = "error"
|
|
46
|
+
STATE_CHANGE = "state_change"
|
|
47
|
+
VERIFICATION = "verification"
|
|
48
|
+
BLOCKER = "blocker"
|
|
49
|
+
FILE_OPERATION = "file_operation"
|
|
50
|
+
SHELL_COMMAND = "shell_command"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class RemediationAction(str, Enum):
|
|
54
|
+
"""Types of remediation actions that can be recommended."""
|
|
55
|
+
|
|
56
|
+
UPDATE_TASK_DESCRIPTION = "update_task_description"
|
|
57
|
+
ANSWER_BLOCKER = "answer_blocker"
|
|
58
|
+
CHANGE_MODEL = "change_model"
|
|
59
|
+
RESOLVE_DEPENDENCY = "resolve_dependency"
|
|
60
|
+
FIX_ENVIRONMENT = "fix_environment"
|
|
61
|
+
RETRY_WITH_CONTEXT = "retry_with_context"
|
|
62
|
+
SPLIT_TASK = "split_task"
|
|
63
|
+
ADD_TEST_DATA = "add_test_data"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class FailureCategory(str, Enum):
|
|
67
|
+
"""Categories of failure for diagnostic analysis."""
|
|
68
|
+
|
|
69
|
+
TASK_DESCRIPTION = "task_description"
|
|
70
|
+
BLOCKER_UNRESOLVED = "blocker_unresolved"
|
|
71
|
+
MODEL_LIMITATION = "model_limitation"
|
|
72
|
+
CODE_QUALITY = "code_quality"
|
|
73
|
+
DEPENDENCY_ISSUE = "dependency_issue"
|
|
74
|
+
ENVIRONMENT_ISSUE = "environment_issue"
|
|
75
|
+
TECHNICAL_ERROR = "technical_error"
|
|
76
|
+
UNKNOWN = "unknown"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Severity(str, Enum):
|
|
80
|
+
"""Severity level for diagnostic reports."""
|
|
81
|
+
|
|
82
|
+
CRITICAL = "critical"
|
|
83
|
+
HIGH = "high"
|
|
84
|
+
MEDIUM = "medium"
|
|
85
|
+
LOW = "low"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# =============================================================================
|
|
89
|
+
# Data Classes
|
|
90
|
+
# =============================================================================
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class RunLogEntry:
|
|
95
|
+
"""A single log entry for a run.
|
|
96
|
+
|
|
97
|
+
Attributes:
|
|
98
|
+
id: Unique entry identifier (auto-assigned from DB)
|
|
99
|
+
run_id: Run this entry belongs to
|
|
100
|
+
task_id: Task being executed
|
|
101
|
+
timestamp: When the entry was created
|
|
102
|
+
log_level: Severity level
|
|
103
|
+
category: Type of log entry
|
|
104
|
+
message: Human-readable message
|
|
105
|
+
metadata: Additional structured data (JSON serializable)
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
run_id: str
|
|
109
|
+
task_id: str
|
|
110
|
+
timestamp: datetime
|
|
111
|
+
log_level: LogLevel
|
|
112
|
+
category: LogCategory
|
|
113
|
+
message: str
|
|
114
|
+
metadata: Optional[dict[str, Any]] = None
|
|
115
|
+
id: Optional[int] = None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class DiagnosticRecommendation:
|
|
120
|
+
"""A single remediation recommendation.
|
|
121
|
+
|
|
122
|
+
Attributes:
|
|
123
|
+
action: Type of remediation action
|
|
124
|
+
reason: Why this action is recommended
|
|
125
|
+
command: CLI command to execute this action
|
|
126
|
+
parameters: Parameters for the action
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
action: RemediationAction
|
|
130
|
+
reason: str
|
|
131
|
+
command: str
|
|
132
|
+
parameters: dict[str, Any] = field(default_factory=dict)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class DiagnosticReport:
|
|
137
|
+
"""Analysis of a failed run with recommendations.
|
|
138
|
+
|
|
139
|
+
Attributes:
|
|
140
|
+
id: Unique report identifier (UUID)
|
|
141
|
+
task_id: Task that failed
|
|
142
|
+
run_id: Run that failed
|
|
143
|
+
root_cause: Description of the root cause
|
|
144
|
+
failure_category: Category of failure
|
|
145
|
+
severity: How severe the issue is
|
|
146
|
+
recommendations: List of recommended actions
|
|
147
|
+
log_summary: Summary of relevant log entries
|
|
148
|
+
created_at: When the report was created
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
task_id: str
|
|
152
|
+
run_id: str
|
|
153
|
+
root_cause: str
|
|
154
|
+
failure_category: FailureCategory
|
|
155
|
+
severity: Severity
|
|
156
|
+
recommendations: list[DiagnosticRecommendation]
|
|
157
|
+
log_summary: str
|
|
158
|
+
created_at: datetime
|
|
159
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# =============================================================================
|
|
163
|
+
# RunLogger Class
|
|
164
|
+
# =============================================================================
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class RunLogger:
|
|
168
|
+
"""Structured logging for agent runs.
|
|
169
|
+
|
|
170
|
+
Captures detailed information about agent execution for
|
|
171
|
+
later analysis by the diagnostic system.
|
|
172
|
+
|
|
173
|
+
Usage:
|
|
174
|
+
logger = RunLogger(workspace, run_id, task_id)
|
|
175
|
+
logger.info(LogCategory.AGENT_ACTION, "Starting planning phase")
|
|
176
|
+
logger.error(LogCategory.ERROR, "Failed to create file", {"path": "main.py"})
|
|
177
|
+
|
|
178
|
+
# Retrieve logs
|
|
179
|
+
logs = get_run_logs(workspace, run_id)
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def __init__(self, workspace: Workspace, run_id: str, task_id: str):
|
|
183
|
+
"""Initialize the run logger.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
workspace: Target workspace
|
|
187
|
+
run_id: ID of the run being logged
|
|
188
|
+
task_id: ID of the task being executed
|
|
189
|
+
"""
|
|
190
|
+
self.workspace = workspace
|
|
191
|
+
self.run_id = run_id
|
|
192
|
+
self.task_id = task_id
|
|
193
|
+
self._buffer: list[RunLogEntry] = []
|
|
194
|
+
self._auto_flush = True
|
|
195
|
+
|
|
196
|
+
def log(
|
|
197
|
+
self,
|
|
198
|
+
level: LogLevel,
|
|
199
|
+
category: LogCategory,
|
|
200
|
+
message: str,
|
|
201
|
+
metadata: Optional[dict[str, Any]] = None,
|
|
202
|
+
) -> RunLogEntry:
|
|
203
|
+
"""Log an entry.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
level: Log level
|
|
207
|
+
category: Entry category
|
|
208
|
+
message: Human-readable message
|
|
209
|
+
metadata: Additional structured data
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
The created log entry
|
|
213
|
+
"""
|
|
214
|
+
entry = RunLogEntry(
|
|
215
|
+
run_id=self.run_id,
|
|
216
|
+
task_id=self.task_id,
|
|
217
|
+
timestamp=_utc_now(),
|
|
218
|
+
log_level=level,
|
|
219
|
+
category=category,
|
|
220
|
+
message=message,
|
|
221
|
+
metadata=metadata,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
self._buffer.append(entry)
|
|
225
|
+
|
|
226
|
+
if self._auto_flush:
|
|
227
|
+
self.flush()
|
|
228
|
+
|
|
229
|
+
return entry
|
|
230
|
+
|
|
231
|
+
def debug(
|
|
232
|
+
self,
|
|
233
|
+
category: LogCategory,
|
|
234
|
+
message: str,
|
|
235
|
+
metadata: Optional[dict[str, Any]] = None,
|
|
236
|
+
) -> RunLogEntry:
|
|
237
|
+
"""Log a DEBUG entry."""
|
|
238
|
+
return self.log(LogLevel.DEBUG, category, message, metadata)
|
|
239
|
+
|
|
240
|
+
def info(
|
|
241
|
+
self,
|
|
242
|
+
category: LogCategory,
|
|
243
|
+
message: str,
|
|
244
|
+
metadata: Optional[dict[str, Any]] = None,
|
|
245
|
+
) -> RunLogEntry:
|
|
246
|
+
"""Log an INFO entry."""
|
|
247
|
+
return self.log(LogLevel.INFO, category, message, metadata)
|
|
248
|
+
|
|
249
|
+
def warning(
|
|
250
|
+
self,
|
|
251
|
+
category: LogCategory,
|
|
252
|
+
message: str,
|
|
253
|
+
metadata: Optional[dict[str, Any]] = None,
|
|
254
|
+
) -> RunLogEntry:
|
|
255
|
+
"""Log a WARNING entry."""
|
|
256
|
+
return self.log(LogLevel.WARNING, category, message, metadata)
|
|
257
|
+
|
|
258
|
+
def error(
|
|
259
|
+
self,
|
|
260
|
+
category: LogCategory,
|
|
261
|
+
message: str,
|
|
262
|
+
metadata: Optional[dict[str, Any]] = None,
|
|
263
|
+
) -> RunLogEntry:
|
|
264
|
+
"""Log an ERROR entry."""
|
|
265
|
+
return self.log(LogLevel.ERROR, category, message, metadata)
|
|
266
|
+
|
|
267
|
+
def flush(self) -> None:
|
|
268
|
+
"""Flush buffered entries to the database."""
|
|
269
|
+
if not self._buffer:
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
conn = get_db_connection(self.workspace)
|
|
273
|
+
try:
|
|
274
|
+
cursor = conn.cursor()
|
|
275
|
+
|
|
276
|
+
for entry in self._buffer:
|
|
277
|
+
cursor.execute(
|
|
278
|
+
"""
|
|
279
|
+
INSERT INTO run_logs (run_id, task_id, timestamp, log_level, category, message, metadata)
|
|
280
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
281
|
+
""",
|
|
282
|
+
(
|
|
283
|
+
entry.run_id,
|
|
284
|
+
entry.task_id,
|
|
285
|
+
entry.timestamp.isoformat(),
|
|
286
|
+
entry.log_level.value,
|
|
287
|
+
entry.category.value,
|
|
288
|
+
entry.message,
|
|
289
|
+
json.dumps(entry.metadata) if entry.metadata else None,
|
|
290
|
+
),
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
conn.commit()
|
|
294
|
+
self._buffer.clear()
|
|
295
|
+
finally:
|
|
296
|
+
conn.close()
|
|
297
|
+
|
|
298
|
+
def set_auto_flush(self, enabled: bool) -> None:
|
|
299
|
+
"""Enable or disable auto-flushing after each log entry.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
enabled: Whether to auto-flush
|
|
303
|
+
"""
|
|
304
|
+
self._auto_flush = enabled
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# =============================================================================
|
|
308
|
+
# Run Log Functions
|
|
309
|
+
# =============================================================================
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def get_run_logs(
|
|
313
|
+
workspace: Workspace,
|
|
314
|
+
run_id: str,
|
|
315
|
+
level: Optional[LogLevel] = None,
|
|
316
|
+
category: Optional[LogCategory] = None,
|
|
317
|
+
limit: int = 1000,
|
|
318
|
+
) -> list[RunLogEntry]:
|
|
319
|
+
"""Get log entries for a run.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
workspace: Target workspace
|
|
323
|
+
run_id: Run to get logs for
|
|
324
|
+
level: Optional level filter
|
|
325
|
+
category: Optional category filter
|
|
326
|
+
limit: Maximum entries to return
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
List of log entries, oldest first
|
|
330
|
+
"""
|
|
331
|
+
conn = get_db_connection(workspace)
|
|
332
|
+
try:
|
|
333
|
+
cursor = conn.cursor()
|
|
334
|
+
|
|
335
|
+
query = """
|
|
336
|
+
SELECT id, run_id, task_id, timestamp, log_level, category, message, metadata
|
|
337
|
+
FROM run_logs
|
|
338
|
+
WHERE run_id = ?
|
|
339
|
+
"""
|
|
340
|
+
params: list = [run_id]
|
|
341
|
+
|
|
342
|
+
if level:
|
|
343
|
+
query += " AND log_level = ?"
|
|
344
|
+
params.append(level.value)
|
|
345
|
+
|
|
346
|
+
if category:
|
|
347
|
+
query += " AND category = ?"
|
|
348
|
+
params.append(category.value)
|
|
349
|
+
|
|
350
|
+
query += " ORDER BY timestamp ASC LIMIT ?"
|
|
351
|
+
params.append(limit)
|
|
352
|
+
|
|
353
|
+
cursor.execute(query, params)
|
|
354
|
+
rows = cursor.fetchall()
|
|
355
|
+
|
|
356
|
+
return [_row_to_log_entry(row) for row in rows]
|
|
357
|
+
finally:
|
|
358
|
+
conn.close()
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def get_run_errors(workspace: Workspace, run_id: str, limit: int = 100) -> list[RunLogEntry]:
|
|
362
|
+
"""Get error log entries for a run.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
workspace: Target workspace
|
|
366
|
+
run_id: Run to get errors for
|
|
367
|
+
limit: Maximum entries to return
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
List of error entries, oldest first
|
|
371
|
+
"""
|
|
372
|
+
return get_run_logs(workspace, run_id, level=LogLevel.ERROR, limit=limit)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def count_logs_by_level(workspace: Workspace, run_id: str) -> dict[str, int]:
|
|
376
|
+
"""Count log entries by level for a run.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
workspace: Target workspace
|
|
380
|
+
run_id: Run to count logs for
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Dict mapping level string to count
|
|
384
|
+
"""
|
|
385
|
+
conn = get_db_connection(workspace)
|
|
386
|
+
try:
|
|
387
|
+
cursor = conn.cursor()
|
|
388
|
+
cursor.execute(
|
|
389
|
+
"""
|
|
390
|
+
SELECT log_level, COUNT(*) as count
|
|
391
|
+
FROM run_logs
|
|
392
|
+
WHERE run_id = ?
|
|
393
|
+
GROUP BY log_level
|
|
394
|
+
""",
|
|
395
|
+
(run_id,),
|
|
396
|
+
)
|
|
397
|
+
rows = cursor.fetchall()
|
|
398
|
+
return {row[0]: row[1] for row in rows}
|
|
399
|
+
finally:
|
|
400
|
+
conn.close()
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _row_to_log_entry(row: tuple) -> RunLogEntry:
|
|
404
|
+
"""Convert a database row to a RunLogEntry."""
|
|
405
|
+
return RunLogEntry(
|
|
406
|
+
id=row[0],
|
|
407
|
+
run_id=row[1],
|
|
408
|
+
task_id=row[2],
|
|
409
|
+
timestamp=datetime.fromisoformat(row[3]),
|
|
410
|
+
log_level=LogLevel(row[4]),
|
|
411
|
+
category=LogCategory(row[5]),
|
|
412
|
+
message=row[6],
|
|
413
|
+
metadata=json.loads(row[7]) if row[7] else None,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
# =============================================================================
|
|
418
|
+
# Diagnostic Report Functions
|
|
419
|
+
# =============================================================================
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def save_diagnostic_report(workspace: Workspace, report: DiagnosticReport) -> DiagnosticReport:
|
|
423
|
+
"""Save a diagnostic report to the database.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
workspace: Target workspace
|
|
427
|
+
report: Report to save
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
The saved report (with ID if newly created)
|
|
431
|
+
"""
|
|
432
|
+
conn = get_db_connection(workspace)
|
|
433
|
+
try:
|
|
434
|
+
cursor = conn.cursor()
|
|
435
|
+
|
|
436
|
+
# Serialize recommendations to JSON
|
|
437
|
+
recommendations_json = json.dumps(
|
|
438
|
+
[
|
|
439
|
+
{
|
|
440
|
+
"action": r.action.value,
|
|
441
|
+
"reason": r.reason,
|
|
442
|
+
"command": r.command,
|
|
443
|
+
"parameters": r.parameters,
|
|
444
|
+
}
|
|
445
|
+
for r in report.recommendations
|
|
446
|
+
]
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
cursor.execute(
|
|
450
|
+
"""
|
|
451
|
+
INSERT OR REPLACE INTO diagnostic_reports
|
|
452
|
+
(id, task_id, run_id, root_cause, failure_category, severity, recommendations, log_summary, created_at)
|
|
453
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
454
|
+
""",
|
|
455
|
+
(
|
|
456
|
+
report.id,
|
|
457
|
+
report.task_id,
|
|
458
|
+
report.run_id,
|
|
459
|
+
report.root_cause,
|
|
460
|
+
report.failure_category.value,
|
|
461
|
+
report.severity.value,
|
|
462
|
+
recommendations_json,
|
|
463
|
+
report.log_summary,
|
|
464
|
+
report.created_at.isoformat(),
|
|
465
|
+
),
|
|
466
|
+
)
|
|
467
|
+
conn.commit()
|
|
468
|
+
|
|
469
|
+
return report
|
|
470
|
+
finally:
|
|
471
|
+
conn.close()
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def get_diagnostic_report(workspace: Workspace, report_id: str) -> Optional[DiagnosticReport]:
|
|
475
|
+
"""Get a diagnostic report by ID.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
workspace: Target workspace
|
|
479
|
+
report_id: Report identifier
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
DiagnosticReport if found, None otherwise
|
|
483
|
+
"""
|
|
484
|
+
conn = get_db_connection(workspace)
|
|
485
|
+
try:
|
|
486
|
+
cursor = conn.cursor()
|
|
487
|
+
cursor.execute(
|
|
488
|
+
"""
|
|
489
|
+
SELECT id, task_id, run_id, root_cause, failure_category, severity, recommendations, log_summary, created_at
|
|
490
|
+
FROM diagnostic_reports
|
|
491
|
+
WHERE id = ?
|
|
492
|
+
""",
|
|
493
|
+
(report_id,),
|
|
494
|
+
)
|
|
495
|
+
row = cursor.fetchone()
|
|
496
|
+
|
|
497
|
+
if not row:
|
|
498
|
+
return None
|
|
499
|
+
|
|
500
|
+
return _row_to_diagnostic_report(row)
|
|
501
|
+
finally:
|
|
502
|
+
conn.close()
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def get_latest_diagnostic_report(
|
|
506
|
+
workspace: Workspace,
|
|
507
|
+
task_id: Optional[str] = None,
|
|
508
|
+
run_id: Optional[str] = None,
|
|
509
|
+
) -> Optional[DiagnosticReport]:
|
|
510
|
+
"""Get the most recent diagnostic report.
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
workspace: Target workspace
|
|
514
|
+
task_id: Optional task filter
|
|
515
|
+
run_id: Optional run filter
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
Most recent DiagnosticReport matching filters, or None
|
|
519
|
+
"""
|
|
520
|
+
conn = get_db_connection(workspace)
|
|
521
|
+
try:
|
|
522
|
+
cursor = conn.cursor()
|
|
523
|
+
|
|
524
|
+
query = """
|
|
525
|
+
SELECT id, task_id, run_id, root_cause, failure_category, severity, recommendations, log_summary, created_at
|
|
526
|
+
FROM diagnostic_reports
|
|
527
|
+
WHERE 1=1
|
|
528
|
+
"""
|
|
529
|
+
params: list = []
|
|
530
|
+
|
|
531
|
+
if task_id:
|
|
532
|
+
query += " AND task_id = ?"
|
|
533
|
+
params.append(task_id)
|
|
534
|
+
|
|
535
|
+
if run_id:
|
|
536
|
+
query += " AND run_id = ?"
|
|
537
|
+
params.append(run_id)
|
|
538
|
+
|
|
539
|
+
query += " ORDER BY created_at DESC LIMIT 1"
|
|
540
|
+
|
|
541
|
+
cursor.execute(query, params)
|
|
542
|
+
row = cursor.fetchone()
|
|
543
|
+
|
|
544
|
+
if not row:
|
|
545
|
+
return None
|
|
546
|
+
|
|
547
|
+
return _row_to_diagnostic_report(row)
|
|
548
|
+
finally:
|
|
549
|
+
conn.close()
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def list_diagnostic_reports(
|
|
553
|
+
workspace: Workspace,
|
|
554
|
+
task_id: Optional[str] = None,
|
|
555
|
+
limit: int = 20,
|
|
556
|
+
) -> list[DiagnosticReport]:
|
|
557
|
+
"""List diagnostic reports.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
workspace: Target workspace
|
|
561
|
+
task_id: Optional task filter
|
|
562
|
+
limit: Maximum reports to return
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
List of reports, newest first
|
|
566
|
+
"""
|
|
567
|
+
conn = get_db_connection(workspace)
|
|
568
|
+
try:
|
|
569
|
+
cursor = conn.cursor()
|
|
570
|
+
|
|
571
|
+
query = """
|
|
572
|
+
SELECT id, task_id, run_id, root_cause, failure_category, severity, recommendations, log_summary, created_at
|
|
573
|
+
FROM diagnostic_reports
|
|
574
|
+
WHERE 1=1
|
|
575
|
+
"""
|
|
576
|
+
params: list = []
|
|
577
|
+
|
|
578
|
+
if task_id:
|
|
579
|
+
query += " AND task_id = ?"
|
|
580
|
+
params.append(task_id)
|
|
581
|
+
|
|
582
|
+
query += " ORDER BY created_at DESC LIMIT ?"
|
|
583
|
+
params.append(limit)
|
|
584
|
+
|
|
585
|
+
cursor.execute(query, params)
|
|
586
|
+
rows = cursor.fetchall()
|
|
587
|
+
|
|
588
|
+
return [_row_to_diagnostic_report(row) for row in rows]
|
|
589
|
+
finally:
|
|
590
|
+
conn.close()
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _row_to_diagnostic_report(row: tuple) -> DiagnosticReport:
|
|
594
|
+
"""Convert a database row to a DiagnosticReport."""
|
|
595
|
+
recommendations_data = json.loads(row[6]) if row[6] else []
|
|
596
|
+
recommendations = [
|
|
597
|
+
DiagnosticRecommendation(
|
|
598
|
+
action=RemediationAction(r["action"]),
|
|
599
|
+
reason=r["reason"],
|
|
600
|
+
command=r["command"],
|
|
601
|
+
parameters=r.get("parameters", {}),
|
|
602
|
+
)
|
|
603
|
+
for r in recommendations_data
|
|
604
|
+
]
|
|
605
|
+
|
|
606
|
+
return DiagnosticReport(
|
|
607
|
+
id=row[0],
|
|
608
|
+
task_id=row[1],
|
|
609
|
+
run_id=row[2],
|
|
610
|
+
root_cause=row[3],
|
|
611
|
+
failure_category=FailureCategory(row[4]),
|
|
612
|
+
severity=Severity(row[5]),
|
|
613
|
+
recommendations=recommendations,
|
|
614
|
+
log_summary=row[7],
|
|
615
|
+
created_at=datetime.fromisoformat(row[8]),
|
|
616
|
+
)
|