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.
Files changed (325) hide show
  1. calc_terminal/__init__.py +14 -0
  2. calc_terminal/__main__.py +14 -0
  3. calc_terminal/activity.py +1334 -0
  4. calc_terminal/agent.py +3387 -0
  5. calc_terminal/agent_runtime.py +519 -0
  6. calc_terminal/ai_context.py +447 -0
  7. calc_terminal/ai_modes.py +752 -0
  8. calc_terminal/ai_personalization.py +286 -0
  9. calc_terminal/ai_preview_feedback.py +213 -0
  10. calc_terminal/aicore.py +2572 -0
  11. calc_terminal/anim.py +367 -0
  12. calc_terminal/app.py +3685 -0
  13. calc_terminal/art.py +639 -0
  14. calc_terminal/atomsim.py +368 -0
  15. calc_terminal/attachments.py +743 -0
  16. calc_terminal/benchmark_system.py +414 -0
  17. calc_terminal/browser/__init__.py +36 -0
  18. calc_terminal/browser/browser_state.py +346 -0
  19. calc_terminal/browser/devserver.py +176 -0
  20. calc_terminal/browser/engine.py +494 -0
  21. calc_terminal/browser/navigation.py +84 -0
  22. calc_terminal/browser/preview.py +429 -0
  23. calc_terminal/browser/preview_entry.py +95 -0
  24. calc_terminal/browser/project_detector.py +144 -0
  25. calc_terminal/browser/server.py +449 -0
  26. calc_terminal/browser/state.py +75 -0
  27. calc_terminal/browser/watcher.py +99 -0
  28. calc_terminal/browser_gui/__init__.py +1 -0
  29. calc_terminal/browser_gui/__main__.py +3 -0
  30. calc_terminal/browser_gui/launcher.py +173 -0
  31. calc_terminal/browser_gui/playwright_browser.py +117 -0
  32. calc_terminal/browser_gui/qt_browser.py +1501 -0
  33. calc_terminal/browser_gui/webview_browser.py +57 -0
  34. calc_terminal/capabilities/__init__.py +35 -0
  35. calc_terminal/capabilities/adapters/__init__.py +33 -0
  36. calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
  37. calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
  38. calc_terminal/capabilities/adapters/filesystem.py +206 -0
  39. calc_terminal/capabilities/adapters/git_adapter.py +202 -0
  40. calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
  41. calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
  42. calc_terminal/capabilities/adapters/platforms.py +200 -0
  43. calc_terminal/capabilities/adapters/python_exec.py +93 -0
  44. calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
  45. calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
  46. calc_terminal/capabilities/adapters/structural_bio.py +161 -0
  47. calc_terminal/capabilities/adapters/terminal.py +99 -0
  48. calc_terminal/capabilities/bus.py +178 -0
  49. calc_terminal/capabilities/discovery.py +207 -0
  50. calc_terminal/capabilities/schema.py +221 -0
  51. calc_terminal/cat.ico +0 -0
  52. calc_terminal/cat_browser.py +2018 -0
  53. calc_terminal/chat_store.py +703 -0
  54. calc_terminal/cli.py +1178 -0
  55. calc_terminal/code_editor.py +640 -0
  56. calc_terminal/collaboration.py +723 -0
  57. calc_terminal/commands_data.py +139 -0
  58. calc_terminal/compatibility_engine.py +352 -0
  59. calc_terminal/compute/__init__.py +31 -0
  60. calc_terminal/compute/fabric.py +350 -0
  61. calc_terminal/config.py +227 -0
  62. calc_terminal/core/__init__.py +41 -0
  63. calc_terminal/core/checkpoint.py +156 -0
  64. calc_terminal/core/input/__init__.py +45 -0
  65. calc_terminal/core/mode_registry.py +300 -0
  66. calc_terminal/core/project_graph.py +172 -0
  67. calc_terminal/core/recovery.py +129 -0
  68. calc_terminal/core/security_layer.py +112 -0
  69. calc_terminal/core/task_graph.py +202 -0
  70. calc_terminal/core/unified_runtime.py +184 -0
  71. calc_terminal/core/verification.py +257 -0
  72. calc_terminal/customization.py +1566 -0
  73. calc_terminal/derivations.py +153 -0
  74. calc_terminal/device_control.py +263 -0
  75. calc_terminal/diagnostics/__init__.py +27 -0
  76. calc_terminal/diagnostics/doctor_engine.py +382 -0
  77. calc_terminal/diagnostics/self_test.py +247 -0
  78. calc_terminal/doctor.py +519 -0
  79. calc_terminal/easter_eggs.py +274 -0
  80. calc_terminal/editor/__init__.py +1 -0
  81. calc_terminal/editor/actions.py +263 -0
  82. calc_terminal/editor/commands.py +160 -0
  83. calc_terminal/editor/shortcuts.py +226 -0
  84. calc_terminal/engine.py +259 -0
  85. calc_terminal/errors.py +120 -0
  86. calc_terminal/event_stream.py +146 -0
  87. calc_terminal/eventbus.py +133 -0
  88. calc_terminal/extensions.py +733 -0
  89. calc_terminal/fallback_cli.py +1321 -0
  90. calc_terminal/first_run.py +265 -0
  91. calc_terminal/fomoji_auth.py +1043 -0
  92. calc_terminal/formulas.py +82 -0
  93. calc_terminal/fs_cache.py +121 -0
  94. calc_terminal/fs_watcher.py +277 -0
  95. calc_terminal/game.py +193 -0
  96. calc_terminal/gen1.py +5 -0
  97. calc_terminal/generators.py +245 -0
  98. calc_terminal/gestures/__init__.py +42 -0
  99. calc_terminal/gestures/bindings.py +175 -0
  100. calc_terminal/gestures/manager.py +477 -0
  101. calc_terminal/goodbye.py +363 -0
  102. calc_terminal/gpu3d.py +290 -0
  103. calc_terminal/graphs.py +358 -0
  104. calc_terminal/hardware_analyzer.py +440 -0
  105. calc_terminal/host/__init__.py +30 -0
  106. calc_terminal/host/browser_manager.py +187 -0
  107. calc_terminal/host/desktop.py +1386 -0
  108. calc_terminal/host/launcher.py +395 -0
  109. calc_terminal/host/terminal.py +279 -0
  110. calc_terminal/identity.py +216 -0
  111. calc_terminal/input/__init__.py +54 -0
  112. calc_terminal/input/capabilities.py +258 -0
  113. calc_terminal/input/focus.py +87 -0
  114. calc_terminal/input/gestures.py +64 -0
  115. calc_terminal/input/pointer.py +114 -0
  116. calc_terminal/input/touch.py +345 -0
  117. calc_terminal/keys.py +84 -0
  118. calc_terminal/live_automation.py +165 -0
  119. calc_terminal/mathtext.py +433 -0
  120. calc_terminal/mcp.py +386 -0
  121. calc_terminal/memory.py +337 -0
  122. calc_terminal/memory_v2.py +479 -0
  123. calc_terminal/metrics.py +333 -0
  124. calc_terminal/mode_detection.py +146 -0
  125. calc_terminal/model.py +2431 -0
  126. calc_terminal/model_router.py +665 -0
  127. calc_terminal/models/__init__.py +0 -0
  128. calc_terminal/models/active_state.py +187 -0
  129. calc_terminal/models/dynamic_registry.py +584 -0
  130. calc_terminal/models/manager.py +781 -0
  131. calc_terminal/models/model_metadata.json +3526 -0
  132. calc_terminal/models/profiles.py +194 -0
  133. calc_terminal/models/registry.py +265 -0
  134. calc_terminal/models/schema.py +197 -0
  135. calc_terminal/models/validator.py +287 -0
  136. calc_terminal/models/verification_engine.py +368 -0
  137. calc_terminal/native_picker.py +215 -0
  138. calc_terminal/ollama_catalog.py +279 -0
  139. calc_terminal/ollama_download.py +233 -0
  140. calc_terminal/orchestrator.py +304 -0
  141. calc_terminal/package_research.py +322 -0
  142. calc_terminal/packages.py +1024 -0
  143. calc_terminal/pc_specs.py +116 -0
  144. calc_terminal/permissions.py +334 -0
  145. calc_terminal/pet.py +106 -0
  146. calc_terminal/pipeline.py +505 -0
  147. calc_terminal/platform/__init__.py +491 -0
  148. calc_terminal/platform/desktop.py +491 -0
  149. calc_terminal/platform/web.py +781 -0
  150. calc_terminal/preview/__init__.py +1 -0
  151. calc_terminal/preview/dev_server.py +303 -0
  152. calc_terminal/preview/diagnostics.py +131 -0
  153. calc_terminal/preview/live_reload.py +66 -0
  154. calc_terminal/preview/manager.py +129 -0
  155. calc_terminal/project_stats.py +209 -0
  156. calc_terminal/projects.py +328 -0
  157. calc_terminal/providers/__init__.py +0 -0
  158. calc_terminal/providers/adapters/__init__.py +80 -0
  159. calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
  160. calc_terminal/providers/adapters/base.py +106 -0
  161. calc_terminal/providers/adapters/chinese_adapters.py +420 -0
  162. calc_terminal/providers/adapters/gemini_adapter.py +101 -0
  163. calc_terminal/providers/adapters/ollama_adapter.py +83 -0
  164. calc_terminal/providers/adapters/openai_adapter.py +159 -0
  165. calc_terminal/providers/adapters/other_adapters.py +246 -0
  166. calc_terminal/providers/anthropic_provider.py +172 -0
  167. calc_terminal/providers/auto_update.py +416 -0
  168. calc_terminal/providers/base_provider.py +105 -0
  169. calc_terminal/providers/discovery_manager.py +207 -0
  170. calc_terminal/providers/gemini_provider.py +178 -0
  171. calc_terminal/providers/lifecycle.py +767 -0
  172. calc_terminal/providers/ollama_adapter.py +707 -0
  173. calc_terminal/providers/openai_provider.py +254 -0
  174. calc_terminal/providers/provider_manager.py +1827 -0
  175. calc_terminal/providers/providers.json +4075 -0
  176. calc_terminal/reactionsim.py +279 -0
  177. calc_terminal/registry.py +337 -0
  178. calc_terminal/report.py +162 -0
  179. calc_terminal/research/__init__.py +45 -0
  180. calc_terminal/research/artifact_intel.py +126 -0
  181. calc_terminal/research/data_lineage.py +123 -0
  182. calc_terminal/research/experiment_ledger.py +303 -0
  183. calc_terminal/research/reproducibility.py +131 -0
  184. calc_terminal/resilience/__init__.py +47 -0
  185. calc_terminal/resilience/agent_state.py +121 -0
  186. calc_terminal/resilience/capability_matcher.py +174 -0
  187. calc_terminal/resilience/circuit_breaker.py +158 -0
  188. calc_terminal/resilience/failover_engine.py +230 -0
  189. calc_terminal/resilience/health_monitor.py +192 -0
  190. calc_terminal/resilience/ollama_adapter.py +125 -0
  191. calc_terminal/resilience/orchestrator.py +312 -0
  192. calc_terminal/resilience/types.py +134 -0
  193. calc_terminal/sandbox.py +98 -0
  194. calc_terminal/scires.py +558 -0
  195. calc_terminal/security_scanner.py +126 -0
  196. calc_terminal/session.py +294 -0
  197. calc_terminal/sim3d.py +206 -0
  198. calc_terminal/solver.py +276 -0
  199. calc_terminal/sound.py +127 -0
  200. calc_terminal/task_reports.py +287 -0
  201. calc_terminal/terminal_host.py +201 -0
  202. calc_terminal/terminal_identity.py +411 -0
  203. calc_terminal/test_ai_mode_reliability.py +344 -0
  204. calc_terminal/test_browser.py +368 -0
  205. calc_terminal/test_code_editor_upgrade.py +485 -0
  206. calc_terminal/test_customization.py +1148 -0
  207. calc_terminal/test_customization_ui.py +612 -0
  208. calc_terminal/test_dynamic_registry.py +304 -0
  209. calc_terminal/test_extensions.py +436 -0
  210. calc_terminal/test_overhaul.py +557 -0
  211. calc_terminal/test_project_detect.py +255 -0
  212. calc_terminal/test_root_cause_fix.py +527 -0
  213. calc_terminal/test_stability.py +532 -0
  214. calc_terminal/test_terminal_identity.py +132 -0
  215. calc_terminal/test_v079_speed.py +460 -0
  216. calc_terminal/theme.py +1107 -0
  217. calc_terminal/timeline.py +139 -0
  218. calc_terminal/todos.py +246 -0
  219. calc_terminal/tool_call_normalizer.py +419 -0
  220. calc_terminal/tui.py +104 -0
  221. calc_terminal/ui/__init__.py +8 -0
  222. calc_terminal/ui/activity_panel.py +231 -0
  223. calc_terminal/ui/activity_stream_panel.py +238 -0
  224. calc_terminal/ui/animations.py +122 -0
  225. calc_terminal/ui/app.py +7271 -0
  226. calc_terminal/ui/attach_panel.py +597 -0
  227. calc_terminal/ui/attachments.py +424 -0
  228. calc_terminal/ui/backup_panel.py +810 -0
  229. calc_terminal/ui/browser_shell.py +887 -0
  230. calc_terminal/ui/cat_agent.py +357 -0
  231. calc_terminal/ui/chats_panel.py +899 -0
  232. calc_terminal/ui/command_palette.py +125 -0
  233. calc_terminal/ui/command_palette_modal.py +166 -0
  234. calc_terminal/ui/composer.py +1141 -0
  235. calc_terminal/ui/context_menu.py +197 -0
  236. calc_terminal/ui/conversation.py +1435 -0
  237. calc_terminal/ui/customization_panel.py +1229 -0
  238. calc_terminal/ui/dashboard.py +404 -0
  239. calc_terminal/ui/design_system.py +557 -0
  240. calc_terminal/ui/diff_panel.py +213 -0
  241. calc_terminal/ui/editor.py +2102 -0
  242. calc_terminal/ui/empty_state.py +302 -0
  243. calc_terminal/ui/events.py +487 -0
  244. calc_terminal/ui/extensions_panel.py +815 -0
  245. calc_terminal/ui/footer.py +166 -0
  246. calc_terminal/ui/gestures_panel.py +383 -0
  247. calc_terminal/ui/goodbye_screen.py +100 -0
  248. calc_terminal/ui/header.py +1034 -0
  249. calc_terminal/ui/help_panel.py +254 -0
  250. calc_terminal/ui/live_activities.py +914 -0
  251. calc_terminal/ui/mcp_panel.py +570 -0
  252. calc_terminal/ui/memory_center.py +524 -0
  253. calc_terminal/ui/mode_colors_panel.py +525 -0
  254. calc_terminal/ui/nav_screens.py +747 -0
  255. calc_terminal/ui/ollama_panel.py +536 -0
  256. calc_terminal/ui/palette.py +221 -0
  257. calc_terminal/ui/permission_panel.py +269 -0
  258. calc_terminal/ui/personalization_panel.py +517 -0
  259. calc_terminal/ui/personalize_center.py +1568 -0
  260. calc_terminal/ui/preview_panel.py +441 -0
  261. calc_terminal/ui/resizers.py +402 -0
  262. calc_terminal/ui/sidebar.py +1285 -0
  263. calc_terminal/ui/statusbar.py +168 -0
  264. calc_terminal/ui/theme_css.py +1396 -0
  265. calc_terminal/ui/thinking.py +226 -0
  266. calc_terminal/ui/timeline_panel.py +102 -0
  267. calc_terminal/ui/todo_panel.py +193 -0
  268. calc_terminal/ui/viewport.py +136 -0
  269. calc_terminal/ui/vision_panel.py +489 -0
  270. calc_terminal/ui/welcome_modal.py +343 -0
  271. calc_terminal/ui/widgets.py +160 -0
  272. calc_terminal/ui/workspace.py +831 -0
  273. calc_terminal/viewers/__init__.py +1 -0
  274. calc_terminal/viewers/document_viewer.py +252 -0
  275. calc_terminal/viewers/image_viewer.py +241 -0
  276. calc_terminal/viewers/pdf_viewer.py +203 -0
  277. calc_terminal/viewers/presentation_viewer.py +164 -0
  278. calc_terminal/viewers/registry.py +120 -0
  279. calc_terminal/viewers/spreadsheet_viewer.py +204 -0
  280. calc_terminal/vision/__init__.py +89 -0
  281. calc_terminal/vision/analysis.py +194 -0
  282. calc_terminal/vision/annotations.py +297 -0
  283. calc_terminal/vision/capture.py +171 -0
  284. calc_terminal/vision/context.py +231 -0
  285. calc_terminal/vision/correlation.py +169 -0
  286. calc_terminal/vision/cursor.py +258 -0
  287. calc_terminal/vision/events.py +66 -0
  288. calc_terminal/vision/frame_pipeline.py +259 -0
  289. calc_terminal/vision/priority.py +218 -0
  290. calc_terminal/vision/provider.py +180 -0
  291. calc_terminal/vision/safety.py +149 -0
  292. calc_terminal/vision/session.py +281 -0
  293. calc_terminal/vision/verify.py +162 -0
  294. calc_terminal/vision.py +514 -0
  295. calc_terminal/vscode_integration.py +113 -0
  296. calc_terminal/web/__init__.py +8 -0
  297. calc_terminal/web/cat_runtime.py +710 -0
  298. calc_terminal/web/server.py +2891 -0
  299. calc_terminal/web/static/css/app.css +3152 -0
  300. calc_terminal/web/static/icons/badge-72.png +0 -0
  301. calc_terminal/web/static/icons/cat.ico +0 -0
  302. calc_terminal/web/static/icons/icon-128.png +0 -0
  303. calc_terminal/web/static/icons/icon-144.png +0 -0
  304. calc_terminal/web/static/icons/icon-152.png +0 -0
  305. calc_terminal/web/static/icons/icon-192.png +0 -0
  306. calc_terminal/web/static/icons/icon-384.png +0 -0
  307. calc_terminal/web/static/icons/icon-512.png +0 -0
  308. calc_terminal/web/static/icons/icon-72.png +0 -0
  309. calc_terminal/web/static/icons/icon-96.png +0 -0
  310. calc_terminal/web/static/icons/icon.svg +34 -0
  311. calc_terminal/web/static/icons/new-project.png +0 -0
  312. calc_terminal/web/static/icons/open-project.png +0 -0
  313. calc_terminal/web/static/index.html +734 -0
  314. calc_terminal/web/static/js/app.js +2403 -0
  315. calc_terminal/web/static/manifest.json +88 -0
  316. calc_terminal/web/static/sw.js +230 -0
  317. calc_terminal/workflow_engine.py +769 -0
  318. calc_terminal/workspace.py +593 -0
  319. calc_terminal/workspace_index.py +385 -0
  320. cct_cli-0.7.9.0.dist-info/METADATA +210 -0
  321. cct_cli-0.7.9.0.dist-info/RECORD +325 -0
  322. cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
  323. cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
  324. cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
  325. cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,192 @@
1
+ """
2
+ CAT — Backup Provider & Resilience System: Health Monitor
3
+ Author: Kazi Zillani
4
+
5
+ Lightweight, real-time provider and model health tracking based on actual
6
+ CAT observations. Strictly guarantees ZERO credential exposure.
7
+ """
8
+
9
+ import re
10
+ import threading
11
+ import time
12
+ from typing import Dict, List, Optional, Tuple
13
+
14
+ from .circuit_breaker import CircuitBreakerState, get_circuit_breaker
15
+ from .types import ProviderHealthState
16
+
17
+ _KEY_CLEAN_RE = re.compile(r"(sk-[a-zA-Z0-9_\-]{8,}|gsk_[a-zA-Z0-9_\-]{8,}|AIzaSy[a-zA-Z0-9_\-]{8,}|[a-f0-9]{32,})", re.I)
18
+
19
+
20
+ def sanitize_error(text: str) -> str:
21
+ """Mask any API keys, credentials, or auth tokens from error strings."""
22
+ if not text or not isinstance(text, str):
23
+ return ""
24
+ cleaned = _KEY_CLEAN_RE.sub("[CREDENTIAL_REDACTED]", text)
25
+ # Remove authorization headers or token params if present
26
+ cleaned = re.sub(
27
+ r"((?:authorization[:=]\s*)?bearer\s+|api[_\-\s]?key[:=\s]+|authorization[:=]\s*)['\"]?[a-zA-Z0-9_\.\-]+['\"]?",
28
+ r"\1[REDACTED]",
29
+ cleaned,
30
+ flags=re.I,
31
+ )
32
+ return cleaned[:200]
33
+
34
+
35
+ class HealthMonitor:
36
+ """Central registry of observable AI health state across CAT."""
37
+
38
+ def __init__(self):
39
+ self._lock = threading.Lock()
40
+ # (provider, model) -> ProviderHealthState
41
+ self._states: Dict[Tuple[str, str], ProviderHealthState] = {}
42
+ self._breaker = get_circuit_breaker()
43
+
44
+ def _key(self, provider: str, model: str) -> Tuple[str, str]:
45
+ return (str(provider or "").strip().lower(), str(model or "").strip().lower())
46
+
47
+ def record_turn_start(self, provider: str, model: str) -> None:
48
+ """Mark turn attempt initiation."""
49
+ with self._lock:
50
+ k = self._key(provider, model)
51
+ if k not in self._states:
52
+ self._states[k] = ProviderHealthState(provider=provider, model=model)
53
+ self._states[k].last_checked = time.time()
54
+
55
+ def record_success(self, provider: str, model: str, latency_ms: Optional[float] = None) -> None:
56
+ """Record successful inference turn."""
57
+ p_low = str(provider or "").strip().lower()
58
+ is_local = p_low in ("ollama", "local", "vllm")
59
+ now = time.time()
60
+
61
+ with self._lock:
62
+ k = self._key(provider, model)
63
+ state = self._states.get(k)
64
+ if not state:
65
+ state = ProviderHealthState(provider=provider, model=model)
66
+ self._states[k] = state
67
+
68
+ state.last_success = now
69
+ state.last_checked = now
70
+ state.consecutive_failures = 0
71
+ state.success_count += 1
72
+ state.circuit_open = False
73
+ state.cooldown_until = 0.0
74
+
75
+ if latency_ms is not None:
76
+ if state.latency_ms is None:
77
+ state.latency_ms = latency_ms
78
+ else:
79
+ # Exponential smoothing
80
+ state.latency_ms = round(0.7 * latency_ms + 0.3 * state.latency_ms, 1)
81
+
82
+ if is_local:
83
+ state.status = "local"
84
+ elif state.latency_ms and state.latency_ms > 3500.0:
85
+ state.status = "slow"
86
+ else:
87
+ state.status = "healthy"
88
+
89
+ self._breaker.record_success(provider, model, latency_ms)
90
+
91
+ def record_failure(
92
+ self,
93
+ provider: str,
94
+ model: str,
95
+ error_message: str = "",
96
+ is_rate_limit: bool = False,
97
+ cooldown_seconds: Optional[float] = None,
98
+ ) -> None:
99
+ """Record a failure without leaking any credential details."""
100
+ now = time.time()
101
+ safe_msg = sanitize_error(error_message)
102
+
103
+ with self._lock:
104
+ k = self._key(provider, model)
105
+ state = self._states.get(k)
106
+ if not state:
107
+ state = ProviderHealthState(provider=provider, model=model)
108
+ self._states[k] = state
109
+
110
+ state.last_checked = now
111
+ state.consecutive_failures += 1
112
+ state.failure_count += 1
113
+ state.last_error = safe_msg
114
+
115
+ if is_rate_limit:
116
+ state.status = "rate_limited"
117
+ cd = cooldown_seconds or 60.0
118
+ state.cooldown_until = now + cd
119
+ state.circuit_open = True
120
+ else:
121
+ if state.consecutive_failures >= self._breaker.failure_threshold:
122
+ state.status = "offline"
123
+ cd = cooldown_seconds or self._breaker.default_cooldown
124
+ state.cooldown_until = now + cd
125
+ state.circuit_open = True
126
+ else:
127
+ state.status = "slow"
128
+
129
+ self._breaker.record_failure(
130
+ provider,
131
+ model,
132
+ error=safe_msg,
133
+ is_rate_limit=is_rate_limit,
134
+ cooldown_seconds=cooldown_seconds,
135
+ )
136
+
137
+ def get_health(self, provider: str, model: str) -> ProviderHealthState:
138
+ """Retrieve the health record for a provider/model."""
139
+ with self._lock:
140
+ k = self._key(provider, model)
141
+ state = self._states.get(k)
142
+ if not state:
143
+ p_low = str(provider or "").strip().lower()
144
+ is_local = p_low in ("ollama", "local", "vllm")
145
+ state = ProviderHealthState(
146
+ provider=provider,
147
+ model=model,
148
+ status="local" if is_local else "disconnected",
149
+ )
150
+ self._states[k] = state
151
+ # Synchronize breaker status
152
+ breaker_state = self._breaker.get_state(provider, model)
153
+ state.circuit_open = breaker_state in (CircuitBreakerState.OPEN,)
154
+ return state
155
+
156
+ def is_healthy(self, provider: str, model: str) -> bool:
157
+ """Check if an endpoint is usable."""
158
+ if not self._breaker.can_attempt(provider, model):
159
+ return False
160
+ st = self.get_health(provider, model)
161
+ return st.status not in ("offline", "rate_limited") or not st.circuit_open
162
+
163
+ def all_states(self) -> List[ProviderHealthState]:
164
+ """Return a snapshot list of all tracked health records."""
165
+ with self._lock:
166
+ return list(self._states.values())
167
+
168
+ def get_status_badge(self, provider: str, model: str) -> Tuple[str, str]:
169
+ """Return (glyph, label) representation (e.g. ('🟢', 'Healthy'))."""
170
+ st = self.get_health(provider, model)
171
+ status = st.status
172
+ if status == "local":
173
+ return ("🟢", "Local")
174
+ elif status == "healthy":
175
+ return ("🟢", "Healthy")
176
+ elif status == "slow":
177
+ return ("🟡", "Slow")
178
+ elif status == "rate_limited":
179
+ return ("🟠", "Rate Limited")
180
+ elif status == "offline":
181
+ return ("🔴", "In Cooldown")
182
+ return ("⚫", "Disconnected")
183
+
184
+
185
+ _GLOBAL_MONITOR: Optional[HealthMonitor] = None
186
+
187
+
188
+ def get_health_monitor() -> HealthMonitor:
189
+ global _GLOBAL_MONITOR
190
+ if _GLOBAL_MONITOR is None:
191
+ _GLOBAL_MONITOR = HealthMonitor()
192
+ return _GLOBAL_MONITOR
@@ -0,0 +1,125 @@
1
+ """
2
+ CAT — Backup Provider & Resilience System: Native Ollama Local Adapter
3
+ Author: Kazi Zillani
4
+
5
+ First-class native integration for local Ollama fallback.
6
+ Requires:
7
+ - Zero API keys
8
+ - Zero cloud endpoints
9
+ - Automatic discovery of local models via /api/tags
10
+ """
11
+
12
+ import json
13
+ import logging
14
+ import os
15
+ import time
16
+ from typing import Any, Dict, List, Optional
17
+
18
+ _LOG = logging.getLogger(__name__)
19
+
20
+ DEFAULT_OLLAMA_URL = "http://localhost:11434"
21
+
22
+ # Quality instruct model heuristics
23
+ INSTRUCT_PREFERENCES = [
24
+ "qwen2.5-coder", "qwen3", "qwen2.5", "llama3.3", "llama3.2",
25
+ "llama3.1", "deepseek-r1", "deepseek-coder", "codellama",
26
+ "mistral", "gemma2", "phi3", "phi4", "starcoder",
27
+ ]
28
+
29
+
30
+ class OllamaAdapter:
31
+ """Zero-config discovery and management for local Ollama backup."""
32
+
33
+ def __init__(self, base_url: str = DEFAULT_OLLAMA_URL):
34
+ self.base_url = (base_url or DEFAULT_OLLAMA_URL).rstrip("/")
35
+ self._cached_models: Optional[List[dict]] = None
36
+ self._last_checked = 0.0
37
+
38
+ def is_available(self, timeout: float = 1.5) -> bool:
39
+ """Check if local Ollama daemon is reachable."""
40
+ try:
41
+ import requests
42
+ resp = requests.get(f"{self.base_url}/api/tags", timeout=timeout)
43
+ return resp.status_code == 200
44
+ except Exception:
45
+ return False
46
+
47
+ def list_installed_models(self, force_refresh: bool = False) -> List[dict]:
48
+ """Fetch models currently pulled on the local machine."""
49
+ now = time.time()
50
+ if not force_refresh and self._cached_models is not None and (now - self._last_checked) < 30.0:
51
+ return self._cached_models
52
+
53
+ try:
54
+ import requests
55
+ resp = requests.get(f"{self.base_url}/api/tags", timeout=2.5)
56
+ if resp.status_code == 200:
57
+ data = resp.json()
58
+ models = data.get("models", [])
59
+ self._cached_models = models
60
+ self._last_checked = now
61
+ return models
62
+ except Exception as e:
63
+ _LOG.debug("Ollama discovery failed: %s", e)
64
+
65
+ return []
66
+
67
+ def get_model_names(self) -> List[str]:
68
+ return [str(m.get("name", "")).strip() for m in self.list_installed_models() if m.get("name")]
69
+
70
+ def select_best_model(self, coding: bool = False, reasoning: bool = False) -> Optional[str]:
71
+ """Pick the best available instruct model installed locally."""
72
+ names = self.get_model_names()
73
+ if not names:
74
+ return None
75
+
76
+ # Filter out raw non-instruct base models
77
+ non_base = [
78
+ n for n in names
79
+ if not any(b in n.lower() for b in ("-base", "_base", "base-q", ":base"))
80
+ ]
81
+ candidates = non_base if non_base else names
82
+
83
+ if coding:
84
+ for pref in ("coder", "code", "qwen", "deepseek"):
85
+ for m in candidates:
86
+ if pref in m.lower():
87
+ return m
88
+
89
+ if reasoning:
90
+ for m in candidates:
91
+ if "r1" in m.lower() or "reasoning" in m.lower():
92
+ return m
93
+
94
+ for pref in INSTRUCT_PREFERENCES:
95
+ for m in candidates:
96
+ if pref in m.lower():
97
+ return m
98
+
99
+ return candidates[0]
100
+
101
+ def build_backup_entry(self, model: Optional[str] = None) -> dict:
102
+ """Create a standard backup configuration entry for Ollama."""
103
+ selected_model = model or self.select_best_model() or "llama3.3"
104
+ return {
105
+ "provider": "ollama",
106
+ "model": selected_model,
107
+ "api_key": "",
108
+ "base_url": self.base_url,
109
+ "api_style": "ollama",
110
+ "name": "Ollama (Local Backup)",
111
+ "enabled": True,
112
+ "status": "connected" if self.is_available() else "disconnected",
113
+ "last_used": 0,
114
+ "priority": 999, # Default to final local safety net
115
+ }
116
+
117
+
118
+ _GLOBAL_OLLAMA: Optional[OllamaAdapter] = None
119
+
120
+
121
+ def get_ollama_adapter() -> OllamaAdapter:
122
+ global _GLOBAL_OLLAMA
123
+ if _GLOBAL_OLLAMA is None:
124
+ _GLOBAL_OLLAMA = OllamaAdapter()
125
+ return _GLOBAL_OLLAMA
@@ -0,0 +1,312 @@
1
+ """
2
+ CAT — Backup Provider & Resilience System: Unified AI Orchestrator
3
+ Author: Kazi Zillani
4
+
5
+ The central orchestrator coordinating AI provider access across CAT's entire
6
+ architecture: Chat, Coding Agent, IDE Agent, Tools, Browser Agent, and Live Preview.
7
+ """
8
+
9
+ import logging
10
+ import time
11
+ from typing import Any, Callable, Dict, Generator, List, Optional, Tuple
12
+
13
+ from .agent_state import AgentTaskState
14
+ from .circuit_breaker import get_circuit_breaker
15
+ from .failover_engine import get_failover_engine
16
+ from .health_monitor import get_health_monitor, sanitize_error
17
+ from .ollama_adapter import get_ollama_adapter
18
+ from .types import CandidateTarget, CostPolicy, FailureType, SecurityLevel, TaskRequirements
19
+
20
+ _LOG = logging.getLogger(__name__)
21
+
22
+
23
+ class AIOrchestrator:
24
+ """Master AI Orchestration & Resilience Core for CAT."""
25
+
26
+ def __init__(self):
27
+ self.engine = get_failover_engine()
28
+ self.health_monitor = get_health_monitor()
29
+ self.circuit_breaker = get_circuit_breaker()
30
+ self.ollama = get_ollama_adapter()
31
+ self.security_level = SecurityLevel.BALANCED
32
+ self.cost_policy = CostPolicy.ANY
33
+ self._failover_callbacks: List[Callable[[str], None]] = []
34
+
35
+ def add_failover_listener(self, callback: Callable[[str], None]) -> None:
36
+ """Register a callback to receive user-facing failover announcements."""
37
+ if callback not in self._failover_callbacks:
38
+ self._failover_callbacks.append(callback)
39
+
40
+ def remove_failover_listener(self, callback: Callable[[str], None]) -> None:
41
+ if callback in self._failover_callbacks:
42
+ self._failover_callbacks.remove(callback)
43
+
44
+ def notify_failover(self, message: str) -> None:
45
+ for cb in self._failover_callbacks:
46
+ try:
47
+ cb(message)
48
+ except Exception:
49
+ pass
50
+
51
+ def get_privacy_policy(self) -> str:
52
+ try:
53
+ from ..model_router import get_privacy_policy
54
+ return get_privacy_policy()
55
+ except Exception:
56
+ return "local_first"
57
+
58
+ def set_privacy_policy(self, policy: str) -> None:
59
+ try:
60
+ from ..model_router import set_privacy_policy
61
+ set_privacy_policy(policy)
62
+ except Exception:
63
+ pass
64
+
65
+ def enable_emergency_offline_mode(self) -> Tuple[bool, str]:
66
+ """Switch CAT to local-only emergency mode: zero cloud, Ollama primary."""
67
+ self.set_privacy_policy("never_cloud")
68
+ self.cost_policy = CostPolicy.LOCAL_ONLY
69
+ is_ollama_ready = self.ollama.is_available()
70
+ models = self.ollama.get_model_names() if is_ollama_ready else []
71
+
72
+ if is_ollama_ready and models:
73
+ msg = f"Offline Local Mode active. Local Ollama available ({len(models)} models: {', '.join(models[:3])})."
74
+ return True, msg
75
+ elif is_ollama_ready:
76
+ msg = "Offline Local Mode active. Local Ollama daemon running, but no models found (run `cat models pull <name>`)."
77
+ return True, msg
78
+ else:
79
+ msg = "Offline Local Mode active. Cloud disabled. Note: Local Ollama is not currently detected at localhost:11434."
80
+ return False, msg
81
+
82
+ def build_targets(
83
+ self,
84
+ primary_config: Optional[dict] = None,
85
+ requirements: Optional[TaskRequirements] = None,
86
+ ) -> List[CandidateTarget]:
87
+ """Resolve ordered list of capable, allowed candidate targets."""
88
+ try:
89
+ from .. import aicore
90
+ prim = dict(primary_config or aicore.load_config() or {})
91
+ except Exception:
92
+ prim = dict(primary_config or {})
93
+
94
+ return self.engine.build_failover_targets(
95
+ primary_config=prim,
96
+ requirements=requirements or TaskRequirements(),
97
+ privacy_policy=self.get_privacy_policy(),
98
+ cost_policy=self.cost_policy,
99
+ security_level=self.security_level,
100
+ )
101
+
102
+ def execute_query(
103
+ self,
104
+ prompt: str,
105
+ system_prompt: str = "",
106
+ history: Optional[List] = None,
107
+ attachments: Optional[List] = None,
108
+ requirements: Optional[TaskRequirements] = None,
109
+ config: Optional[dict] = None,
110
+ on_failover: Optional[Callable[[str], None]] = None,
111
+ size_class: str = "normal",
112
+ ) -> str:
113
+ """Execute chat/generation turn with resilient automatic failover."""
114
+ targets = self.build_targets(primary_config=config, requirements=requirements)
115
+ if not targets:
116
+ return "No eligible AI providers available for this task under current privacy and capability settings."
117
+
118
+ from ..aicore import (
119
+ _attempts_per_provider,
120
+ _backoff_sleep,
121
+ _mark_backup_success,
122
+ _query_ai_once,
123
+ _short_id,
124
+ log_request_event,
125
+ )
126
+
127
+ last_result = ""
128
+ max_attempts = _attempts_per_provider()
129
+ fallback_used = False
130
+
131
+ for t_idx, target in enumerate(targets):
132
+ cfg = target.config
133
+ provider = target.provider
134
+ model = target.model
135
+
136
+ self.health_monitor.record_turn_start(provider, model)
137
+ t_start = time.time()
138
+
139
+ for attempt in range(max_attempts):
140
+ if attempt > 0:
141
+ log_request_event(
142
+ _short_id(), "retry_same_provider",
143
+ provider=provider, model=model,
144
+ size_class=size_class, retry_count=attempt
145
+ )
146
+
147
+ last_result = _query_ai_once(
148
+ prompt,
149
+ system_prompt=system_prompt,
150
+ history=history,
151
+ config=cfg,
152
+ attachments=attachments,
153
+ size_class=size_class,
154
+ )
155
+
156
+ if not self.engine.should_failover(last_result):
157
+ elapsed_ms = (time.time() - t_start) * 1000.0
158
+ self.health_monitor.record_success(provider, model, latency_ms=elapsed_ms)
159
+ if target.is_backup and target.entry:
160
+ _mark_backup_success(target.entry)
161
+ self.notify_failover(f"✓ Connected to backup ({provider}/{model}).")
162
+ if on_failover:
163
+ on_failover(f"✓ Connected to backup ({provider}/{model}).")
164
+
165
+ log_request_event(
166
+ _short_id(), "finish",
167
+ provider=provider, model=model,
168
+ size_class=size_class, chars=len(last_result),
169
+ fallback=fallback_used
170
+ )
171
+ return last_result
172
+
173
+ # Classify failure
174
+ ftype = self.engine.classify_failure(last_result)
175
+ is_rate_limit = ftype == FailureType.RATE_LIMIT
176
+
177
+ if attempt + 1 < max_attempts and self.engine.is_retryable(ftype):
178
+ log_request_event(
179
+ _short_id(), "attempt_failed_retryable",
180
+ provider=provider, detail=last_result[:120],
181
+ size_class=size_class
182
+ )
183
+ _backoff_sleep(attempt)
184
+ continue
185
+
186
+ # Non-retryable or retries exhausted
187
+ self.health_monitor.record_failure(
188
+ provider, model, error_message=last_result, is_rate_limit=is_rate_limit
189
+ )
190
+ break
191
+
192
+ # Failover to next target
193
+ if t_idx + 1 < len(targets):
194
+ fallback_used = True
195
+ nxt = targets[t_idx + 1]
196
+ cur_desc = f"{provider} ({model})"
197
+ nxt_desc = f"{nxt.provider} ({nxt.model})"
198
+ msg = f"⚠ Provider issue with {cur_desc}. Switching to {'Model Fallback' if nxt.tier == 1 else 'Backup Provider'} ({nxt_desc})..."
199
+ self.notify_failover(msg)
200
+ if on_failover:
201
+ on_failover(msg)
202
+
203
+ return last_result or "All configured AI providers failed to respond."
204
+
205
+ def execute_stream(
206
+ self,
207
+ prompt: str,
208
+ system_prompt: str = "",
209
+ history: Optional[List] = None,
210
+ attachments: Optional[List] = None,
211
+ requirements: Optional[TaskRequirements] = None,
212
+ config: Optional[dict] = None,
213
+ on_failover: Optional[Callable[[str], None]] = None,
214
+ size_class: str = "normal",
215
+ ) -> Generator[str, None, None]:
216
+ """Stream response fragments with automatic failover if first chunk fails."""
217
+ targets = self.build_targets(primary_config=config, requirements=requirements)
218
+ if not targets:
219
+ yield "No eligible AI providers available for this task."
220
+ return
221
+
222
+ from ..aicore import (
223
+ _attempts_per_provider,
224
+ _backoff_sleep,
225
+ _mark_backup_success,
226
+ _peek_first,
227
+ _stream_ai_once,
228
+ )
229
+
230
+ last_err = ""
231
+ max_attempts = _attempts_per_provider()
232
+
233
+ for t_idx, target in enumerate(targets):
234
+ cfg = target.config
235
+ provider = target.provider
236
+ model = target.model
237
+
238
+ self.health_monitor.record_turn_start(provider, model)
239
+ t_start = time.time()
240
+
241
+ for attempt in range(max_attempts):
242
+ gen = _stream_ai_once(
243
+ prompt,
244
+ system_prompt=system_prompt,
245
+ history=history,
246
+ config=cfg,
247
+ attachments=attachments,
248
+ size_class=size_class,
249
+ )
250
+ first, rest = _peek_first(gen)
251
+
252
+ if first is None:
253
+ last_err = "No output produced by AI provider."
254
+ continue
255
+
256
+ if not self.engine.should_failover(first):
257
+ elapsed_ms = (time.time() - t_start) * 1000.0
258
+ self.health_monitor.record_success(provider, model, latency_ms=elapsed_ms)
259
+ if target.is_backup and target.entry:
260
+ _mark_backup_success(target.entry)
261
+ yield first
262
+ yield from rest
263
+ return
264
+
265
+ # First chunk indicates failure
266
+ last_err = first
267
+ ftype = self.engine.classify_failure(first)
268
+ is_rate_limit = ftype == FailureType.RATE_LIMIT
269
+
270
+ if attempt + 1 < max_attempts and self.engine.is_retryable(ftype):
271
+ _backoff_sleep(attempt)
272
+ continue
273
+
274
+ self.health_monitor.record_failure(
275
+ provider, model, error_message=first, is_rate_limit=is_rate_limit
276
+ )
277
+ break
278
+
279
+ if t_idx + 1 < len(targets):
280
+ nxt = targets[t_idx + 1]
281
+ msg = f"⚠ Switching provider from {provider}/{model} to {nxt.provider}/{nxt.model}..."
282
+ self.notify_failover(msg)
283
+ if on_failover:
284
+ on_failover(msg)
285
+
286
+ yield last_err or "All configured AI providers failed."
287
+
288
+ def test_provider(self, config: dict) -> Tuple[bool, str, List[str]]:
289
+ """Verify connectivity, model availability, and response latency."""
290
+ from ..providers import provider_manager as pm
291
+ start = time.time()
292
+ ok, msg, models = pm.connect_provider(config)
293
+ elapsed_ms = (time.time() - start) * 1000.0
294
+
295
+ prov = config.get("provider", "?")
296
+ model = config.get("model", "")
297
+ if ok:
298
+ self.health_monitor.record_success(prov, model, latency_ms=elapsed_ms)
299
+ else:
300
+ self.health_monitor.record_failure(prov, model, error_message=msg)
301
+
302
+ return ok, sanitize_error(msg), models
303
+
304
+
305
+ _GLOBAL_ORCHESTRATOR: Optional[AIOrchestrator] = None
306
+
307
+
308
+ def get_orchestrator() -> AIOrchestrator:
309
+ global _GLOBAL_ORCHESTRATOR
310
+ if _GLOBAL_ORCHESTRATOR is None:
311
+ _GLOBAL_ORCHESTRATOR = AIOrchestrator()
312
+ return _GLOBAL_ORCHESTRATOR