htmlgraph 0.24.2__py3-none-any.whl → 0.25.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.
- htmlgraph/__init__.py +20 -1
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/analytics/cross_session.py +4 -3
- htmlgraph/analytics/work_type.py +52 -16
- htmlgraph/analytics_index.py +51 -19
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/main.py +2115 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +812 -0
- htmlgraph/api/templates/dashboard.html +783 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +570 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +509 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +163 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/builders/base.py +55 -1
- htmlgraph/builders/bug.py +17 -2
- htmlgraph/builders/chore.py +17 -2
- htmlgraph/builders/epic.py +17 -2
- htmlgraph/builders/feature.py +25 -2
- htmlgraph/builders/phase.py +17 -2
- htmlgraph/builders/spike.py +27 -2
- htmlgraph/builders/track.py +14 -0
- htmlgraph/cigs/__init__.py +4 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cli.py +1427 -401
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +2 -0
- htmlgraph/collections/base.py +21 -0
- htmlgraph/collections/session.py +189 -0
- htmlgraph/collections/spike.py +7 -1
- htmlgraph/collections/task_delegation.py +236 -0
- htmlgraph/collections/traces.py +482 -0
- htmlgraph/config.py +113 -0
- htmlgraph/converter.py +41 -0
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +3315 -492
- htmlgraph-0.24.2.data/data/htmlgraph/dashboard.html → htmlgraph/dashboard.html.backup +2246 -248
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1334 -0
- htmlgraph/deploy.py +26 -27
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +710 -0
- htmlgraph/docs/README.md +533 -0
- htmlgraph/docs/version_check.py +3 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +2 -0
- htmlgraph/hooks/__init__.py +8 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/context.py +271 -0
- htmlgraph/hooks/drift_handler.py +521 -0
- htmlgraph/hooks/event_tracker.py +405 -15
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/pretooluse.py +476 -6
- htmlgraph/hooks/prompt_analyzer.py +648 -0
- htmlgraph/hooks/session_handler.py +583 -0
- htmlgraph/hooks/state_manager.py +501 -0
- htmlgraph/hooks/subagent_stop.py +309 -0
- htmlgraph/hooks/task_enforcer.py +39 -0
- htmlgraph/models.py +111 -15
- htmlgraph/operations/fastapi_server.py +230 -0
- htmlgraph/orchestration/headless_spawner.py +22 -14
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/repo_hash.py +511 -0
- htmlgraph/sdk.py +348 -10
- htmlgraph/server.py +194 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +131 -1
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/system_prompts.py +449 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +19 -0
- htmlgraph/validation.py +115 -0
- htmlgraph-0.25.0.data/data/htmlgraph/dashboard.html +7417 -0
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.25.0.dist-info}/METADATA +91 -64
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.25.0.dist-info}/RECORD +103 -42
- {htmlgraph-0.24.2.data → htmlgraph-0.25.0.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.24.2.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.24.2.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.24.2.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.25.0.dist-info}/WHEEL +0 -0
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.25.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool Execution Traces Collection
|
|
3
|
+
|
|
4
|
+
Provides query interface for tool execution traces stored in tool_traces table.
|
|
5
|
+
Enables analysis of tool performance, error patterns, and execution hierarchies.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from htmlgraph.sdk import SDK
|
|
9
|
+
>>> sdk = SDK(agent="claude")
|
|
10
|
+
>>>
|
|
11
|
+
>>> # Get traces for current session
|
|
12
|
+
>>> traces = sdk.traces.get_traces(session_id="sess-abc123")
|
|
13
|
+
>>> for trace in traces:
|
|
14
|
+
... print(f"{trace.tool_name}: {trace.duration_ms}ms")
|
|
15
|
+
>>>
|
|
16
|
+
>>> # Find slow tool calls
|
|
17
|
+
>>> slow = sdk.traces.get_slow_traces(threshold_ms=1000)
|
|
18
|
+
>>>
|
|
19
|
+
>>> # Get hierarchical view (parent-child relationships)
|
|
20
|
+
>>> tree = sdk.traces.get_trace_tree(trace_id="trace-xyz")
|
|
21
|
+
>>> print(f"Root: {tree.root.tool_name}")
|
|
22
|
+
>>> print(f"Children: {len(tree.children)}")
|
|
23
|
+
>>>
|
|
24
|
+
>>> # Get error traces for debugging
|
|
25
|
+
>>> errors = sdk.traces.get_error_traces(session_id="sess-abc123")
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from datetime import datetime, timezone
|
|
32
|
+
from typing import TYPE_CHECKING
|
|
33
|
+
|
|
34
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from htmlgraph.sdk import SDK
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class TraceRecord:
|
|
42
|
+
"""
|
|
43
|
+
Single tool execution trace.
|
|
44
|
+
|
|
45
|
+
Represents one complete execution of a tool with timing, input, output, and status.
|
|
46
|
+
Parent-child relationships via parent_tool_use_id enable hierarchical analysis.
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
tool_use_id: Unique identifier for this tool execution (UUID v4)
|
|
50
|
+
trace_id: Parent trace ID for grouping related executions
|
|
51
|
+
session_id: Session this execution belongs to
|
|
52
|
+
tool_name: Name of the tool executed (e.g., "Bash", "Read", "Write")
|
|
53
|
+
tool_input: Input parameters passed to the tool (dict, may be None)
|
|
54
|
+
tool_output: Result returned by the tool (dict, may be None if not yet complete)
|
|
55
|
+
start_time: When execution started (UTC datetime)
|
|
56
|
+
end_time: When execution ended (UTC datetime, None if still running)
|
|
57
|
+
duration_ms: Milliseconds to complete (None if still running or error)
|
|
58
|
+
status: Execution status (started, completed, failed, timeout, cancelled)
|
|
59
|
+
error_message: Error details if status is 'failed'
|
|
60
|
+
parent_tool_use_id: tool_use_id of parent tool if nested (None if top-level)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
tool_use_id: str
|
|
64
|
+
trace_id: str
|
|
65
|
+
session_id: str
|
|
66
|
+
tool_name: str
|
|
67
|
+
tool_input: dict | None
|
|
68
|
+
tool_output: dict | None
|
|
69
|
+
start_time: datetime
|
|
70
|
+
end_time: datetime | None
|
|
71
|
+
duration_ms: int | None
|
|
72
|
+
status: str | None
|
|
73
|
+
error_message: str | None
|
|
74
|
+
parent_tool_use_id: str | None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class TraceTree:
|
|
79
|
+
"""
|
|
80
|
+
Hierarchical view of traces (parent-child relationships).
|
|
81
|
+
|
|
82
|
+
Enables analysis of nested tool executions where one tool invokes others.
|
|
83
|
+
Example: Bash tool calls may invoke other tools as nested executions.
|
|
84
|
+
|
|
85
|
+
Attributes:
|
|
86
|
+
root: Root trace record (this execution)
|
|
87
|
+
children: List of child traces (tools invoked by this tool)
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
root: TraceRecord
|
|
91
|
+
children: list[TraceTree]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TraceCollection:
|
|
95
|
+
"""
|
|
96
|
+
Query interface for tool execution traces.
|
|
97
|
+
|
|
98
|
+
Provides methods to retrieve, filter, and analyze tool execution traces
|
|
99
|
+
stored in the tool_traces database table. Supports querying by session,
|
|
100
|
+
tool name, performance thresholds, and error status.
|
|
101
|
+
|
|
102
|
+
All queries return data sorted by start_time DESC (newest first).
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
>>> sdk = SDK(agent="claude")
|
|
106
|
+
>>> traces = sdk.traces
|
|
107
|
+
>>>
|
|
108
|
+
>>> # Single trace
|
|
109
|
+
>>> trace = traces.get_trace("tool-use-id-123")
|
|
110
|
+
>>>
|
|
111
|
+
>>> # All traces for session
|
|
112
|
+
>>> all_traces = traces.get_traces("sess-123")
|
|
113
|
+
>>>
|
|
114
|
+
>>> # By tool name
|
|
115
|
+
>>> bash_traces = traces.get_traces_by_tool("Bash")
|
|
116
|
+
>>>
|
|
117
|
+
>>> # Performance analysis
|
|
118
|
+
>>> slow = traces.get_slow_traces(threshold_ms=1000)
|
|
119
|
+
>>> errors = traces.get_error_traces("sess-123")
|
|
120
|
+
>>>
|
|
121
|
+
>>> # Hierarchical view
|
|
122
|
+
>>> tree = traces.get_trace_tree("trace-id-123")
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def __init__(self, sdk: SDK):
|
|
126
|
+
"""
|
|
127
|
+
Initialize traces collection.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
sdk: Parent SDK instance
|
|
131
|
+
"""
|
|
132
|
+
self._sdk = sdk
|
|
133
|
+
self._db = HtmlGraphDB()
|
|
134
|
+
|
|
135
|
+
def _row_to_trace(self, row: tuple) -> TraceRecord:
|
|
136
|
+
"""
|
|
137
|
+
Convert database row to TraceRecord dataclass.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
row: SQLite row tuple from tool_traces query
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
TraceRecord with parsed fields
|
|
144
|
+
"""
|
|
145
|
+
import json
|
|
146
|
+
|
|
147
|
+
# Unpack tuple: (tool_use_id, trace_id, session_id, tool_name, tool_input,
|
|
148
|
+
# tool_output, start_time, end_time, duration_ms, status,
|
|
149
|
+
# error_message, parent_tool_use_id)
|
|
150
|
+
(
|
|
151
|
+
tool_use_id,
|
|
152
|
+
trace_id,
|
|
153
|
+
session_id,
|
|
154
|
+
tool_name,
|
|
155
|
+
tool_input_json,
|
|
156
|
+
tool_output_json,
|
|
157
|
+
start_time_iso,
|
|
158
|
+
end_time_iso,
|
|
159
|
+
duration_ms,
|
|
160
|
+
status,
|
|
161
|
+
error_message,
|
|
162
|
+
parent_tool_use_id,
|
|
163
|
+
) = row
|
|
164
|
+
|
|
165
|
+
# Parse JSON fields
|
|
166
|
+
tool_input = None
|
|
167
|
+
if tool_input_json:
|
|
168
|
+
try:
|
|
169
|
+
tool_input = json.loads(tool_input_json)
|
|
170
|
+
except (json.JSONDecodeError, TypeError):
|
|
171
|
+
tool_input = None
|
|
172
|
+
|
|
173
|
+
tool_output = None
|
|
174
|
+
if tool_output_json:
|
|
175
|
+
try:
|
|
176
|
+
tool_output = json.loads(tool_output_json)
|
|
177
|
+
except (json.JSONDecodeError, TypeError):
|
|
178
|
+
tool_output = None
|
|
179
|
+
|
|
180
|
+
# Parse timestamps
|
|
181
|
+
start_time: datetime | None = None
|
|
182
|
+
if start_time_iso:
|
|
183
|
+
try:
|
|
184
|
+
start_time = datetime.fromisoformat(
|
|
185
|
+
start_time_iso.replace("Z", "+00:00")
|
|
186
|
+
)
|
|
187
|
+
except (ValueError, AttributeError):
|
|
188
|
+
start_time = None
|
|
189
|
+
|
|
190
|
+
end_time: datetime | None = None
|
|
191
|
+
if end_time_iso:
|
|
192
|
+
try:
|
|
193
|
+
end_time = datetime.fromisoformat(end_time_iso.replace("Z", "+00:00"))
|
|
194
|
+
except (ValueError, AttributeError):
|
|
195
|
+
end_time = None
|
|
196
|
+
|
|
197
|
+
# Use current time if start_time is missing
|
|
198
|
+
if start_time is None:
|
|
199
|
+
start_time = datetime.now(timezone.utc)
|
|
200
|
+
|
|
201
|
+
return TraceRecord(
|
|
202
|
+
tool_use_id=tool_use_id,
|
|
203
|
+
trace_id=trace_id,
|
|
204
|
+
session_id=session_id,
|
|
205
|
+
tool_name=tool_name,
|
|
206
|
+
tool_input=tool_input,
|
|
207
|
+
tool_output=tool_output,
|
|
208
|
+
start_time=start_time,
|
|
209
|
+
end_time=end_time,
|
|
210
|
+
duration_ms=duration_ms,
|
|
211
|
+
status=status,
|
|
212
|
+
error_message=error_message,
|
|
213
|
+
parent_tool_use_id=parent_tool_use_id,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def get_trace(self, tool_use_id: str) -> TraceRecord | None:
|
|
217
|
+
"""
|
|
218
|
+
Get single trace by tool_use_id.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
tool_use_id: Unique tool execution ID (UUID v4)
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
TraceRecord if found, None otherwise
|
|
225
|
+
"""
|
|
226
|
+
try:
|
|
227
|
+
if not self._db.connection:
|
|
228
|
+
self._db.connect()
|
|
229
|
+
|
|
230
|
+
cursor = self._db.connection.cursor() # type: ignore[union-attr]
|
|
231
|
+
cursor.execute(
|
|
232
|
+
"""
|
|
233
|
+
SELECT tool_use_id, trace_id, session_id, tool_name, tool_input,
|
|
234
|
+
tool_output, start_time, end_time, duration_ms, status,
|
|
235
|
+
error_message, parent_tool_use_id
|
|
236
|
+
FROM tool_traces
|
|
237
|
+
WHERE tool_use_id = ?
|
|
238
|
+
""",
|
|
239
|
+
(tool_use_id,),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
row = cursor.fetchone()
|
|
243
|
+
if row:
|
|
244
|
+
return self._row_to_trace(row)
|
|
245
|
+
|
|
246
|
+
return None
|
|
247
|
+
except Exception as e:
|
|
248
|
+
print(f"Error getting trace {tool_use_id}: {e}")
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
def get_traces(
|
|
252
|
+
self,
|
|
253
|
+
session_id: str,
|
|
254
|
+
limit: int = 100,
|
|
255
|
+
start_time: datetime | None = None,
|
|
256
|
+
) -> list[TraceRecord]:
|
|
257
|
+
"""
|
|
258
|
+
Get traces for a session, ordered by start_time DESC (newest first).
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
session_id: Session to query
|
|
262
|
+
limit: Maximum traces to return (default 100)
|
|
263
|
+
start_time: Optional filter - only traces after this time
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
List of TraceRecord objects, newest first
|
|
267
|
+
"""
|
|
268
|
+
try:
|
|
269
|
+
if not self._db.connection:
|
|
270
|
+
self._db.connect()
|
|
271
|
+
|
|
272
|
+
cursor = self._db.connection.cursor() # type: ignore[union-attr]
|
|
273
|
+
|
|
274
|
+
if start_time:
|
|
275
|
+
start_time_iso = start_time.isoformat()
|
|
276
|
+
cursor.execute(
|
|
277
|
+
"""
|
|
278
|
+
SELECT tool_use_id, trace_id, session_id, tool_name, tool_input,
|
|
279
|
+
tool_output, start_time, end_time, duration_ms, status,
|
|
280
|
+
error_message, parent_tool_use_id
|
|
281
|
+
FROM tool_traces
|
|
282
|
+
WHERE session_id = ? AND start_time >= ?
|
|
283
|
+
ORDER BY start_time DESC
|
|
284
|
+
LIMIT ?
|
|
285
|
+
""",
|
|
286
|
+
(session_id, start_time_iso, limit),
|
|
287
|
+
)
|
|
288
|
+
else:
|
|
289
|
+
cursor.execute(
|
|
290
|
+
"""
|
|
291
|
+
SELECT tool_use_id, trace_id, session_id, tool_name, tool_input,
|
|
292
|
+
tool_output, start_time, end_time, duration_ms, status,
|
|
293
|
+
error_message, parent_tool_use_id
|
|
294
|
+
FROM tool_traces
|
|
295
|
+
WHERE session_id = ?
|
|
296
|
+
ORDER BY start_time DESC
|
|
297
|
+
LIMIT ?
|
|
298
|
+
""",
|
|
299
|
+
(session_id, limit),
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
rows = cursor.fetchall()
|
|
303
|
+
return [self._row_to_trace(row) for row in rows]
|
|
304
|
+
except Exception as e:
|
|
305
|
+
print(f"Error getting traces for session {session_id}: {e}")
|
|
306
|
+
return []
|
|
307
|
+
|
|
308
|
+
def get_traces_by_tool(self, tool_name: str, limit: int = 100) -> list[TraceRecord]:
|
|
309
|
+
"""
|
|
310
|
+
Get traces for specific tool name.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
tool_name: Name of the tool (e.g., "Bash", "Read", "Write")
|
|
314
|
+
limit: Maximum traces to return (default 100)
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
List of TraceRecord objects, newest first
|
|
318
|
+
"""
|
|
319
|
+
try:
|
|
320
|
+
if not self._db.connection:
|
|
321
|
+
self._db.connect()
|
|
322
|
+
|
|
323
|
+
cursor = self._db.connection.cursor() # type: ignore[union-attr]
|
|
324
|
+
cursor.execute(
|
|
325
|
+
"""
|
|
326
|
+
SELECT tool_use_id, trace_id, session_id, tool_name, tool_input,
|
|
327
|
+
tool_output, start_time, end_time, duration_ms, status,
|
|
328
|
+
error_message, parent_tool_use_id
|
|
329
|
+
FROM tool_traces
|
|
330
|
+
WHERE tool_name = ?
|
|
331
|
+
ORDER BY start_time DESC
|
|
332
|
+
LIMIT ?
|
|
333
|
+
""",
|
|
334
|
+
(tool_name, limit),
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
rows = cursor.fetchall()
|
|
338
|
+
return [self._row_to_trace(row) for row in rows]
|
|
339
|
+
except Exception as e:
|
|
340
|
+
print(f"Error getting traces for tool {tool_name}: {e}")
|
|
341
|
+
return []
|
|
342
|
+
|
|
343
|
+
def get_trace_tree(self, trace_id: str) -> TraceTree | None:
|
|
344
|
+
"""
|
|
345
|
+
Get hierarchical view with parent-child relationships.
|
|
346
|
+
|
|
347
|
+
Recursively builds a tree of traces where each node can have children
|
|
348
|
+
(tools invoked by that tool). Useful for understanding nested execution.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
trace_id: Root trace_id to build tree from
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
TraceTree with root and children, or None if not found
|
|
355
|
+
"""
|
|
356
|
+
try:
|
|
357
|
+
if not self._db.connection:
|
|
358
|
+
self._db.connect()
|
|
359
|
+
|
|
360
|
+
cursor = self._db.connection.cursor() # type: ignore[union-attr]
|
|
361
|
+
|
|
362
|
+
# Get root trace
|
|
363
|
+
cursor.execute(
|
|
364
|
+
"""
|
|
365
|
+
SELECT tool_use_id, trace_id, session_id, tool_name, tool_input,
|
|
366
|
+
tool_output, start_time, end_time, duration_ms, status,
|
|
367
|
+
error_message, parent_tool_use_id
|
|
368
|
+
FROM tool_traces
|
|
369
|
+
WHERE trace_id = ?
|
|
370
|
+
ORDER BY start_time DESC
|
|
371
|
+
LIMIT 1
|
|
372
|
+
""",
|
|
373
|
+
(trace_id,),
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
root_row = cursor.fetchone()
|
|
377
|
+
if not root_row:
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
root = self._row_to_trace(root_row)
|
|
381
|
+
|
|
382
|
+
# Recursively get children
|
|
383
|
+
def build_tree(parent_trace: TraceRecord) -> TraceTree:
|
|
384
|
+
cursor.execute(
|
|
385
|
+
"""
|
|
386
|
+
SELECT tool_use_id, trace_id, session_id, tool_name, tool_input,
|
|
387
|
+
tool_output, start_time, end_time, duration_ms, status,
|
|
388
|
+
error_message, parent_tool_use_id
|
|
389
|
+
FROM tool_traces
|
|
390
|
+
WHERE parent_tool_use_id = ?
|
|
391
|
+
ORDER BY start_time ASC
|
|
392
|
+
""",
|
|
393
|
+
(parent_trace.tool_use_id,),
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
child_rows = cursor.fetchall()
|
|
397
|
+
children = []
|
|
398
|
+
for child_row in child_rows:
|
|
399
|
+
child = self._row_to_trace(child_row)
|
|
400
|
+
children.append(build_tree(child))
|
|
401
|
+
|
|
402
|
+
return TraceTree(root=parent_trace, children=children)
|
|
403
|
+
|
|
404
|
+
return build_tree(root)
|
|
405
|
+
except Exception as e:
|
|
406
|
+
print(f"Error getting trace tree for {trace_id}: {e}")
|
|
407
|
+
return None
|
|
408
|
+
|
|
409
|
+
def get_slow_traces(self, threshold_ms: int, limit: int = 100) -> list[TraceRecord]:
|
|
410
|
+
"""
|
|
411
|
+
Find traces exceeding duration threshold.
|
|
412
|
+
|
|
413
|
+
Useful for identifying performance bottlenecks.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
threshold_ms: Minimum duration in milliseconds
|
|
417
|
+
limit: Maximum traces to return (default 100)
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
List of slow TraceRecord objects, slowest first
|
|
421
|
+
"""
|
|
422
|
+
try:
|
|
423
|
+
if not self._db.connection:
|
|
424
|
+
self._db.connect()
|
|
425
|
+
|
|
426
|
+
cursor = self._db.connection.cursor() # type: ignore[union-attr]
|
|
427
|
+
cursor.execute(
|
|
428
|
+
"""
|
|
429
|
+
SELECT tool_use_id, trace_id, session_id, tool_name, tool_input,
|
|
430
|
+
tool_output, start_time, end_time, duration_ms, status,
|
|
431
|
+
error_message, parent_tool_use_id
|
|
432
|
+
FROM tool_traces
|
|
433
|
+
WHERE duration_ms IS NOT NULL AND duration_ms >= ?
|
|
434
|
+
ORDER BY duration_ms DESC
|
|
435
|
+
LIMIT ?
|
|
436
|
+
""",
|
|
437
|
+
(threshold_ms, limit),
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
rows = cursor.fetchall()
|
|
441
|
+
return [self._row_to_trace(row) for row in rows]
|
|
442
|
+
except Exception as e:
|
|
443
|
+
print(f"Error getting slow traces: {e}")
|
|
444
|
+
return []
|
|
445
|
+
|
|
446
|
+
def get_error_traces(self, session_id: str, limit: int = 100) -> list[TraceRecord]:
|
|
447
|
+
"""
|
|
448
|
+
Get traces with errors/failures.
|
|
449
|
+
|
|
450
|
+
Filters for traces with status='failed' or error_message is not null.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
session_id: Session to query
|
|
454
|
+
limit: Maximum traces to return (default 100)
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
List of error TraceRecord objects, newest first
|
|
458
|
+
"""
|
|
459
|
+
try:
|
|
460
|
+
if not self._db.connection:
|
|
461
|
+
self._db.connect()
|
|
462
|
+
|
|
463
|
+
cursor = self._db.connection.cursor() # type: ignore[union-attr]
|
|
464
|
+
cursor.execute(
|
|
465
|
+
"""
|
|
466
|
+
SELECT tool_use_id, trace_id, session_id, tool_name, tool_input,
|
|
467
|
+
tool_output, start_time, end_time, duration_ms, status,
|
|
468
|
+
error_message, parent_tool_use_id
|
|
469
|
+
FROM tool_traces
|
|
470
|
+
WHERE session_id = ? AND (status IN ('failed', 'timeout', 'cancelled')
|
|
471
|
+
OR error_message IS NOT NULL)
|
|
472
|
+
ORDER BY start_time DESC
|
|
473
|
+
LIMIT ?
|
|
474
|
+
""",
|
|
475
|
+
(session_id, limit),
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
rows = cursor.fetchall()
|
|
479
|
+
return [self._row_to_trace(row) for row in rows]
|
|
480
|
+
except Exception as e:
|
|
481
|
+
print(f"Error getting error traces: {e}")
|
|
482
|
+
return []
|
htmlgraph/config.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HtmlGraph Configuration Management.
|
|
3
|
+
|
|
4
|
+
This module provides centralized configuration management using Pydantic Settings,
|
|
5
|
+
allowing configuration from environment variables, .env files, and CLI arguments.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pydantic_settings import BaseSettings
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HtmlGraphConfig(BaseSettings):
|
|
15
|
+
"""Global HtmlGraph configuration using Pydantic Settings.
|
|
16
|
+
|
|
17
|
+
Configuration can be provided via:
|
|
18
|
+
1. Environment variables (prefix: HTMLGRAPH_)
|
|
19
|
+
2. .env file
|
|
20
|
+
3. Direct instantiation with parameters
|
|
21
|
+
4. CLI argument overrides
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# Core paths
|
|
25
|
+
graph_dir: Path = Path.home() / ".htmlgraph"
|
|
26
|
+
|
|
27
|
+
# Feature tracking
|
|
28
|
+
features_dir: Path | None = None
|
|
29
|
+
sessions_dir: Path | None = None
|
|
30
|
+
spikes_dir: Path | None = None
|
|
31
|
+
tracks_dir: Path | None = None
|
|
32
|
+
archives_dir: Path | None = None
|
|
33
|
+
|
|
34
|
+
# CLI behavior
|
|
35
|
+
debug: bool = False
|
|
36
|
+
verbose: bool = False
|
|
37
|
+
auto_sync: bool = True
|
|
38
|
+
color_output: bool = True
|
|
39
|
+
|
|
40
|
+
# Session management
|
|
41
|
+
max_sessions: int = 100
|
|
42
|
+
session_retention_days: int = 30
|
|
43
|
+
auto_archive_sessions: bool = True
|
|
44
|
+
|
|
45
|
+
# Performance
|
|
46
|
+
max_query_results: int = 1000
|
|
47
|
+
cache_enabled: bool = True
|
|
48
|
+
cache_ttl_seconds: int = 3600
|
|
49
|
+
|
|
50
|
+
# Logging
|
|
51
|
+
log_level: str = "INFO"
|
|
52
|
+
log_file: Path | None = None
|
|
53
|
+
|
|
54
|
+
model_config = {
|
|
55
|
+
"env_prefix": "HTMLGRAPH_",
|
|
56
|
+
"env_file": ".env",
|
|
57
|
+
"case_sensitive": False,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def __init__(self, **data: Any) -> None:
|
|
61
|
+
"""Initialize config and compute derived paths."""
|
|
62
|
+
super().__init__(**data)
|
|
63
|
+
# Compute derived paths if not explicitly set
|
|
64
|
+
if self.features_dir is None:
|
|
65
|
+
self.features_dir = self.graph_dir / "features"
|
|
66
|
+
if self.sessions_dir is None:
|
|
67
|
+
self.sessions_dir = self.graph_dir / "sessions"
|
|
68
|
+
if self.spikes_dir is None:
|
|
69
|
+
self.spikes_dir = self.graph_dir / "spikes"
|
|
70
|
+
if self.tracks_dir is None:
|
|
71
|
+
self.tracks_dir = self.graph_dir / "tracks"
|
|
72
|
+
if self.archives_dir is None:
|
|
73
|
+
self.archives_dir = self.graph_dir / "archives"
|
|
74
|
+
|
|
75
|
+
def ensure_directories(self) -> None:
|
|
76
|
+
"""Create all configured directories if they don't exist."""
|
|
77
|
+
for directory in [
|
|
78
|
+
self.graph_dir,
|
|
79
|
+
self.features_dir,
|
|
80
|
+
self.sessions_dir,
|
|
81
|
+
self.spikes_dir,
|
|
82
|
+
self.tracks_dir,
|
|
83
|
+
self.archives_dir,
|
|
84
|
+
]:
|
|
85
|
+
if directory:
|
|
86
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
|
|
88
|
+
def get_config_dict(self) -> dict[str, Any]:
|
|
89
|
+
"""Get configuration as dictionary."""
|
|
90
|
+
return {
|
|
91
|
+
"graph_dir": str(self.graph_dir),
|
|
92
|
+
"features_dir": str(self.features_dir),
|
|
93
|
+
"sessions_dir": str(self.sessions_dir),
|
|
94
|
+
"spikes_dir": str(self.spikes_dir),
|
|
95
|
+
"tracks_dir": str(self.tracks_dir),
|
|
96
|
+
"archives_dir": str(self.archives_dir),
|
|
97
|
+
"debug": self.debug,
|
|
98
|
+
"verbose": self.verbose,
|
|
99
|
+
"auto_sync": self.auto_sync,
|
|
100
|
+
"color_output": self.color_output,
|
|
101
|
+
"max_sessions": self.max_sessions,
|
|
102
|
+
"session_retention_days": self.session_retention_days,
|
|
103
|
+
"auto_archive_sessions": self.auto_archive_sessions,
|
|
104
|
+
"max_query_results": self.max_query_results,
|
|
105
|
+
"cache_enabled": self.cache_enabled,
|
|
106
|
+
"cache_ttl_seconds": self.cache_ttl_seconds,
|
|
107
|
+
"log_level": self.log_level,
|
|
108
|
+
"log_file": str(self.log_file) if self.log_file else None,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# Global configuration instance
|
|
113
|
+
config: HtmlGraphConfig = HtmlGraphConfig()
|
htmlgraph/converter.py
CHANGED
|
@@ -595,6 +595,47 @@ def html_to_session(filepath: Path | str) -> Session:
|
|
|
595
595
|
|
|
596
596
|
data["detected_patterns"] = detected_patterns
|
|
597
597
|
|
|
598
|
+
# Parse error log from error section (if present)
|
|
599
|
+
error_log = []
|
|
600
|
+
for details in parser.query("section[data-error-log] details"):
|
|
601
|
+
error_data = {
|
|
602
|
+
"error_type": details.attrs.get("data-error-type", "Unknown"),
|
|
603
|
+
"message": "",
|
|
604
|
+
"traceback": None,
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
ts = details.attrs.get("data-ts")
|
|
608
|
+
if ts:
|
|
609
|
+
error_data["timestamp"] = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
610
|
+
|
|
611
|
+
tool = details.attrs.get("data-tool")
|
|
612
|
+
if tool:
|
|
613
|
+
error_data["tool"] = tool
|
|
614
|
+
|
|
615
|
+
# Parse summary text (first line of details)
|
|
616
|
+
summary_el_results = details.query("summary")
|
|
617
|
+
summary_el = summary_el_results[0] if summary_el_results else None
|
|
618
|
+
if summary_el:
|
|
619
|
+
summary_text = summary_el.to_text().strip()
|
|
620
|
+
# Extract message from "ErrorType: message" format
|
|
621
|
+
if ": " in summary_text:
|
|
622
|
+
error_data["message"] = summary_text.split(": ", 1)[1]
|
|
623
|
+
else:
|
|
624
|
+
error_data["message"] = summary_text
|
|
625
|
+
|
|
626
|
+
# Parse traceback (if present)
|
|
627
|
+
traceback_el_results = details.query("pre.traceback")
|
|
628
|
+
traceback_el = traceback_el_results[0] if traceback_el_results else None
|
|
629
|
+
if traceback_el:
|
|
630
|
+
error_data["traceback"] = traceback_el.to_text().strip()
|
|
631
|
+
|
|
632
|
+
if error_data.get("message") or error_data.get("traceback"):
|
|
633
|
+
from htmlgraph.models import ErrorEntry
|
|
634
|
+
|
|
635
|
+
error_log.append(ErrorEntry(**error_data))
|
|
636
|
+
|
|
637
|
+
data["error_log"] = error_log
|
|
638
|
+
|
|
598
639
|
return Session(**data)
|
|
599
640
|
|
|
600
641
|
|