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
calc_terminal/memory.py
ADDED
|
@@ -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
|
+
}
|