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,337 @@
1
+ """
2
+ Persistent memory for CCT's AI & Agent (v0.5.2).
3
+
4
+ Gives the AI and the Chemistry Agent a real, on-disk memory that survives
5
+ across restarts, so they can:
6
+
7
+ * recall the previous turns of the *current* conversation (recent N),
8
+ * remember durable facts about the user (their name, exam board, the
9
+ topics they find hard, preferred units...) that either the user asked
10
+ the AI to remember explicitly, or the AI proactively stashed,
11
+ * recall "every movement of the person" — a lightweight activity log of
12
+ what the user has solved/asked/simulated so far (topic frequencies +
13
+ a timestamped recent-activity stream),
14
+ * surface a short "what I know about you / what we just talked about"
15
+ preamble that both /ai and /agent inject into their system prompt, so
16
+ follow-up questions actually feel continuous instead of amnesiac.
17
+
18
+ Storage is one JSON file in the user's home dir (~/.cct_memory.json),
19
+ written defensively (temp-file + atomic replace) so a crash mid-write
20
+ never corrupts the whole memory. Everything here is best-effort: any I/O
21
+ failure degrades to an empty in-memory store rather than crashing the app.
22
+ """
23
+
24
+ if __name__ == '__main__':
25
+ print("This is a library file and is not meant to be run directly.")
26
+ print("Please run 'python main.py' or 'python model.py' from the project root directory.")
27
+ import sys
28
+ sys.exit(1)
29
+
30
+ import json
31
+ import os
32
+ import re
33
+ import time
34
+
35
+ MEMORY_FILE = os.path.join(os.path.expanduser("~"), ".cct_memory.json")
36
+
37
+ # How much to keep in RAM / inject into prompts. Kept modest so the
38
+ # injected context never bloats a request past a provider's token budget.
39
+ MAX_TURNS = 24 # recent conversation turns (user+assistant pairs counted as 2)
40
+ MAX_FACTS = 40 # durable user facts
41
+ MAX_ACTIVITY = 60 # timestamped "what the person did" events
42
+ MAX_TOPICS = 30 # distinct topic frequency counters
43
+
44
+
45
+ # ------------------------------------------------------------------- load/save
46
+ def _empty():
47
+ return {
48
+ "turns": [], # [{"role":"user"/"assistant", "text":..., "t": epoch, "mode": "ai"/"agent"}]
49
+ "facts": [], # ["the user's name is Aarav", "exam board: CBSE class 12", ...]
50
+ "activity": [], # [{"type":..., "label":..., "t": epoch}]
51
+ "topics": {}, # {"first order": 5, "mole concept": 2, ...}
52
+ "created": time.time(),
53
+ "updated": time.time(),
54
+ }
55
+
56
+
57
+ def load():
58
+ """Load the memory dict, or a fresh empty one if none/invalid."""
59
+ if not os.path.exists(MEMORY_FILE):
60
+ return _empty()
61
+ try:
62
+ with open(MEMORY_FILE, "r", encoding="utf-8") as f:
63
+ data = json.load(f)
64
+ if not isinstance(data, dict):
65
+ return _empty()
66
+ # Backfill any missing keys (forward-compatible with older files).
67
+ base = _empty()
68
+ base.update(data)
69
+ for k in ("turns", "facts", "activity"):
70
+ if not isinstance(base.get(k), list):
71
+ base[k] = []
72
+ if not isinstance(base.get("topics"), dict):
73
+ base["topics"] = {}
74
+ return base
75
+ except Exception:
76
+ return _empty()
77
+
78
+
79
+ def save(mem):
80
+ """Atomic write: serialize to a temp file in the same dir, then
81
+ os.replace() onto the real path. Avoids the half-written-file
82
+ corruption window a plain open(w)+dump leaves open."""
83
+ try:
84
+ mem["updated"] = time.time()
85
+ d = os.path.dirname(MEMORY_FILE) or "."
86
+ tmp = MEMORY_FILE + ".tmp"
87
+ with open(tmp, "w", encoding="utf-8") as f:
88
+ json.dump(mem, f, ensure_ascii=False, indent=1)
89
+ os.replace(tmp, MEMORY_FILE)
90
+ except Exception:
91
+ # Memory is best-effort; never let a write failure break the chat.
92
+ pass
93
+
94
+
95
+ # ------------------------------------------------------------------- mutations
96
+ def add_turn(role, text, mode="ai"):
97
+ """Append a conversation turn (role: 'user' or 'assistant') and trim
98
+ to the last MAX_TURNS entries. Persists immediately so a crash right
99
+ after a reply still keeps it."""
100
+ if not text:
101
+ return
102
+ mem = load()
103
+ mem["turns"].append({
104
+ "role": role,
105
+ "text": str(text).strip()[:4000], # cap each turn so one huge
106
+ "t": time.time(), # notebook answer can't
107
+ "mode": mode, # crowd out all the others
108
+ })
109
+ mem["turns"] = mem["turns"][-MAX_TURNS:]
110
+ save(mem)
111
+
112
+
113
+ def add_activity(event_type, label):
114
+ """Record one 'movement' — anything the person did: asked a question,
115
+ solved a numerical, ran a simulation, played the quiz..."""
116
+ if not label:
117
+ return
118
+ add_activities([(event_type, label)])
119
+
120
+
121
+ def add_activities(pairs):
122
+ """v0.7.9.0 batch form of add_activity: one load→append→save cycle for
123
+ N events instead of one full-file rewrite per event. `pairs` is an
124
+ iterable of (event_type, label)."""
125
+ pairs = [(str(t), str(l).strip()[:200]) for t, l in (pairs or []) if l]
126
+ if not pairs:
127
+ return
128
+ mem = load()
129
+ now = time.time()
130
+ for event_type, label in pairs:
131
+ mem["activity"].append({"type": event_type, "label": label, "t": now})
132
+ mem["activity"] = mem["activity"][-MAX_ACTIVITY:]
133
+ save(mem)
134
+
135
+
136
+ def bump_topic(topic):
137
+ """Increment a topic's frequency counter — powers the 'you've asked
138
+ about X a lot' recall."""
139
+ topic = (topic or "").strip().lower()
140
+ if not topic:
141
+ return
142
+ mem = load()
143
+ mem["topics"][topic] = mem["topics"].get(topic, 0) + 1
144
+ # Keep the dict bounded by dropping the least-used once it grows.
145
+ if len(mem["topics"]) > MAX_TOPICS:
146
+ for k, _ in sorted(mem["topics"].items(), key=lambda kv: kv[1])[: len(mem["topics"]) - MAX_TOPICS]:
147
+ del mem["topics"][k]
148
+ save(mem)
149
+
150
+
151
+ def add_fact(fact):
152
+ """Store a durable user fact (the AI's long-term memory about the
153
+ person). De-duplicated case-insensitively so 'remember my name is X'
154
+ twice doesn't leave two copies."""
155
+ fact = (fact or "").strip()
156
+ if not fact:
157
+ return False
158
+ mem = load()
159
+ existing = {f.lower().rstrip(".") for f in mem["facts"]}
160
+ if fact.lower().rstrip(".") in existing:
161
+ return False
162
+ mem["facts"].append(fact[:400])
163
+ mem["facts"] = mem["facts"][-MAX_FACTS:]
164
+ save(mem)
165
+ return True
166
+
167
+
168
+ def clear():
169
+ """Wipe the entire memory (used by /memory -> clear)."""
170
+ save(_empty())
171
+
172
+
173
+ # ------------------------------------------------------- fact auto-detection
174
+ _REMEMBER_RE = re.compile(
175
+ r"\b(?:remember|note|don'?t forget|keep in mind|fyi|for your info|"
176
+ r"my name is|i am|i'm|call me|my exam|my board|my class|my grade|"
177
+ r"i prefer|i like|i find|i struggle|i'm weak|i'm good)\b",
178
+ re.IGNORECASE,
179
+ )
180
+
181
+
182
+ def maybe_extract_fact(text):
183
+ """Heuristic: if the user phrases something as something to remember
184
+ ('remember my exam is in may', 'my name is aarav', 'i prefer SI
185
+ units'), stash the whole sentence as a durable fact. Returns the fact
186
+ string if one was stored, else None.
187
+
188
+ Intentionally permissive — it's better to over-remember a harmless
189
+ sentence than to miss 'my name is X'. The AI can also stash facts
190
+ deliberately via the agent's 'remember' field."""
191
+ if not text:
192
+ return None
193
+ # Only short, declarative-looking lines make good facts — a full
194
+ # multi-line numerical isn't something to "remember" wholesale.
195
+ sentences = re.split(r"(?<=[.!?])\s+|\n", text.strip())
196
+ for s in sentences:
197
+ s = s.strip(" -\u2022")
198
+ if 4 <= len(s) <= 160 and _REMEMBER_RE.search(s):
199
+ return s
200
+ return None
201
+
202
+
203
+ # ------------------------------------------------------------ prompt building
204
+ def recent_turns(n=8, mode=None):
205
+ """Return the last `n` turns as a list of dicts, optionally filtered
206
+ to a single mode ('ai' or 'agent')."""
207
+ mem = load()
208
+ turns = mem["turns"]
209
+ if mode:
210
+ turns = [t for t in turns if t.get("mode") == mode]
211
+ return turns[-n:]
212
+
213
+
214
+ def _fmt_ago(epoch):
215
+ """Human 'x min/h/d ago' for the activity log."""
216
+ try:
217
+ secs = max(0, int(time.time() - float(epoch)))
218
+ except Exception:
219
+ return ""
220
+ if secs < 60:
221
+ return "just now"
222
+ if secs < 3600:
223
+ return f"{secs // 60}m ago"
224
+ if secs < 86400:
225
+ return f"{secs // 3600}h ago"
226
+ return f"{secs // 86400}d ago"
227
+
228
+
229
+ def _keywords(text):
230
+ """Small keyword set for relevance scoring (lowercased, len>=3,
231
+ deduped). Deliberately tiny — this must stay microseconds-cheap."""
232
+ return {w for w in re.findall(r"[a-z0-9]{3,}", (text or "").lower())}
233
+
234
+
235
+ def context_block(mode="ai", max_turns=8, query=None):
236
+ """Build the 'memory' preamble injected into the AI/Agent system
237
+ prompt. Returns a single string (empty if there's genuinely nothing
238
+ worth recalling), e.g.:
239
+
240
+ ## What you remember about the user
241
+ - the user's name is Aarav
242
+ - exam board: CBSE class 12
243
+
244
+ ## Recent conversation (last 6 turns)
245
+ USER: what is a first order reaction?
246
+ ASSISTANT: A first order reaction is one whose rate...
247
+
248
+ ## User's recent activity
249
+ - solved a first order numerical (12m ago)
250
+
251
+ Use this context to make your answer continuous and personal.
252
+ Refer to earlier turns by all means; don't re-ask what's known.
253
+
254
+ v0.7.9.0 — memory must not make CAT slow (requirement #13): when a
255
+ `query` is given, facts and turns are RELEVANCE-FILTERED by cheap
256
+ keyword overlap instead of dumping everything, and the activity log /
257
+ topic list are only included when they actually relate to the query.
258
+ """
259
+ mem = load()
260
+ qk = _keywords(query) if query else set()
261
+ parts = []
262
+
263
+ facts = mem.get("facts", [])
264
+ if qk:
265
+ def _rel(text):
266
+ return len(qk & _keywords(text))
267
+ scored = [(f, _rel(f)) for f in facts[-24:]]
268
+ relevant = [f for f, s in scored if s > 0]
269
+ facts = (relevant + facts[-4:]) if relevant else facts[-6:]
270
+ if facts:
271
+ seen = set()
272
+ uniq = []
273
+ for f in facts:
274
+ k = f.lower()
275
+ if k not in seen:
276
+ seen.add(k)
277
+ uniq.append(f)
278
+ parts.append("## What you remember about the user")
279
+ parts.extend(f"- {f}" for f in uniq[-12:])
280
+ parts.append("")
281
+
282
+ turns = mem.get("turns", [])
283
+ if mode:
284
+ turns = [t for t in turns if t.get("mode") == mode]
285
+ recent = turns[-max_turns:]
286
+ if qk:
287
+ # Relevant-but-older turns first, then the most recent few for
288
+ # conversational continuity — capped so the block stays small.
289
+ older = [t for t in turns[:-max_turns] if qk & _keywords(t.get("text", ""))][-3:]
290
+ turns = (older + recent)[-max_turns:] if older else recent
291
+ else:
292
+ turns = recent
293
+ if turns:
294
+ parts.append(f"## Recent conversation ({len(turns)} turn(s))")
295
+ for t in turns:
296
+ who = "USER" if t.get("role") == "user" else "ASSISTANT"
297
+ txt = t.get("text", "").replace("\n", " ")
298
+ if len(txt) > 280:
299
+ txt = txt[:277] + "..."
300
+ parts.append(f"{who}: {txt}")
301
+ parts.append("")
302
+
303
+ # Activity + topics only when they relate to the query — otherwise
304
+ # they're pure token cost on every request.
305
+ if qk:
306
+ rel_activity = [a for a in mem.get("activity", [])[-30:]
307
+ if qk & _keywords(a.get("label", ""))][-5:]
308
+ else:
309
+ rel_activity = mem.get("activity", [])[-8:]
310
+ if rel_activity:
311
+ parts.append("## User's recent activity")
312
+ for a in reversed(rel_activity):
313
+ parts.append(f"- {a.get('label', a.get('type', ''))} ({_fmt_ago(a.get('t'))})")
314
+ parts.append("")
315
+
316
+ topics = sorted(mem.get("topics", {}).items(), key=lambda kv: kv[1], reverse=True)
317
+ if topics:
318
+ top = ", ".join(f"{k} ({v})" for k, v in topics[:5])
319
+ parts.append(f"## Topics the user asks about most\n{top}")
320
+ parts.append("")
321
+
322
+ if parts:
323
+ parts.append("Use this context to make your answer continuous and personal. "
324
+ "Refer to earlier turns by all means; do not re-ask anything already known.")
325
+ return "\n".join(parts)
326
+
327
+
328
+ def stats():
329
+ """A small summary dict for the /memory view screen."""
330
+ mem = load()
331
+ return {
332
+ "turns": len(mem.get("turns", [])),
333
+ "facts": len(mem.get("facts", [])),
334
+ "activity": len(mem.get("activity", [])),
335
+ "topics": len(mem.get("topics", {})),
336
+ "updated": mem.get("updated", mem.get("created", 0)),
337
+ }