htmlgraph 0.20.1__py3-none-any.whl → 0.27.5__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/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +51 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/__init__.py +8 -1
- htmlgraph/analytics/cli.py +86 -20
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +10 -6
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +67 -27
- htmlgraph/analytics_index.py +53 -20
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -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 +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -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 +578 -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 +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +2 -1
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/base.py +57 -2
- htmlgraph/builders/bug.py +19 -3
- htmlgraph/builders/chore.py +19 -3
- htmlgraph/builders/epic.py +19 -3
- htmlgraph/builders/feature.py +27 -3
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +19 -3
- htmlgraph/builders/spike.py +29 -3
- htmlgraph/builders/track.py +42 -1
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- 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 +197 -14
- htmlgraph/collections/bug.py +2 -1
- htmlgraph/collections/chore.py +2 -1
- htmlgraph/collections/epic.py +2 -1
- htmlgraph/collections/feature.py +2 -1
- htmlgraph/collections/insight.py +2 -1
- htmlgraph/collections/metric.py +2 -1
- htmlgraph/collections/pattern.py +2 -1
- htmlgraph/collections/phase.py +2 -1
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +13 -2
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +14 -1
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/converter.py +116 -7
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2246 -248
- htmlgraph/dashboard.html.backup +6592 -0
- 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 +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +2 -1
- 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 +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +2 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +86 -37
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +67 -9
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +8 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +790 -99
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +327 -76
- htmlgraph/hooks/orchestrator_reflector.py +31 -4
- htmlgraph/hooks/post_tool_use_failure.py +32 -7
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +92 -19
- htmlgraph/hooks/pretooluse.py +527 -7
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +99 -4
- htmlgraph/hooks/validator.py +212 -91
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +125 -100
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/models.py +217 -18
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +115 -4
- htmlgraph/parallel.py +2 -1
- htmlgraph/parser.py +86 -6
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +2 -1
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +295 -107
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +285 -3
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +33 -1
- htmlgraph/track_manager.py +38 -0
- htmlgraph/transcript.py +18 -5
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -4839
- htmlgraph/sdk.py +0 -2359
- htmlgraph-0.20.1.dist-info/RECORD +0 -118
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket Integration for Real-Time Cost Alerts - Phase 3.1
|
|
3
|
+
|
|
4
|
+
Streams cost monitoring alerts to clients with:
|
|
5
|
+
- <1s latency (critical requirement)
|
|
6
|
+
- Real-time budget warnings
|
|
7
|
+
- Cost trajectory predictions
|
|
8
|
+
- Per-model and per-tool cost breakdowns
|
|
9
|
+
|
|
10
|
+
Integrates with:
|
|
11
|
+
- WebSocketManager for connection handling
|
|
12
|
+
- CostMonitor for alert generation
|
|
13
|
+
- EventSubscriptionFilter for cost-specific filtering
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from htmlgraph.analytics.cost_monitor import CostMonitor
|
|
23
|
+
from htmlgraph.api.websocket import EventSubscriptionFilter, WebSocketManager
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CostAlertFilter(EventSubscriptionFilter):
|
|
29
|
+
"""Extended filter for cost alert subscriptions."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
session_id: str | None = None,
|
|
34
|
+
alert_types: list[str] | None = None,
|
|
35
|
+
min_severity: str = "info",
|
|
36
|
+
cost_threshold_usd: float | None = None,
|
|
37
|
+
):
|
|
38
|
+
"""
|
|
39
|
+
Initialize cost alert filter.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
session_id: Filter by session
|
|
43
|
+
alert_types: Filter by alert types (e.g., ["budget_warning", "breach"])
|
|
44
|
+
min_severity: Minimum severity level ("info", "warning", "critical")
|
|
45
|
+
cost_threshold_usd: Alert on costs >= threshold
|
|
46
|
+
"""
|
|
47
|
+
# Initialize parent with cost-specific event types
|
|
48
|
+
super().__init__(
|
|
49
|
+
event_types=["cost_alert"],
|
|
50
|
+
session_id=session_id,
|
|
51
|
+
statuses=["alert"],
|
|
52
|
+
)
|
|
53
|
+
self.alert_types = alert_types or [
|
|
54
|
+
"budget_warning",
|
|
55
|
+
"trajectory_overage",
|
|
56
|
+
"model_overage",
|
|
57
|
+
"breach",
|
|
58
|
+
]
|
|
59
|
+
self.min_severity = min_severity
|
|
60
|
+
self.cost_threshold_usd = cost_threshold_usd
|
|
61
|
+
|
|
62
|
+
def matches_cost_alert(self, alert: dict[str, Any]) -> bool:
|
|
63
|
+
"""Check if alert matches all cost-specific filters."""
|
|
64
|
+
# Alert type filter
|
|
65
|
+
if alert.get("alert_type") not in self.alert_types:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
# Severity filter (info < warning < critical)
|
|
69
|
+
severity_levels = {"info": 0, "warning": 1, "critical": 2}
|
|
70
|
+
alert_severity = severity_levels.get(alert.get("severity", "info"), 0)
|
|
71
|
+
min_level = severity_levels.get(self.min_severity, 0)
|
|
72
|
+
if alert_severity < min_level:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
# Cost threshold filter
|
|
76
|
+
if self.cost_threshold_usd:
|
|
77
|
+
if alert.get("current_cost_usd", 0) < self.cost_threshold_usd:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class CostAlertStreamManager:
|
|
84
|
+
"""
|
|
85
|
+
Manages real-time cost alert streaming via WebSocket.
|
|
86
|
+
|
|
87
|
+
Features:
|
|
88
|
+
- <1s latency for alert delivery
|
|
89
|
+
- Per-client cost alert filtering
|
|
90
|
+
- Alert aggregation and deduplication
|
|
91
|
+
- Trajectory prediction streaming
|
|
92
|
+
- Cost breakdown updates
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
websocket_manager: WebSocketManager,
|
|
98
|
+
cost_monitor: CostMonitor,
|
|
99
|
+
poll_interval_ms: float = 100.0,
|
|
100
|
+
alert_batch_size: int = 10,
|
|
101
|
+
):
|
|
102
|
+
"""
|
|
103
|
+
Initialize cost alert stream manager.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
websocket_manager: WebSocketManager for connection handling
|
|
107
|
+
cost_monitor: CostMonitor for cost data
|
|
108
|
+
poll_interval_ms: How often to check for new alerts
|
|
109
|
+
alert_batch_size: Max alerts per batch
|
|
110
|
+
"""
|
|
111
|
+
self.websocket_manager = websocket_manager
|
|
112
|
+
self.cost_monitor = cost_monitor
|
|
113
|
+
self.poll_interval_ms = poll_interval_ms / 1000.0 # Convert to seconds
|
|
114
|
+
self.alert_batch_size = alert_batch_size
|
|
115
|
+
|
|
116
|
+
# Track last seen alert timestamp per session
|
|
117
|
+
self.last_alert_timestamp: dict[str, str] = {}
|
|
118
|
+
|
|
119
|
+
async def stream_cost_alerts(
|
|
120
|
+
self, session_id: str, client_id: str, cost_alert_filter: CostAlertFilter
|
|
121
|
+
) -> None:
|
|
122
|
+
"""
|
|
123
|
+
Stream cost alerts to a connected client.
|
|
124
|
+
|
|
125
|
+
Maintains <1s latency by:
|
|
126
|
+
- Quick database queries (indexed on timestamp)
|
|
127
|
+
- Efficient batching
|
|
128
|
+
- Minimal processing per alert
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
session_id: Session ID
|
|
132
|
+
client_id: Client ID
|
|
133
|
+
cost_alert_filter: Subscription filter for alerts
|
|
134
|
+
"""
|
|
135
|
+
if (
|
|
136
|
+
session_id not in self.websocket_manager.connections
|
|
137
|
+
or client_id not in self.websocket_manager.connections[session_id]
|
|
138
|
+
):
|
|
139
|
+
logger.warning(f"Client not found: {session_id}/{client_id}")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
client = self.websocket_manager.connections[session_id][client_id]
|
|
143
|
+
last_alert_time = self.last_alert_timestamp.get(
|
|
144
|
+
session_id, "1970-01-01T00:00:00Z"
|
|
145
|
+
)
|
|
146
|
+
consecutive_empty_polls = 0
|
|
147
|
+
max_empty_polls = 20
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
while True:
|
|
151
|
+
try:
|
|
152
|
+
# Fetch new alerts since last poll (rapid query)
|
|
153
|
+
alerts = await self._fetch_new_alerts(
|
|
154
|
+
session_id, last_alert_time, cost_alert_filter
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if alerts:
|
|
158
|
+
consecutive_empty_polls = 0
|
|
159
|
+
last_alert_time = alerts[-1]["timestamp"]
|
|
160
|
+
self.last_alert_timestamp[session_id] = last_alert_time
|
|
161
|
+
|
|
162
|
+
# Batch alerts for sending
|
|
163
|
+
for i in range(0, len(alerts), self.alert_batch_size):
|
|
164
|
+
batch = alerts[i : i + self.alert_batch_size]
|
|
165
|
+
|
|
166
|
+
message = {
|
|
167
|
+
"type": "cost_alerts",
|
|
168
|
+
"session_id": session_id,
|
|
169
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
170
|
+
"alerts": batch,
|
|
171
|
+
"count": len(batch),
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# Send with timestamp for latency tracking
|
|
175
|
+
message["sent_at"] = datetime.now(timezone.utc).isoformat()
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
await client.websocket.send_json(message)
|
|
179
|
+
client.events_sent += 1
|
|
180
|
+
client.bytes_sent += len(json.dumps(message))
|
|
181
|
+
self.websocket_manager.metrics[
|
|
182
|
+
"total_events_broadcast"
|
|
183
|
+
] += 1
|
|
184
|
+
self.websocket_manager.metrics["total_bytes_sent"] += (
|
|
185
|
+
len(json.dumps(message))
|
|
186
|
+
)
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.error(f"Failed to send alert: {e}")
|
|
189
|
+
return
|
|
190
|
+
else:
|
|
191
|
+
consecutive_empty_polls += 1
|
|
192
|
+
|
|
193
|
+
# Adaptive polling: increase interval if no alerts
|
|
194
|
+
if consecutive_empty_polls > max_empty_polls:
|
|
195
|
+
poll_interval = min(self.poll_interval_ms * 2, 1.0)
|
|
196
|
+
else:
|
|
197
|
+
poll_interval = self.poll_interval_ms
|
|
198
|
+
|
|
199
|
+
await asyncio.sleep(poll_interval)
|
|
200
|
+
|
|
201
|
+
except Exception as e:
|
|
202
|
+
logger.error(f"Error in cost alert streaming: {e}")
|
|
203
|
+
await asyncio.sleep(self.poll_interval_ms)
|
|
204
|
+
|
|
205
|
+
except asyncio.CancelledError:
|
|
206
|
+
logger.info(f"Cost alert stream cancelled for {session_id}/{client_id}")
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.error(f"Unexpected error in cost alert stream: {e}")
|
|
209
|
+
|
|
210
|
+
async def _fetch_new_alerts(
|
|
211
|
+
self,
|
|
212
|
+
session_id: str,
|
|
213
|
+
since_timestamp: str,
|
|
214
|
+
cost_alert_filter: CostAlertFilter,
|
|
215
|
+
) -> list[dict[str, Any]]:
|
|
216
|
+
"""
|
|
217
|
+
Fetch new cost alerts for a session.
|
|
218
|
+
|
|
219
|
+
Optimized for <1s latency with indexed queries.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
session_id: Session ID
|
|
223
|
+
since_timestamp: Only fetch alerts after this timestamp
|
|
224
|
+
cost_alert_filter: Filter for alert types
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
List of alert dictionaries
|
|
228
|
+
"""
|
|
229
|
+
conn = self.cost_monitor.connect()
|
|
230
|
+
cursor = conn.cursor()
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
# Query with composite index on (session_id, timestamp DESC)
|
|
234
|
+
cursor.execute(
|
|
235
|
+
"""
|
|
236
|
+
SELECT event_id, session_id, alert_type, message,
|
|
237
|
+
current_cost_usd, budget_usd, severity, timestamp
|
|
238
|
+
FROM cost_events
|
|
239
|
+
WHERE session_id = ? AND alert_type IS NOT NULL
|
|
240
|
+
AND timestamp > ?
|
|
241
|
+
ORDER BY timestamp ASC
|
|
242
|
+
LIMIT ?
|
|
243
|
+
""",
|
|
244
|
+
(session_id, since_timestamp, self.alert_batch_size * 2),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
alerts = []
|
|
248
|
+
for row in cursor.fetchall():
|
|
249
|
+
alert_dict = {
|
|
250
|
+
"alert_id": row["event_id"],
|
|
251
|
+
"alert_type": row["alert_type"],
|
|
252
|
+
"session_id": row["session_id"],
|
|
253
|
+
"message": row["message"],
|
|
254
|
+
"current_cost_usd": row["current_cost_usd"],
|
|
255
|
+
"budget_usd": row["budget_usd"],
|
|
256
|
+
"severity": row["severity"],
|
|
257
|
+
"timestamp": row["timestamp"],
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
# Apply custom cost alert filters
|
|
261
|
+
if cost_alert_filter.matches_cost_alert(alert_dict):
|
|
262
|
+
alerts.append(alert_dict)
|
|
263
|
+
|
|
264
|
+
return alerts
|
|
265
|
+
|
|
266
|
+
except Exception as e:
|
|
267
|
+
logger.error(f"Error fetching alerts: {e}")
|
|
268
|
+
return []
|
|
269
|
+
|
|
270
|
+
async def stream_cost_breakdown(
|
|
271
|
+
self, session_id: str, client_id: str, update_interval_seconds: float = 5.0
|
|
272
|
+
) -> None:
|
|
273
|
+
"""
|
|
274
|
+
Stream cost breakdown updates to a client.
|
|
275
|
+
|
|
276
|
+
Sends periodic updates of cost by model, tool, and agent.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
session_id: Session ID
|
|
280
|
+
client_id: Client ID
|
|
281
|
+
update_interval_seconds: How often to send updates
|
|
282
|
+
"""
|
|
283
|
+
if (
|
|
284
|
+
session_id not in self.websocket_manager.connections
|
|
285
|
+
or client_id not in self.websocket_manager.connections[session_id]
|
|
286
|
+
):
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
client = self.websocket_manager.connections[session_id][client_id]
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
while True:
|
|
293
|
+
try:
|
|
294
|
+
# Get current cost breakdown
|
|
295
|
+
breakdown = self.cost_monitor.get_cost_breakdown(session_id)
|
|
296
|
+
|
|
297
|
+
message = {
|
|
298
|
+
"type": "cost_breakdown",
|
|
299
|
+
"session_id": session_id,
|
|
300
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
301
|
+
"by_model": breakdown.by_model,
|
|
302
|
+
"by_tool": breakdown.by_tool,
|
|
303
|
+
"by_agent": breakdown.by_agent,
|
|
304
|
+
"by_subagent_type": breakdown.by_subagent_type,
|
|
305
|
+
"total_cost_usd": breakdown.total_cost_usd,
|
|
306
|
+
"total_tokens": breakdown.total_tokens,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
await client.websocket.send_json(message)
|
|
310
|
+
client.events_sent += 1
|
|
311
|
+
self.websocket_manager.metrics["total_events_broadcast"] += 1
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
logger.error(f"Error sending cost breakdown: {e}")
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
await asyncio.sleep(update_interval_seconds)
|
|
318
|
+
|
|
319
|
+
except asyncio.CancelledError:
|
|
320
|
+
logger.info(f"Cost breakdown stream cancelled for {session_id}/{client_id}")
|
|
321
|
+
|
|
322
|
+
async def stream_cost_trajectory(
|
|
323
|
+
self,
|
|
324
|
+
session_id: str,
|
|
325
|
+
client_id: str,
|
|
326
|
+
update_interval_seconds: float = 10.0,
|
|
327
|
+
lookback_minutes: int = 5,
|
|
328
|
+
) -> None:
|
|
329
|
+
"""
|
|
330
|
+
Stream cost trajectory predictions to a client.
|
|
331
|
+
|
|
332
|
+
Periodically recalculates cost trajectory and sends predictions.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
session_id: Session ID
|
|
336
|
+
client_id: Client ID
|
|
337
|
+
update_interval_seconds: How often to update predictions
|
|
338
|
+
lookback_minutes: How far back to analyze
|
|
339
|
+
"""
|
|
340
|
+
if (
|
|
341
|
+
session_id not in self.websocket_manager.connections
|
|
342
|
+
or client_id not in self.websocket_manager.connections[session_id]
|
|
343
|
+
):
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
client = self.websocket_manager.connections[session_id][client_id]
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
while True:
|
|
350
|
+
try:
|
|
351
|
+
# Get trajectory prediction
|
|
352
|
+
prediction = self.cost_monitor.predict_cost_trajectory(
|
|
353
|
+
session_id, lookback_minutes=lookback_minutes
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
message = {
|
|
357
|
+
"type": "cost_trajectory",
|
|
358
|
+
"session_id": session_id,
|
|
359
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
360
|
+
"prediction": prediction,
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if prediction.get("prediction_available"):
|
|
364
|
+
await client.websocket.send_json(message)
|
|
365
|
+
client.events_sent += 1
|
|
366
|
+
self.websocket_manager.metrics["total_events_broadcast"] += 1
|
|
367
|
+
|
|
368
|
+
except Exception as e:
|
|
369
|
+
logger.error(f"Error sending cost trajectory: {e}")
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
await asyncio.sleep(update_interval_seconds)
|
|
373
|
+
|
|
374
|
+
except asyncio.CancelledError:
|
|
375
|
+
logger.info(
|
|
376
|
+
f"Cost trajectory stream cancelled for {session_id}/{client_id}"
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
async def create_cost_alert_subscription(
|
|
381
|
+
session_id: str,
|
|
382
|
+
client_id: str,
|
|
383
|
+
websocket_manager: WebSocketManager,
|
|
384
|
+
cost_monitor: CostMonitor,
|
|
385
|
+
alert_types: list[str] | None = None,
|
|
386
|
+
min_severity: str = "warning",
|
|
387
|
+
) -> CostAlertStreamManager:
|
|
388
|
+
"""
|
|
389
|
+
Create a cost alert subscription for a WebSocket client.
|
|
390
|
+
|
|
391
|
+
Factory function to set up cost alert streaming.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
session_id: Session ID
|
|
395
|
+
client_id: Client ID
|
|
396
|
+
websocket_manager: WebSocketManager instance
|
|
397
|
+
cost_monitor: CostMonitor instance
|
|
398
|
+
alert_types: Which alert types to subscribe to
|
|
399
|
+
min_severity: Minimum alert severity
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
CostAlertStreamManager instance
|
|
403
|
+
"""
|
|
404
|
+
manager = CostAlertStreamManager(websocket_manager, cost_monitor)
|
|
405
|
+
filter_obj = CostAlertFilter(
|
|
406
|
+
session_id=session_id,
|
|
407
|
+
alert_types=alert_types,
|
|
408
|
+
min_severity=min_severity,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# Start streaming tasks
|
|
412
|
+
asyncio.create_task(manager.stream_cost_alerts(session_id, client_id, filter_obj))
|
|
413
|
+
asyncio.create_task(manager.stream_cost_breakdown(session_id, client_id))
|
|
414
|
+
asyncio.create_task(manager.stream_cost_trajectory(session_id, client_id))
|
|
415
|
+
|
|
416
|
+
return manager
|