htmlgraph 0.25.0__py3-none-any.whl → 0.26.2__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.
Files changed (41) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +1 -1
  5. htmlgraph/api/main.py +252 -47
  6. htmlgraph/api/templates/dashboard.html +11 -0
  7. htmlgraph/api/templates/partials/activity-feed.html +517 -8
  8. htmlgraph/cli.py +1 -1
  9. htmlgraph/config.py +173 -96
  10. htmlgraph/dashboard.html +632 -7237
  11. htmlgraph/db/schema.py +258 -9
  12. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  13. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  14. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  15. htmlgraph/hooks/cigs_pretool_enforcer.py +2 -2
  16. htmlgraph/hooks/concurrent_sessions.py +208 -0
  17. htmlgraph/hooks/context.py +88 -10
  18. htmlgraph/hooks/drift_handler.py +24 -20
  19. htmlgraph/hooks/event_tracker.py +264 -189
  20. htmlgraph/hooks/orchestrator.py +6 -4
  21. htmlgraph/hooks/orchestrator_reflector.py +4 -4
  22. htmlgraph/hooks/pretooluse.py +63 -36
  23. htmlgraph/hooks/prompt_analyzer.py +14 -25
  24. htmlgraph/hooks/session_handler.py +123 -69
  25. htmlgraph/hooks/state_manager.py +7 -4
  26. htmlgraph/hooks/subagent_stop.py +3 -2
  27. htmlgraph/hooks/validator.py +15 -11
  28. htmlgraph/operations/fastapi_server.py +2 -2
  29. htmlgraph/orchestration/headless_spawner.py +489 -16
  30. htmlgraph/orchestration/live_events.py +377 -0
  31. htmlgraph/server.py +100 -203
  32. htmlgraph-0.26.2.data/data/htmlgraph/dashboard.html +812 -0
  33. {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.2.dist-info}/METADATA +1 -1
  34. {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.2.dist-info}/RECORD +40 -32
  35. htmlgraph-0.25.0.data/data/htmlgraph/dashboard.html +0 -7417
  36. {htmlgraph-0.25.0.data → htmlgraph-0.26.2.data}/data/htmlgraph/styles.css +0 -0
  37. {htmlgraph-0.25.0.data → htmlgraph-0.26.2.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  38. {htmlgraph-0.25.0.data → htmlgraph-0.26.2.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  39. {htmlgraph-0.25.0.data → htmlgraph-0.26.2.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  40. {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.2.dist-info}/WHEEL +0 -0
  41. {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,377 @@
1
+ """
2
+ Live Event Publisher for Real-Time WebSocket Streaming.
3
+
4
+ This module provides a centralized way to publish live events that will be
5
+ streamed to connected WebSocket clients in real-time. Events are stored in
6
+ a SQLite table and polled by the WebSocket handler.
7
+
8
+ Usage:
9
+ from htmlgraph.orchestration.live_events import LiveEventPublisher
10
+
11
+ publisher = LiveEventPublisher()
12
+ publisher.spawner_start("gemini", "Analyze codebase", parent_event_id="evt-123")
13
+ publisher.spawner_phase("gemini", "executing", progress=50)
14
+ publisher.spawner_complete("gemini", success=True, duration=15.3, response="...")
15
+ """
16
+
17
+ import json
18
+ import logging
19
+ import os
20
+ from datetime import datetime, timezone
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class LiveEventPublisher:
28
+ """
29
+ Publisher for live events that get streamed via WebSocket.
30
+
31
+ Events are written to the live_events table in SQLite and polled
32
+ by the dashboard WebSocket handler for real-time streaming.
33
+ """
34
+
35
+ def __init__(self, db_path: str | None = None):
36
+ """
37
+ Initialize the live event publisher.
38
+
39
+ Args:
40
+ db_path: Path to SQLite database. If None, uses default location.
41
+ """
42
+ self._db_path = db_path
43
+ self._db: Any = None
44
+
45
+ def _get_db(self) -> Any:
46
+ """Get or create database connection."""
47
+ if self._db is None:
48
+ try:
49
+ from htmlgraph.db.schema import HtmlGraphDB
50
+
51
+ if self._db_path:
52
+ self._db = HtmlGraphDB(self._db_path)
53
+ else:
54
+ # Use project database path from environment or cwd
55
+ project_root = os.getenv("HTMLGRAPH_PROJECT_ROOT", os.getcwd())
56
+ default_path = str(
57
+ Path(project_root) / ".htmlgraph" / "index.sqlite"
58
+ )
59
+
60
+ # Check if database exists
61
+ if not Path(default_path).exists():
62
+ logger.debug(f"Database not found at {default_path}")
63
+ return None
64
+
65
+ self._db = HtmlGraphDB(default_path)
66
+ except Exception as e:
67
+ logger.warning(f"Failed to initialize database for live events: {e}")
68
+ return None
69
+ return self._db
70
+
71
+ def _get_session_id(self) -> str | None:
72
+ """Get current session ID from environment."""
73
+ return os.getenv("HTMLGRAPH_PARENT_SESSION") or os.getenv("CLAUDE_SESSION_ID")
74
+
75
+ def publish(
76
+ self,
77
+ event_type: str,
78
+ event_data: dict[str, Any],
79
+ parent_event_id: str | None = None,
80
+ session_id: str | None = None,
81
+ spawner_type: str | None = None,
82
+ ) -> int | None:
83
+ """
84
+ Publish a live event for WebSocket streaming.
85
+
86
+ Args:
87
+ event_type: Type of event (e.g., spawner_start, spawner_complete)
88
+ event_data: Event payload dictionary
89
+ parent_event_id: Parent event ID for hierarchical linking
90
+ session_id: Session this event belongs to
91
+ spawner_type: Spawner type if applicable (gemini, codex, copilot)
92
+
93
+ Returns:
94
+ Live event ID if successful, None otherwise
95
+ """
96
+ db = self._get_db()
97
+ if db is None:
98
+ logger.debug("Database not available for live events")
99
+ return None
100
+
101
+ # Add timestamp to event data if not present
102
+ if "timestamp" not in event_data:
103
+ event_data["timestamp"] = datetime.now(timezone.utc).isoformat()
104
+
105
+ # Use session from environment if not provided
106
+ if session_id is None:
107
+ session_id = self._get_session_id()
108
+
109
+ try:
110
+ result: int | None = db.insert_live_event(
111
+ event_type=event_type,
112
+ event_data=event_data,
113
+ parent_event_id=parent_event_id,
114
+ session_id=session_id,
115
+ spawner_type=spawner_type,
116
+ )
117
+ return result
118
+ except Exception as e:
119
+ logger.warning(f"Failed to publish live event: {e}")
120
+ return None
121
+
122
+ def spawner_start(
123
+ self,
124
+ spawner_type: str,
125
+ prompt: str,
126
+ parent_event_id: str | None = None,
127
+ model: str | None = None,
128
+ session_id: str | None = None,
129
+ ) -> int | None:
130
+ """
131
+ Publish a spawner start event.
132
+
133
+ Args:
134
+ spawner_type: Type of spawner (gemini, codex, copilot)
135
+ prompt: Task prompt being executed
136
+ parent_event_id: Parent delegation event ID
137
+ model: Model being used (optional)
138
+ session_id: Session ID (optional, auto-detected)
139
+
140
+ Returns:
141
+ Live event ID if successful
142
+ """
143
+ event_data = {
144
+ "spawner_type": spawner_type,
145
+ "prompt_preview": prompt[:200] if prompt else "",
146
+ "prompt_length": len(prompt) if prompt else 0,
147
+ "status": "started",
148
+ "phase": "initializing",
149
+ }
150
+ if model:
151
+ event_data["model"] = model
152
+
153
+ return self.publish(
154
+ event_type="spawner_start",
155
+ event_data=event_data,
156
+ parent_event_id=parent_event_id,
157
+ session_id=session_id,
158
+ spawner_type=spawner_type,
159
+ )
160
+
161
+ def spawner_phase(
162
+ self,
163
+ spawner_type: str,
164
+ phase: str,
165
+ progress: int | None = None,
166
+ details: str | None = None,
167
+ parent_event_id: str | None = None,
168
+ session_id: str | None = None,
169
+ ) -> int | None:
170
+ """
171
+ Publish a spawner phase update event.
172
+
173
+ Args:
174
+ spawner_type: Type of spawner (gemini, codex, copilot)
175
+ phase: Current phase (e.g., "executing", "processing", "streaming")
176
+ progress: Progress percentage (0-100) if applicable
177
+ details: Additional details about the phase
178
+ parent_event_id: Parent delegation event ID
179
+ session_id: Session ID (optional, auto-detected)
180
+
181
+ Returns:
182
+ Live event ID if successful
183
+ """
184
+ event_data: dict[str, Any] = {
185
+ "spawner_type": spawner_type,
186
+ "phase": phase,
187
+ "status": "in_progress",
188
+ }
189
+ if progress is not None:
190
+ event_data["progress"] = progress
191
+ if details:
192
+ event_data["details"] = details[:200]
193
+
194
+ return self.publish(
195
+ event_type="spawner_phase",
196
+ event_data=event_data,
197
+ parent_event_id=parent_event_id,
198
+ session_id=session_id,
199
+ spawner_type=spawner_type,
200
+ )
201
+
202
+ def spawner_complete(
203
+ self,
204
+ spawner_type: str,
205
+ success: bool,
206
+ duration_seconds: float | None = None,
207
+ response_preview: str | None = None,
208
+ tokens_used: int | None = None,
209
+ error: str | None = None,
210
+ parent_event_id: str | None = None,
211
+ session_id: str | None = None,
212
+ ) -> int | None:
213
+ """
214
+ Publish a spawner completion event.
215
+
216
+ Args:
217
+ spawner_type: Type of spawner (gemini, codex, copilot)
218
+ success: Whether the spawner completed successfully
219
+ duration_seconds: Execution duration in seconds
220
+ response_preview: Preview of the response (first 200 chars)
221
+ tokens_used: Number of tokens used
222
+ error: Error message if failed
223
+ parent_event_id: Parent delegation event ID
224
+ session_id: Session ID (optional, auto-detected)
225
+
226
+ Returns:
227
+ Live event ID if successful
228
+ """
229
+ event_data: dict[str, Any] = {
230
+ "spawner_type": spawner_type,
231
+ "success": success,
232
+ "status": "completed" if success else "failed",
233
+ "phase": "done",
234
+ }
235
+ if duration_seconds is not None:
236
+ event_data["duration_seconds"] = round(duration_seconds, 2)
237
+ if response_preview:
238
+ event_data["response_preview"] = response_preview[:200]
239
+ if tokens_used is not None:
240
+ event_data["tokens_used"] = tokens_used
241
+ if error:
242
+ event_data["error"] = error[:500]
243
+
244
+ return self.publish(
245
+ event_type="spawner_complete",
246
+ event_data=event_data,
247
+ parent_event_id=parent_event_id,
248
+ session_id=session_id,
249
+ spawner_type=spawner_type,
250
+ )
251
+
252
+ def spawner_tool_use(
253
+ self,
254
+ spawner_type: str,
255
+ tool_name: str,
256
+ tool_input: dict[str, Any] | None = None,
257
+ parent_event_id: str | None = None,
258
+ session_id: str | None = None,
259
+ ) -> int | None:
260
+ """
261
+ Publish a spawner tool use event (when spawned AI uses a tool).
262
+
263
+ Args:
264
+ spawner_type: Type of spawner (gemini, codex, copilot)
265
+ tool_name: Name of the tool being used
266
+ tool_input: Tool input parameters
267
+ parent_event_id: Parent delegation event ID
268
+ session_id: Session ID (optional, auto-detected)
269
+
270
+ Returns:
271
+ Live event ID if successful
272
+ """
273
+ event_data: dict[str, Any] = {
274
+ "spawner_type": spawner_type,
275
+ "tool_name": tool_name,
276
+ "status": "tool_use",
277
+ "phase": "executing",
278
+ }
279
+ if tool_input:
280
+ # Truncate tool input for preview
281
+ input_str = json.dumps(tool_input)
282
+ event_data["tool_input_preview"] = input_str[:200]
283
+
284
+ return self.publish(
285
+ event_type="spawner_tool_use",
286
+ event_data=event_data,
287
+ parent_event_id=parent_event_id,
288
+ session_id=session_id,
289
+ spawner_type=spawner_type,
290
+ )
291
+
292
+ def spawner_message(
293
+ self,
294
+ spawner_type: str,
295
+ message: str,
296
+ role: str = "assistant",
297
+ parent_event_id: str | None = None,
298
+ session_id: str | None = None,
299
+ ) -> int | None:
300
+ """
301
+ Publish a spawner message event (when spawned AI sends a message).
302
+
303
+ Args:
304
+ spawner_type: Type of spawner (gemini, codex, copilot)
305
+ message: Message content
306
+ role: Message role (assistant, user, system)
307
+ parent_event_id: Parent delegation event ID
308
+ session_id: Session ID (optional, auto-detected)
309
+
310
+ Returns:
311
+ Live event ID if successful
312
+ """
313
+ event_data = {
314
+ "spawner_type": spawner_type,
315
+ "message_preview": message[:200] if message else "",
316
+ "message_length": len(message) if message else 0,
317
+ "role": role,
318
+ "status": "streaming",
319
+ "phase": "responding",
320
+ }
321
+
322
+ return self.publish(
323
+ event_type="spawner_message",
324
+ event_data=event_data,
325
+ parent_event_id=parent_event_id,
326
+ session_id=session_id,
327
+ spawner_type=spawner_type,
328
+ )
329
+
330
+
331
+ # Global singleton instance for convenience
332
+ _publisher: LiveEventPublisher | None = None
333
+
334
+
335
+ def get_publisher(db_path: str | None = None) -> LiveEventPublisher:
336
+ """
337
+ Get the global LiveEventPublisher instance.
338
+
339
+ Args:
340
+ db_path: Optional database path (only used on first call)
341
+
342
+ Returns:
343
+ LiveEventPublisher instance
344
+ """
345
+ global _publisher
346
+ if _publisher is None:
347
+ _publisher = LiveEventPublisher(db_path)
348
+ return _publisher
349
+
350
+
351
+ def publish_live_event(
352
+ event_type: str,
353
+ event_data: dict[str, Any],
354
+ parent_event_id: str | None = None,
355
+ session_id: str | None = None,
356
+ spawner_type: str | None = None,
357
+ ) -> int | None:
358
+ """
359
+ Convenience function to publish a live event using the global publisher.
360
+
361
+ Args:
362
+ event_type: Type of event
363
+ event_data: Event payload
364
+ parent_event_id: Parent event ID
365
+ session_id: Session ID
366
+ spawner_type: Spawner type
367
+
368
+ Returns:
369
+ Live event ID if successful
370
+ """
371
+ return get_publisher().publish(
372
+ event_type=event_type,
373
+ event_data=event_data,
374
+ parent_event_id=parent_event_id,
375
+ session_id=session_id,
376
+ spawner_type=spawner_type,
377
+ )