htmlgraph 0.9.3__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 +173 -17
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +127 -0
- htmlgraph/agent_registry.py +45 -30
- htmlgraph/agents.py +160 -107
- htmlgraph/analytics/__init__.py +9 -2
- htmlgraph/analytics/cli.py +190 -51
- 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 +192 -100
- 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 +190 -14
- htmlgraph/analytics_index.py +135 -51
- 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 +208 -0
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/__init__.py +14 -0
- htmlgraph/builders/base.py +118 -29
- htmlgraph/builders/bug.py +150 -0
- htmlgraph/builders/chore.py +119 -0
- htmlgraph/builders/epic.py +150 -0
- htmlgraph/builders/feature.py +31 -6
- htmlgraph/builders/insight.py +195 -0
- htmlgraph/builders/metric.py +217 -0
- htmlgraph/builders/pattern.py +202 -0
- htmlgraph/builders/phase.py +162 -0
- htmlgraph/builders/spike.py +52 -19
- htmlgraph/builders/track.py +148 -72
- 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 +18 -0
- htmlgraph/collections/base.py +415 -98
- htmlgraph/collections/bug.py +53 -0
- htmlgraph/collections/chore.py +53 -0
- htmlgraph/collections/epic.py +53 -0
- htmlgraph/collections/feature.py +12 -26
- htmlgraph/collections/insight.py +100 -0
- htmlgraph/collections/metric.py +92 -0
- htmlgraph/collections/pattern.py +97 -0
- htmlgraph/collections/phase.py +53 -0
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +56 -16
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +511 -0
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +344 -0
- htmlgraph/converter.py +216 -28
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2406 -307
- 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 +19 -2
- htmlgraph/deploy.py +142 -125
- htmlgraph/deployment_models.py +474 -0
- 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 +182 -27
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +100 -52
- htmlgraph/event_migration.py +13 -4
- htmlgraph/exceptions.py +49 -0
- htmlgraph/file_watcher.py +101 -28
- htmlgraph/find_api.py +75 -63
- htmlgraph/git_events.py +145 -63
- htmlgraph/graph.py +1122 -106
- 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 +45 -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 +1314 -0
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/hooks-config.example.json +12 -0
- htmlgraph/hooks/installer.py +343 -0
- htmlgraph/hooks/orchestrator.py +674 -0
- htmlgraph/hooks/orchestrator_reflector.py +223 -0
- htmlgraph/hooks/post-checkout.sh +28 -0
- htmlgraph/hooks/post-commit.sh +24 -0
- htmlgraph/hooks/post-merge.sh +26 -0
- htmlgraph/hooks/post_tool_use_failure.py +273 -0
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +408 -0
- htmlgraph/hooks/pre-commit.sh +94 -0
- htmlgraph/hooks/pre-push.sh +28 -0
- htmlgraph/hooks/pretooluse.py +819 -0
- 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 +255 -0
- htmlgraph/hooks/task_validator.py +177 -0
- htmlgraph/hooks/validator.py +628 -0
- htmlgraph/ids.py +41 -27
- htmlgraph/index.d.ts +286 -0
- htmlgraph/learning.py +767 -0
- htmlgraph/mcp_server.py +69 -23
- htmlgraph/models.py +1586 -87
- 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/task_coordination.py +343 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +669 -0
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +328 -0
- htmlgraph/orchestrator_validator.py +133 -0
- htmlgraph/parallel.py +646 -0
- htmlgraph/parser.py +160 -35
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/planning.py +147 -52
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +109 -72
- 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/routing.py +8 -19
- htmlgraph/scripts/deploy.py +1 -2
- 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 +685 -180
- htmlgraph/services/__init__.py +10 -0
- htmlgraph/services/claiming.py +199 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +1392 -175
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +201 -0
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/setup.py +34 -17
- htmlgraph/spike_index.py +143 -0
- htmlgraph/sync_docs.py +12 -15
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph/templates/GEMINI.md.template +87 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +146 -15
- htmlgraph/track_manager.py +69 -21
- htmlgraph/transcript.py +890 -0
- htmlgraph/transcript_analytics.py +699 -0
- htmlgraph/types.py +323 -0
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +8 -5
- htmlgraph/work_type_utils.py +3 -2
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
- htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -2688
- htmlgraph/sdk.py +0 -709
- htmlgraph-0.9.3.dist-info/RECORD +0 -61
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +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
|
+
)
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""Intelligent model selection for task routing.
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
This module provides functionality to select the best AI model for a given task
|
|
8
|
+
based on task type, complexity, and budget constraints.
|
|
9
|
+
|
|
10
|
+
Model Selection Strategy:
|
|
11
|
+
- Exploration: Use Gemini (free tier) for cost-effective research
|
|
12
|
+
- Debugging: Use Claude Sonnet (high context) for complex error analysis
|
|
13
|
+
- Implementation: Use Codex (programming specialized) for code generation
|
|
14
|
+
- Quality: Use Claude Haiku (fast) for linting and formatting
|
|
15
|
+
|
|
16
|
+
Fallback Chain:
|
|
17
|
+
Each model has fallback options if primary model is unavailable:
|
|
18
|
+
- Gemini → Claude Haiku → Claude Sonnet
|
|
19
|
+
- Codex → Claude Sonnet
|
|
20
|
+
- Copilot → Claude Sonnet
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from enum import Enum
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TaskType(str, Enum):
|
|
27
|
+
"""Task classification types."""
|
|
28
|
+
|
|
29
|
+
EXPLORATION = "exploration"
|
|
30
|
+
DEBUGGING = "debugging"
|
|
31
|
+
IMPLEMENTATION = "implementation"
|
|
32
|
+
QUALITY = "quality"
|
|
33
|
+
GENERAL = "general"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ComplexityLevel(str, Enum):
|
|
37
|
+
"""Complexity assessment levels."""
|
|
38
|
+
|
|
39
|
+
LOW = "low"
|
|
40
|
+
MEDIUM = "medium"
|
|
41
|
+
HIGH = "high"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BudgetMode(str, Enum):
|
|
45
|
+
"""Budget constraints."""
|
|
46
|
+
|
|
47
|
+
FREE = "free" # Use only free models
|
|
48
|
+
BALANCED = "balanced" # Balance cost and quality
|
|
49
|
+
QUALITY = "quality" # Prioritize best model
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ModelSelection:
|
|
53
|
+
"""Intelligent model selection engine."""
|
|
54
|
+
|
|
55
|
+
# Decision matrix: (task_type, complexity, budget) -> model
|
|
56
|
+
DECISION_MATRIX = {
|
|
57
|
+
# Exploration tasks - prioritize free/cheap options
|
|
58
|
+
(TaskType.EXPLORATION, ComplexityLevel.LOW, BudgetMode.FREE): "gemini",
|
|
59
|
+
(TaskType.EXPLORATION, ComplexityLevel.MEDIUM, BudgetMode.FREE): "gemini",
|
|
60
|
+
(TaskType.EXPLORATION, ComplexityLevel.HIGH, BudgetMode.FREE): "gemini",
|
|
61
|
+
(TaskType.EXPLORATION, ComplexityLevel.LOW, BudgetMode.BALANCED): "gemini",
|
|
62
|
+
(TaskType.EXPLORATION, ComplexityLevel.MEDIUM, BudgetMode.BALANCED): "gemini",
|
|
63
|
+
(
|
|
64
|
+
TaskType.EXPLORATION,
|
|
65
|
+
ComplexityLevel.HIGH,
|
|
66
|
+
BudgetMode.BALANCED,
|
|
67
|
+
): "claude-sonnet",
|
|
68
|
+
(
|
|
69
|
+
TaskType.EXPLORATION,
|
|
70
|
+
ComplexityLevel.LOW,
|
|
71
|
+
BudgetMode.QUALITY,
|
|
72
|
+
): "claude-sonnet",
|
|
73
|
+
(
|
|
74
|
+
TaskType.EXPLORATION,
|
|
75
|
+
ComplexityLevel.MEDIUM,
|
|
76
|
+
BudgetMode.QUALITY,
|
|
77
|
+
): "claude-sonnet",
|
|
78
|
+
(TaskType.EXPLORATION, ComplexityLevel.HIGH, BudgetMode.QUALITY): "claude-opus",
|
|
79
|
+
# Debugging tasks - need strong reasoning
|
|
80
|
+
(TaskType.DEBUGGING, ComplexityLevel.LOW, BudgetMode.FREE): "claude-haiku",
|
|
81
|
+
(TaskType.DEBUGGING, ComplexityLevel.MEDIUM, BudgetMode.FREE): "claude-haiku",
|
|
82
|
+
(TaskType.DEBUGGING, ComplexityLevel.HIGH, BudgetMode.FREE): "claude-haiku",
|
|
83
|
+
(TaskType.DEBUGGING, ComplexityLevel.LOW, BudgetMode.BALANCED): "claude-sonnet",
|
|
84
|
+
(
|
|
85
|
+
TaskType.DEBUGGING,
|
|
86
|
+
ComplexityLevel.MEDIUM,
|
|
87
|
+
BudgetMode.BALANCED,
|
|
88
|
+
): "claude-sonnet",
|
|
89
|
+
(TaskType.DEBUGGING, ComplexityLevel.HIGH, BudgetMode.BALANCED): "claude-opus",
|
|
90
|
+
(TaskType.DEBUGGING, ComplexityLevel.LOW, BudgetMode.QUALITY): "claude-opus",
|
|
91
|
+
(TaskType.DEBUGGING, ComplexityLevel.MEDIUM, BudgetMode.QUALITY): "claude-opus",
|
|
92
|
+
(TaskType.DEBUGGING, ComplexityLevel.HIGH, BudgetMode.QUALITY): "claude-opus",
|
|
93
|
+
# Implementation tasks - balance speed and quality
|
|
94
|
+
(TaskType.IMPLEMENTATION, ComplexityLevel.LOW, BudgetMode.FREE): "claude-haiku",
|
|
95
|
+
(
|
|
96
|
+
TaskType.IMPLEMENTATION,
|
|
97
|
+
ComplexityLevel.MEDIUM,
|
|
98
|
+
BudgetMode.FREE,
|
|
99
|
+
): "claude-haiku",
|
|
100
|
+
(
|
|
101
|
+
TaskType.IMPLEMENTATION,
|
|
102
|
+
ComplexityLevel.HIGH,
|
|
103
|
+
BudgetMode.FREE,
|
|
104
|
+
): "claude-haiku",
|
|
105
|
+
(TaskType.IMPLEMENTATION, ComplexityLevel.LOW, BudgetMode.BALANCED): "codex",
|
|
106
|
+
(TaskType.IMPLEMENTATION, ComplexityLevel.MEDIUM, BudgetMode.BALANCED): "codex",
|
|
107
|
+
(
|
|
108
|
+
TaskType.IMPLEMENTATION,
|
|
109
|
+
ComplexityLevel.HIGH,
|
|
110
|
+
BudgetMode.BALANCED,
|
|
111
|
+
): "claude-opus",
|
|
112
|
+
(
|
|
113
|
+
TaskType.IMPLEMENTATION,
|
|
114
|
+
ComplexityLevel.LOW,
|
|
115
|
+
BudgetMode.QUALITY,
|
|
116
|
+
): "claude-opus",
|
|
117
|
+
(
|
|
118
|
+
TaskType.IMPLEMENTATION,
|
|
119
|
+
ComplexityLevel.MEDIUM,
|
|
120
|
+
BudgetMode.QUALITY,
|
|
121
|
+
): "claude-opus",
|
|
122
|
+
(
|
|
123
|
+
TaskType.IMPLEMENTATION,
|
|
124
|
+
ComplexityLevel.HIGH,
|
|
125
|
+
BudgetMode.QUALITY,
|
|
126
|
+
): "claude-opus",
|
|
127
|
+
# Quality tasks - fast and cheap
|
|
128
|
+
(TaskType.QUALITY, ComplexityLevel.LOW, BudgetMode.FREE): "claude-haiku",
|
|
129
|
+
(TaskType.QUALITY, ComplexityLevel.MEDIUM, BudgetMode.FREE): "claude-haiku",
|
|
130
|
+
(TaskType.QUALITY, ComplexityLevel.HIGH, BudgetMode.FREE): "claude-haiku",
|
|
131
|
+
(TaskType.QUALITY, ComplexityLevel.LOW, BudgetMode.BALANCED): "claude-haiku",
|
|
132
|
+
(
|
|
133
|
+
TaskType.QUALITY,
|
|
134
|
+
ComplexityLevel.MEDIUM,
|
|
135
|
+
BudgetMode.BALANCED,
|
|
136
|
+
): "claude-sonnet",
|
|
137
|
+
(TaskType.QUALITY, ComplexityLevel.HIGH, BudgetMode.BALANCED): "claude-sonnet",
|
|
138
|
+
(TaskType.QUALITY, ComplexityLevel.LOW, BudgetMode.QUALITY): "claude-sonnet",
|
|
139
|
+
(TaskType.QUALITY, ComplexityLevel.MEDIUM, BudgetMode.QUALITY): "claude-opus",
|
|
140
|
+
(TaskType.QUALITY, ComplexityLevel.HIGH, BudgetMode.QUALITY): "claude-opus",
|
|
141
|
+
# General tasks - safe defaults
|
|
142
|
+
(TaskType.GENERAL, ComplexityLevel.LOW, BudgetMode.FREE): "claude-haiku",
|
|
143
|
+
(TaskType.GENERAL, ComplexityLevel.MEDIUM, BudgetMode.FREE): "claude-haiku",
|
|
144
|
+
(TaskType.GENERAL, ComplexityLevel.HIGH, BudgetMode.FREE): "claude-haiku",
|
|
145
|
+
(TaskType.GENERAL, ComplexityLevel.LOW, BudgetMode.BALANCED): "claude-sonnet",
|
|
146
|
+
(
|
|
147
|
+
TaskType.GENERAL,
|
|
148
|
+
ComplexityLevel.MEDIUM,
|
|
149
|
+
BudgetMode.BALANCED,
|
|
150
|
+
): "claude-sonnet",
|
|
151
|
+
(TaskType.GENERAL, ComplexityLevel.HIGH, BudgetMode.BALANCED): "claude-opus",
|
|
152
|
+
(TaskType.GENERAL, ComplexityLevel.LOW, BudgetMode.QUALITY): "claude-opus",
|
|
153
|
+
(TaskType.GENERAL, ComplexityLevel.MEDIUM, BudgetMode.QUALITY): "claude-opus",
|
|
154
|
+
(TaskType.GENERAL, ComplexityLevel.HIGH, BudgetMode.QUALITY): "claude-opus",
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Fallback chains for when primary model is unavailable
|
|
158
|
+
FALLBACK_CHAINS = {
|
|
159
|
+
"gemini": ["claude-haiku", "claude-sonnet", "claude-opus"],
|
|
160
|
+
"codex": ["claude-sonnet", "claude-opus"],
|
|
161
|
+
"copilot": ["claude-sonnet", "claude-opus"],
|
|
162
|
+
"claude-haiku": ["claude-sonnet", "claude-opus"],
|
|
163
|
+
"claude-sonnet": ["claude-opus", "claude-haiku"],
|
|
164
|
+
"claude-opus": ["claude-sonnet", "claude-haiku"],
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def select_model(
|
|
169
|
+
task_type: str | TaskType,
|
|
170
|
+
complexity: str | ComplexityLevel = "medium",
|
|
171
|
+
budget: str | BudgetMode = "balanced",
|
|
172
|
+
) -> str:
|
|
173
|
+
"""
|
|
174
|
+
Select best model for the given task parameters.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
task_type: Type of task (exploration, debugging, implementation, quality, general)
|
|
178
|
+
complexity: Task complexity level (low, medium, high). Default: medium
|
|
179
|
+
budget: Budget mode (free, balanced, quality). Default: balanced
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Model name (e.g., "claude-sonnet", "gemini")
|
|
183
|
+
|
|
184
|
+
Example:
|
|
185
|
+
>>> model = ModelSelection.select_model("implementation", "high", "balanced")
|
|
186
|
+
>>> logger.info("%s", model)
|
|
187
|
+
'claude-opus'
|
|
188
|
+
"""
|
|
189
|
+
# Normalize inputs
|
|
190
|
+
if isinstance(task_type, str):
|
|
191
|
+
try:
|
|
192
|
+
task_type = TaskType(task_type)
|
|
193
|
+
except ValueError:
|
|
194
|
+
task_type = TaskType.GENERAL
|
|
195
|
+
|
|
196
|
+
if isinstance(complexity, str):
|
|
197
|
+
try:
|
|
198
|
+
complexity = ComplexityLevel(complexity)
|
|
199
|
+
except ValueError:
|
|
200
|
+
complexity = ComplexityLevel.MEDIUM
|
|
201
|
+
|
|
202
|
+
if isinstance(budget, str):
|
|
203
|
+
try:
|
|
204
|
+
budget = BudgetMode(budget)
|
|
205
|
+
except ValueError:
|
|
206
|
+
budget = BudgetMode.BALANCED
|
|
207
|
+
|
|
208
|
+
# Look up in decision matrix
|
|
209
|
+
key = (task_type, complexity, budget)
|
|
210
|
+
return ModelSelection.DECISION_MATRIX.get(key, "claude-sonnet")
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def get_fallback_chain(primary_model: str) -> list[str]:
|
|
214
|
+
"""
|
|
215
|
+
Get fallback models if primary model is unavailable.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
primary_model: Primary model name
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
List of fallback models in order of preference
|
|
222
|
+
|
|
223
|
+
Example:
|
|
224
|
+
>>> fallbacks = ModelSelection.get_fallback_chain("gemini")
|
|
225
|
+
>>> logger.info("%s", fallbacks)
|
|
226
|
+
['claude-haiku', 'claude-sonnet', 'claude-opus']
|
|
227
|
+
"""
|
|
228
|
+
return ModelSelection.FALLBACK_CHAINS.get(primary_model, ["claude-sonnet"])
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def estimate_tokens(
|
|
232
|
+
task_description: str, complexity: str | ComplexityLevel = "medium"
|
|
233
|
+
) -> int:
|
|
234
|
+
"""
|
|
235
|
+
Estimate token usage for a task.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
task_description: Description of the task
|
|
239
|
+
complexity: Task complexity level
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Estimated tokens for the task
|
|
243
|
+
|
|
244
|
+
Estimation formula:
|
|
245
|
+
- Low complexity: ~500-1000 tokens
|
|
246
|
+
- Medium complexity: ~1000-5000 tokens
|
|
247
|
+
- High complexity: ~5000-20000 tokens
|
|
248
|
+
"""
|
|
249
|
+
if isinstance(complexity, str):
|
|
250
|
+
try:
|
|
251
|
+
complexity = ComplexityLevel(complexity)
|
|
252
|
+
except ValueError:
|
|
253
|
+
complexity = ComplexityLevel.MEDIUM
|
|
254
|
+
|
|
255
|
+
# Base estimate on description length
|
|
256
|
+
description_tokens = len(task_description.split()) * 1.3 # ~1.3 tokens per word
|
|
257
|
+
|
|
258
|
+
# Add complexity multiplier
|
|
259
|
+
multipliers = {
|
|
260
|
+
ComplexityLevel.LOW: 1.0,
|
|
261
|
+
ComplexityLevel.MEDIUM: 2.0,
|
|
262
|
+
ComplexityLevel.HIGH: 5.0,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
multiplier = multipliers.get(complexity, 2.0)
|
|
266
|
+
return int(description_tokens * multiplier)
|
|
267
|
+
|
|
268
|
+
@staticmethod
|
|
269
|
+
def is_model_available(model: str) -> bool:
|
|
270
|
+
"""
|
|
271
|
+
Check if a model is available (basic check).
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
model: Model name to check
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
True if model is known, False otherwise
|
|
278
|
+
|
|
279
|
+
Note:
|
|
280
|
+
This is a simple availability check. For actual availability,
|
|
281
|
+
you should check Claude CLI, Gemini CLI, etc.
|
|
282
|
+
"""
|
|
283
|
+
available_models = {
|
|
284
|
+
"gemini",
|
|
285
|
+
"codex",
|
|
286
|
+
"copilot",
|
|
287
|
+
"claude-haiku",
|
|
288
|
+
"claude-sonnet",
|
|
289
|
+
"claude-opus",
|
|
290
|
+
}
|
|
291
|
+
return model in available_models
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def select_model(
|
|
295
|
+
task_type: str = "general",
|
|
296
|
+
complexity: str = "medium",
|
|
297
|
+
budget: str = "balanced",
|
|
298
|
+
) -> str:
|
|
299
|
+
"""
|
|
300
|
+
Convenience function for model selection.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
task_type: Type of task. Default: general
|
|
304
|
+
complexity: Complexity level. Default: medium
|
|
305
|
+
budget: Budget mode. Default: balanced
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Recommended model name
|
|
309
|
+
|
|
310
|
+
Example:
|
|
311
|
+
>>> model = select_model("implementation", "high")
|
|
312
|
+
>>> logger.info("%s", model)
|
|
313
|
+
"""
|
|
314
|
+
return ModelSelection.select_model(task_type, complexity, budget)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def get_fallback_chain(model: str) -> list[str]:
|
|
318
|
+
"""
|
|
319
|
+
Convenience function for getting fallback models.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
model: Primary model name
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
List of fallback models
|
|
326
|
+
"""
|
|
327
|
+
return ModelSelection.get_fallback_chain(model)
|