cct-cli 0.7.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT v0.8.a — Frame Priority Engine (spec 9).
|
|
3
|
+
|
|
4
|
+
Scores frames/events for AI analysis priority:
|
|
5
|
+
|
|
6
|
+
score = visual_change + interaction_event + annotation_event
|
|
7
|
+
+ user_request + error_signal + AI_requested_frame
|
|
8
|
+
|
|
9
|
+
Only high-value frames are sent to expensive multimodal reasoning.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PriorityLevel(str, Enum):
|
|
21
|
+
SKIP = "skip"
|
|
22
|
+
LOW = "low"
|
|
23
|
+
NORMAL = "normal"
|
|
24
|
+
HIGH = "high"
|
|
25
|
+
CRITICAL = "critical"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class PriorityScore:
|
|
30
|
+
total: float
|
|
31
|
+
level: PriorityLevel
|
|
32
|
+
breakdown: Dict[str, float]
|
|
33
|
+
should_send: bool
|
|
34
|
+
reasons: List[str]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FramePriorityEngine:
|
|
38
|
+
"""
|
|
39
|
+
Event-driven priority scoring. Call score_event() for each signal;
|
|
40
|
+
call score_frame() to combine into a final decision for a candidate frame.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
# Weights — tuned so obvious signals dominate redundant frame dribbles
|
|
44
|
+
WEIGHTS = {
|
|
45
|
+
"visual_change_none": 0,
|
|
46
|
+
"visual_change_minor": 5,
|
|
47
|
+
"visual_change_moderate": 15,
|
|
48
|
+
"visual_change_major": 30,
|
|
49
|
+
"visual_change_critical": 50,
|
|
50
|
+
"cursor_move": 2,
|
|
51
|
+
"cursor_dwell": 12,
|
|
52
|
+
"click": 18,
|
|
53
|
+
"annotation": 25,
|
|
54
|
+
"scroll": 4,
|
|
55
|
+
"user_chat": 35,
|
|
56
|
+
"user_analyze_request": 40,
|
|
57
|
+
"ai_requested_frame": 45,
|
|
58
|
+
"error_signal": 40,
|
|
59
|
+
"preview_change": 20,
|
|
60
|
+
"ui_state_change": 10,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Thresholds for final level
|
|
64
|
+
THRESHOLDS = {
|
|
65
|
+
PriorityLevel.SKIP: 0,
|
|
66
|
+
PriorityLevel.LOW: 5,
|
|
67
|
+
PriorityLevel.NORMAL: 15,
|
|
68
|
+
PriorityLevel.HIGH: 30,
|
|
69
|
+
PriorityLevel.CRITICAL: 45,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def __init__(self, high_threshold: float = 30.0):
|
|
73
|
+
self.high_threshold = high_threshold
|
|
74
|
+
self._pending_signals: List[Dict[str, Any]] = []
|
|
75
|
+
self._last_send_time: float = 0.0
|
|
76
|
+
self._min_send_interval: float = 1.0 # seconds, adaptive
|
|
77
|
+
self._consecutive_skips: int = 0
|
|
78
|
+
|
|
79
|
+
def add_signal(self, kind: str, value: float = 1.0, detail: str = ""):
|
|
80
|
+
"""Queue a signal; it will be consumed on next score_frame()."""
|
|
81
|
+
self._pending_signals.append({
|
|
82
|
+
"kind": kind,
|
|
83
|
+
"value": value,
|
|
84
|
+
"detail": detail,
|
|
85
|
+
"ts": time.time(),
|
|
86
|
+
})
|
|
87
|
+
# keep bounded
|
|
88
|
+
if len(self._pending_signals) > 100:
|
|
89
|
+
self._pending_signals = self._pending_signals[-50:]
|
|
90
|
+
|
|
91
|
+
# Convenience helpers
|
|
92
|
+
def signal_visual_change(self, level: str):
|
|
93
|
+
key = f"visual_change_{level}"
|
|
94
|
+
self.add_signal(key, detail=level)
|
|
95
|
+
|
|
96
|
+
def signal_cursor_move(self): self.add_signal("cursor_move")
|
|
97
|
+
def signal_cursor_dwell(self): self.add_signal("cursor_dwell")
|
|
98
|
+
def signal_click(self): self.add_signal("click")
|
|
99
|
+
def signal_annotation(self, tool: str = ""): self.add_signal("annotation", detail=tool)
|
|
100
|
+
def signal_scroll(self): self.add_signal("scroll")
|
|
101
|
+
def signal_user_chat(self): self.add_signal("user_chat")
|
|
102
|
+
def signal_user_analyze(self): self.add_signal("user_analyze_request")
|
|
103
|
+
def signal_ai_request(self): self.add_signal("ai_requested_frame")
|
|
104
|
+
def signal_error(self, msg: str = ""): self.add_signal("error_signal", detail=msg)
|
|
105
|
+
def signal_preview_change(self): self.add_signal("preview_change")
|
|
106
|
+
def signal_ui_state_change(self): self.add_signal("ui_state_change")
|
|
107
|
+
|
|
108
|
+
def score_frame(self,
|
|
109
|
+
visual_change: str = "none",
|
|
110
|
+
has_annotation: bool = False,
|
|
111
|
+
has_cursor_event: bool = False,
|
|
112
|
+
has_user_request: bool = False,
|
|
113
|
+
has_error: bool = False,
|
|
114
|
+
ai_requested: bool = False,
|
|
115
|
+
force: bool = False) -> PriorityScore:
|
|
116
|
+
"""
|
|
117
|
+
Score a candidate frame. Also drains pending signals.
|
|
118
|
+
Set force=True for explicit user Analyze actions (always send).
|
|
119
|
+
"""
|
|
120
|
+
breakdown: Dict[str, float] = {}
|
|
121
|
+
reasons: List[str] = []
|
|
122
|
+
total = 0.0
|
|
123
|
+
|
|
124
|
+
# visual change component
|
|
125
|
+
vc_key = f"visual_change_{visual_change}"
|
|
126
|
+
vc_score = self.WEIGHTS.get(vc_key, 0)
|
|
127
|
+
if vc_score:
|
|
128
|
+
breakdown["visual_change"] = vc_score
|
|
129
|
+
reasons.append(f"visual:{visual_change} +{vc_score}")
|
|
130
|
+
total += vc_score
|
|
131
|
+
|
|
132
|
+
# pending signals
|
|
133
|
+
for sig in self._pending_signals:
|
|
134
|
+
w = self.WEIGHTS.get(sig["kind"], 0)
|
|
135
|
+
if w:
|
|
136
|
+
breakdown[sig["kind"]] = breakdown.get(sig["kind"], 0) + w
|
|
137
|
+
reasons.append(f"{sig['kind']} +{w}")
|
|
138
|
+
total += w
|
|
139
|
+
self._pending_signals.clear()
|
|
140
|
+
|
|
141
|
+
# explicit flags (additive if not already counted via signals)
|
|
142
|
+
if has_annotation and "annotation" not in breakdown:
|
|
143
|
+
w = self.WEIGHTS["annotation"]
|
|
144
|
+
breakdown["annotation"] = w
|
|
145
|
+
reasons.append(f"annotation +{w}")
|
|
146
|
+
total += w
|
|
147
|
+
if has_cursor_event and "cursor_move" not in breakdown and "cursor_dwell" not in breakdown:
|
|
148
|
+
w = self.WEIGHTS["cursor_move"]
|
|
149
|
+
breakdown["cursor_event"] = w
|
|
150
|
+
total += w
|
|
151
|
+
if has_user_request and "user_chat" not in breakdown:
|
|
152
|
+
w = self.WEIGHTS["user_chat"]
|
|
153
|
+
breakdown["user_request"] = w
|
|
154
|
+
reasons.append(f"user_request +{w}")
|
|
155
|
+
total += w
|
|
156
|
+
if has_error:
|
|
157
|
+
w = self.WEIGHTS["error_signal"]
|
|
158
|
+
breakdown["error_signal"] = breakdown.get("error_signal", 0) + w
|
|
159
|
+
reasons.append(f"error +{w}")
|
|
160
|
+
total += w
|
|
161
|
+
if ai_requested:
|
|
162
|
+
w = self.WEIGHTS["ai_requested_frame"]
|
|
163
|
+
breakdown["ai_requested"] = w
|
|
164
|
+
reasons.append(f"ai_requested +{w}")
|
|
165
|
+
total += w
|
|
166
|
+
|
|
167
|
+
# level
|
|
168
|
+
if force:
|
|
169
|
+
level = PriorityLevel.CRITICAL
|
|
170
|
+
should_send = True
|
|
171
|
+
elif total >= self.THRESHOLDS[PriorityLevel.CRITICAL]:
|
|
172
|
+
level = PriorityLevel.CRITICAL
|
|
173
|
+
should_send = True
|
|
174
|
+
elif total >= self.THRESHOLDS[PriorityLevel.HIGH]:
|
|
175
|
+
level = PriorityLevel.HIGH
|
|
176
|
+
should_send = True
|
|
177
|
+
elif total >= self.THRESHOLDS[PriorityLevel.NORMAL]:
|
|
178
|
+
level = PriorityLevel.NORMAL
|
|
179
|
+
# throttle normal frames
|
|
180
|
+
should_send = (time.time() - self._last_send_time) >= self._min_send_interval
|
|
181
|
+
if not should_send:
|
|
182
|
+
reasons.append("throttled (normal priority, interval)")
|
|
183
|
+
elif total >= self.THRESHOLDS[PriorityLevel.LOW]:
|
|
184
|
+
level = PriorityLevel.LOW
|
|
185
|
+
should_send = False
|
|
186
|
+
reasons.append("low priority — skipped")
|
|
187
|
+
else:
|
|
188
|
+
level = PriorityLevel.SKIP
|
|
189
|
+
should_send = False
|
|
190
|
+
|
|
191
|
+
if should_send:
|
|
192
|
+
self._last_send_time = time.time()
|
|
193
|
+
self._consecutive_skips = 0
|
|
194
|
+
else:
|
|
195
|
+
self._consecutive_skips += 1
|
|
196
|
+
# anti-starvation: force send after many skips if we have moderate visual change
|
|
197
|
+
if self._consecutive_skips >= 10 and visual_change in ("moderate", "major", "critical"):
|
|
198
|
+
should_send = True
|
|
199
|
+
level = PriorityLevel.NORMAL
|
|
200
|
+
reasons.append("anti-starvation override")
|
|
201
|
+
self._consecutive_skips = 0
|
|
202
|
+
self._last_send_time = time.time()
|
|
203
|
+
|
|
204
|
+
return PriorityScore(
|
|
205
|
+
total=round(total, 1),
|
|
206
|
+
level=level,
|
|
207
|
+
breakdown=breakdown,
|
|
208
|
+
should_send=should_send,
|
|
209
|
+
reasons=reasons,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def set_min_interval(self, seconds: float):
|
|
213
|
+
self._min_send_interval = max(0.2, seconds)
|
|
214
|
+
|
|
215
|
+
def reset(self):
|
|
216
|
+
self._pending_signals.clear()
|
|
217
|
+
self._last_send_time = 0.0
|
|
218
|
+
self._consecutive_skips = 0
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT v0.8.a — Vision Provider Router.
|
|
3
|
+
|
|
4
|
+
Capability-aware routing for vision requests. Routes vision analysis
|
|
5
|
+
to the best available provider based on capabilities, cost, and quality.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class VisionCapability:
|
|
17
|
+
vision: bool = False
|
|
18
|
+
max_image_tokens: int = 0
|
|
19
|
+
supported_formats: tuple = ("png", "jpeg", "gif", "webp")
|
|
20
|
+
supports_video: bool = False
|
|
21
|
+
supports_audio: bool = False
|
|
22
|
+
supports_streaming: bool = False
|
|
23
|
+
cost_per_image_token: float = 0.0
|
|
24
|
+
quality_score: float = 0.0
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
27
|
+
return {
|
|
28
|
+
"vision": self.vision,
|
|
29
|
+
"max_image_tokens": self.max_image_tokens,
|
|
30
|
+
"supported_formats": list(self.supported_formats),
|
|
31
|
+
"supports_video": self.supports_video,
|
|
32
|
+
"supports_audio": self.supports_audio,
|
|
33
|
+
"supports_streaming": self.supports_streaming,
|
|
34
|
+
"cost_per_image_token": self.cost_per_image_token,
|
|
35
|
+
"quality_score": self.quality_score,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
PROVIDER_VISION_CAPS: Dict[str, VisionCapability] = {
|
|
40
|
+
"openai": VisionCapability(
|
|
41
|
+
vision=True, max_image_tokens=8192,
|
|
42
|
+
supported_formats=("png", "jpeg", "gif", "webp"),
|
|
43
|
+
cost_per_image_token=0.01, quality_score=0.95,
|
|
44
|
+
),
|
|
45
|
+
"anthropic": VisionCapability(
|
|
46
|
+
vision=True, max_image_tokens=8192,
|
|
47
|
+
supported_formats=("png", "jpeg", "gif", "webp"),
|
|
48
|
+
cost_per_image_token=0.015, quality_score=0.93,
|
|
49
|
+
),
|
|
50
|
+
"google": VisionCapability(
|
|
51
|
+
vision=True, max_image_tokens=16384,
|
|
52
|
+
supported_formats=("png", "jpeg", "gif", "webp"),
|
|
53
|
+
cost_per_image_token=0.008, quality_score=0.90,
|
|
54
|
+
),
|
|
55
|
+
"ollama": VisionCapability(
|
|
56
|
+
vision=True, max_image_tokens=4096,
|
|
57
|
+
supported_formats=("png", "jpeg"),
|
|
58
|
+
cost_per_image_token=0.0, quality_score=0.70,
|
|
59
|
+
),
|
|
60
|
+
"groq": VisionCapability(
|
|
61
|
+
vision=True, max_image_tokens=4096,
|
|
62
|
+
supported_formats=("png", "jpeg", "webp"),
|
|
63
|
+
cost_per_image_token=0.003, quality_score=0.80,
|
|
64
|
+
),
|
|
65
|
+
"mistral": VisionCapability(
|
|
66
|
+
vision=True, max_image_tokens=8192,
|
|
67
|
+
supported_formats=("png", "jpeg", "gif", "webp"),
|
|
68
|
+
cost_per_image_token=0.01, quality_score=0.85,
|
|
69
|
+
),
|
|
70
|
+
"deepseek": VisionCapability(
|
|
71
|
+
vision=True, max_image_tokens=8192,
|
|
72
|
+
supported_formats=("png", "jpeg", "gif", "webp"),
|
|
73
|
+
cost_per_image_token=0.005, quality_score=0.82,
|
|
74
|
+
),
|
|
75
|
+
"together": VisionCapability(
|
|
76
|
+
vision=True, max_image_tokens=8192,
|
|
77
|
+
supported_formats=("png", "jpeg", "gif", "webp"),
|
|
78
|
+
cost_per_image_token=0.008, quality_score=0.85,
|
|
79
|
+
),
|
|
80
|
+
"openrouter": VisionCapability(
|
|
81
|
+
vision=True, max_image_tokens=16384,
|
|
82
|
+
supported_formats=("png", "jpeg", "gif", "webp"),
|
|
83
|
+
cost_per_image_token=0.01, quality_score=0.88,
|
|
84
|
+
),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class VisionProviderRouter:
|
|
89
|
+
"""Routes vision requests to the best available provider."""
|
|
90
|
+
|
|
91
|
+
def __init__(self, config: Optional[Dict] = None):
|
|
92
|
+
self._config = config or {}
|
|
93
|
+
self._fallback_order = [
|
|
94
|
+
"openai", "anthropic", "google", "deepseek",
|
|
95
|
+
"groq", "mistral", "together", "openrouter", "ollama",
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
def get_capabilities(self, provider: str) -> VisionCapability:
|
|
99
|
+
return PROVIDER_VISION_CAPS.get(provider, VisionCapability())
|
|
100
|
+
|
|
101
|
+
def find_vision_capable(self, config: Optional[Dict] = None) -> Optional[Tuple[Dict, VisionCapability]]:
|
|
102
|
+
cfg = config or self._config
|
|
103
|
+
provider = cfg.get("provider", "")
|
|
104
|
+
model = cfg.get("model", "")
|
|
105
|
+
|
|
106
|
+
caps = self.get_capabilities(provider)
|
|
107
|
+
if caps.vision:
|
|
108
|
+
return cfg, caps
|
|
109
|
+
|
|
110
|
+
for fallback_provider in self._fallback_order:
|
|
111
|
+
if fallback_provider == provider:
|
|
112
|
+
continue
|
|
113
|
+
fallback_caps = self.get_capabilities(fallback_provider)
|
|
114
|
+
if fallback_caps.vision:
|
|
115
|
+
fallback_config = dict(cfg)
|
|
116
|
+
fallback_config["provider"] = fallback_provider
|
|
117
|
+
return fallback_config, fallback_caps
|
|
118
|
+
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
def select_best_provider(self, image_size: int = 0,
|
|
122
|
+
need_video: bool = False,
|
|
123
|
+
prefer_free: bool = False) -> Optional[Tuple[str, VisionCapability]]:
|
|
124
|
+
candidates = []
|
|
125
|
+
for provider, caps in PROVIDER_VISION_CAPS.items():
|
|
126
|
+
if not caps.vision:
|
|
127
|
+
continue
|
|
128
|
+
if need_video and not caps.supports_video:
|
|
129
|
+
continue
|
|
130
|
+
if prefer_free and caps.cost_per_image_token > 0:
|
|
131
|
+
continue
|
|
132
|
+
if image_size > 0 and image_size > caps.max_image_tokens * 100:
|
|
133
|
+
continue
|
|
134
|
+
candidates.append((provider, caps))
|
|
135
|
+
|
|
136
|
+
if prefer_free:
|
|
137
|
+
candidates = [(p, c) for p, c in candidates if c.cost_per_image_token == 0]
|
|
138
|
+
|
|
139
|
+
if not candidates:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
candidates.sort(key=lambda x: (
|
|
143
|
+
-x[1].quality_score,
|
|
144
|
+
x[1].cost_per_image_token,
|
|
145
|
+
))
|
|
146
|
+
return candidates[0]
|
|
147
|
+
|
|
148
|
+
def route_vision_request(self, frame_b64: str,
|
|
149
|
+
frame_size_bytes: int = 0,
|
|
150
|
+
config: Optional[Dict] = None) -> Dict[str, Any]:
|
|
151
|
+
cfg = config or self._config
|
|
152
|
+
provider = cfg.get("provider", "")
|
|
153
|
+
caps = self.get_capabilities(provider)
|
|
154
|
+
|
|
155
|
+
result = {
|
|
156
|
+
"provider": provider,
|
|
157
|
+
"capabilities": caps.to_dict(),
|
|
158
|
+
"routed": caps.vision,
|
|
159
|
+
"fallback_used": False,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if not caps.vision:
|
|
163
|
+
route = self.find_vision_capable(cfg)
|
|
164
|
+
if route:
|
|
165
|
+
fallback_cfg, fallback_caps = route
|
|
166
|
+
result["provider"] = fallback_cfg.get("provider", "")
|
|
167
|
+
result["capabilities"] = fallback_caps.to_dict()
|
|
168
|
+
result["routed"] = True
|
|
169
|
+
result["fallback_used"] = True
|
|
170
|
+
result["original_provider"] = provider
|
|
171
|
+
else:
|
|
172
|
+
result["error"] = "No vision-capable provider available"
|
|
173
|
+
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
def get_all_capabilities(self) -> Dict[str, Dict]:
|
|
177
|
+
return {
|
|
178
|
+
provider: caps.to_dict()
|
|
179
|
+
for provider, caps in PROVIDER_VISION_CAPS.items()
|
|
180
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT v0.8.a — Privacy & Safety Controls.
|
|
3
|
+
|
|
4
|
+
Configurable privacy filtering, blocked region detection,
|
|
5
|
+
data retention limits, and safety safeguards.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class SafetyConfig:
|
|
17
|
+
max_session_minutes: int = 60
|
|
18
|
+
max_frames_per_session: int = 500
|
|
19
|
+
max_total_frames_per_day: int = 5000
|
|
20
|
+
block_password_fields: bool = True
|
|
21
|
+
block_financial_data: bool = False
|
|
22
|
+
block_pii_detection: bool = True
|
|
23
|
+
blur_sensitive_regions: bool = True
|
|
24
|
+
auto_stop_on_error: bool = True
|
|
25
|
+
require_confirmation_for_persist: bool = True
|
|
26
|
+
log_all_access: bool = True
|
|
27
|
+
max_context_history: int = 20
|
|
28
|
+
data_retention_hours: int = 24
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
31
|
+
return {
|
|
32
|
+
"max_session_minutes": self.max_session_minutes,
|
|
33
|
+
"max_frames_per_session": self.max_frames_per_session,
|
|
34
|
+
"max_total_frames_per_day": self.max_total_frames_per_day,
|
|
35
|
+
"block_password_fields": self.block_password_fields,
|
|
36
|
+
"block_financial_data": self.block_financial_data,
|
|
37
|
+
"block_pii_detection": self.block_pii_detection,
|
|
38
|
+
"blur_sensitive_regions": self.blur_sensitive_regions,
|
|
39
|
+
"auto_stop_on_error": self.auto_stop_on_error,
|
|
40
|
+
"require_confirmation_for_persist": self.require_confirmation_for_persist,
|
|
41
|
+
"log_all_access": self.log_all_access,
|
|
42
|
+
"max_context_history": self.max_context_history,
|
|
43
|
+
"data_retention_hours": self.data_retention_hours,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PrivacyController:
|
|
48
|
+
"""Enforces privacy rules on captured frames and context."""
|
|
49
|
+
|
|
50
|
+
SENSITIVE_KEYWORDS = [
|
|
51
|
+
"password", "passwd", "secret", "token", "api_key", "apikey",
|
|
52
|
+
"authorization", "credit_card", "ssn", "social_security",
|
|
53
|
+
"bank_account", "routing_number", "pin_code",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
def __init__(self, config: Optional[SafetyConfig] = None):
|
|
57
|
+
self.config = config or SafetyConfig()
|
|
58
|
+
self._access_log: List[Dict] = []
|
|
59
|
+
self._daily_frame_count = 0
|
|
60
|
+
self._day_start = self._today()
|
|
61
|
+
|
|
62
|
+
def _today(self) -> str:
|
|
63
|
+
return time.strftime("%Y-%m-%d")
|
|
64
|
+
|
|
65
|
+
def _reset_daily_if_needed(self):
|
|
66
|
+
today = self._today()
|
|
67
|
+
if today != self._day_start:
|
|
68
|
+
self._daily_frame_count = 0
|
|
69
|
+
self._day_start = today
|
|
70
|
+
|
|
71
|
+
def validate_config(self, config: Any) -> None:
|
|
72
|
+
if hasattr(config, "fps") and config.fps > 10:
|
|
73
|
+
raise ValueError("Frame rate exceeds safety limit (10 fps max)")
|
|
74
|
+
if hasattr(config, "max_frames") and config.max_frames > self.config.max_frames_per_session:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Max frames exceeds safety limit "
|
|
77
|
+
f"({self.config.max_frames_per_session})"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def is_blocked_region(self, frame_b64: str) -> bool:
|
|
81
|
+
if not self.config.block_pii_detection:
|
|
82
|
+
return False
|
|
83
|
+
if len(frame_b64) < 100:
|
|
84
|
+
return False
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
def check_text_sensitivity(self, text: str) -> Dict[str, Any]:
|
|
88
|
+
text_lower = text.lower()
|
|
89
|
+
blocked = [
|
|
90
|
+
kw for kw in self.SENSITIVE_KEYWORDS
|
|
91
|
+
if kw in text_lower
|
|
92
|
+
]
|
|
93
|
+
return {
|
|
94
|
+
"sensitive": bool(blocked),
|
|
95
|
+
"keywords_found": blocked,
|
|
96
|
+
"recommendation": "block" if blocked else "allow",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
def filter_frame_metadata(self, meta: Dict[str, Any]) -> Dict[str, Any]:
|
|
100
|
+
filtered = dict(meta)
|
|
101
|
+
if self.config.block_password_fields:
|
|
102
|
+
if "active_element" in filtered:
|
|
103
|
+
el = filtered["active_element"].lower()
|
|
104
|
+
if any(kw in el for kw in ("password", "secret", "pin")):
|
|
105
|
+
filtered["active_element"] = "[REDACTED]"
|
|
106
|
+
return filtered
|
|
107
|
+
|
|
108
|
+
def log_access(self, action: str, resource: str, details: Optional[Dict] = None):
|
|
109
|
+
if not self.config.log_all_access:
|
|
110
|
+
return
|
|
111
|
+
entry = {
|
|
112
|
+
"ts": time.time(),
|
|
113
|
+
"action": action,
|
|
114
|
+
"resource": resource,
|
|
115
|
+
}
|
|
116
|
+
if details:
|
|
117
|
+
entry.update(details)
|
|
118
|
+
self._access_log.append(entry)
|
|
119
|
+
if len(self._access_log) > 10000:
|
|
120
|
+
self._access_log = self._access_log[-5000:]
|
|
121
|
+
|
|
122
|
+
def can_capture(self) -> Tuple[bool, str]:
|
|
123
|
+
self._reset_daily_if_needed()
|
|
124
|
+
if self._daily_frame_count >= self.config.max_total_frames_per_day:
|
|
125
|
+
return False, "Daily frame limit reached"
|
|
126
|
+
return True, ""
|
|
127
|
+
|
|
128
|
+
def record_capture(self):
|
|
129
|
+
self._reset_daily_if_needed()
|
|
130
|
+
self._daily_frame_count += 1
|
|
131
|
+
|
|
132
|
+
def can_persist(self) -> Tuple[bool, str]:
|
|
133
|
+
if self.config.require_confirmation_for_persist:
|
|
134
|
+
return True, "Confirmation required"
|
|
135
|
+
return True, ""
|
|
136
|
+
|
|
137
|
+
def get_access_log(self, limit: int = 100) -> List[Dict]:
|
|
138
|
+
return self._access_log[-limit:]
|
|
139
|
+
|
|
140
|
+
def get_privacy_summary(self) -> Dict[str, Any]:
|
|
141
|
+
return {
|
|
142
|
+
"daily_frames": self._daily_frame_count,
|
|
143
|
+
"daily_limit": self.config.max_total_frames_per_day,
|
|
144
|
+
"session_limit": self.config.max_frames_per_session,
|
|
145
|
+
"block_password_fields": self.config.block_password_fields,
|
|
146
|
+
"block_pii_detection": self.config.block_pii_detection,
|
|
147
|
+
"blur_sensitive_regions": self.config.blur_sensitive_regions,
|
|
148
|
+
"access_log_entries": len(self._access_log),
|
|
149
|
+
}
|