cct-cli 0.7.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
import uuid
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _log(msg):
|
|
10
|
+
try:
|
|
11
|
+
print(f"[Normalizer] {msg}", file=sys.stderr)
|
|
12
|
+
except Exception:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
TOOL_ALIASES = {
|
|
17
|
+
"runcommand": "run_terminal",
|
|
18
|
+
"run_command": "run_terminal",
|
|
19
|
+
"execute_command": "run_terminal",
|
|
20
|
+
"execute_terminal": "run_terminal",
|
|
21
|
+
"run_cmd": "run_terminal",
|
|
22
|
+
"terminal": "run_terminal",
|
|
23
|
+
"shell": "run_terminal",
|
|
24
|
+
"bash": "run_terminal",
|
|
25
|
+
"listdirectory": "list_directory",
|
|
26
|
+
"list_dir": "list_directory",
|
|
27
|
+
"list_files": "list_directory",
|
|
28
|
+
"ls": "list_directory",
|
|
29
|
+
"dir": "list_directory",
|
|
30
|
+
"create_file": "write_file",
|
|
31
|
+
"new_file": "write_file",
|
|
32
|
+
"rewrite_file": "write_file",
|
|
33
|
+
"writefile": "write_file",
|
|
34
|
+
"write": "write_file",
|
|
35
|
+
"edit_file": "edit_file",
|
|
36
|
+
"editfile": "edit_file",
|
|
37
|
+
"modify_file": "edit_file",
|
|
38
|
+
"patch_file": "edit_file",
|
|
39
|
+
"replace_in_file": "edit_file",
|
|
40
|
+
"str_replace_editor": "edit_file",
|
|
41
|
+
"delete_file": "delete_file",
|
|
42
|
+
"deletefile": "delete_file",
|
|
43
|
+
"remove_file": "delete_file",
|
|
44
|
+
"rm": "delete_file",
|
|
45
|
+
"move_file": "rename_file",
|
|
46
|
+
"movefile": "rename_file",
|
|
47
|
+
"rename": "rename_file",
|
|
48
|
+
"mv": "rename_file",
|
|
49
|
+
"create_directory": "create_folder",
|
|
50
|
+
"create_dir": "create_folder",
|
|
51
|
+
"mkdir": "create_folder",
|
|
52
|
+
"read_file": "read_file",
|
|
53
|
+
"readfile": "read_file",
|
|
54
|
+
"cat": "read_file",
|
|
55
|
+
"search_workspace": "search_workspace",
|
|
56
|
+
"search": "search_workspace",
|
|
57
|
+
"find": "search_workspace",
|
|
58
|
+
"grep": "search_workspace",
|
|
59
|
+
"run_build": "run_build",
|
|
60
|
+
"build_project": "run_build",
|
|
61
|
+
"build": "run_build",
|
|
62
|
+
"run_tests": "run_tests",
|
|
63
|
+
"run_test": "run_tests",
|
|
64
|
+
"runtests": "run_tests",
|
|
65
|
+
"test_suite": "run_tests",
|
|
66
|
+
"inspect_project": "inspect_project",
|
|
67
|
+
"project_info": "inspect_project",
|
|
68
|
+
"inspect": "inspect_project",
|
|
69
|
+
"install_package": "install_packages",
|
|
70
|
+
"install": "install_packages",
|
|
71
|
+
"archive_list": "archive_list",
|
|
72
|
+
"list_archive": "archive_list",
|
|
73
|
+
"zip_list": "archive_list",
|
|
74
|
+
"archive_extract": "archive_extract",
|
|
75
|
+
"extract_archive": "archive_extract",
|
|
76
|
+
"unzip": "archive_extract",
|
|
77
|
+
"extract": "archive_extract",
|
|
78
|
+
"archive_delete_entries": "archive_delete_entries",
|
|
79
|
+
"archive_remove_entries": "archive_delete_entries",
|
|
80
|
+
"archive_add_entries": "archive_add_entries",
|
|
81
|
+
"add_to_archive": "archive_add_entries",
|
|
82
|
+
"archive_repack": "archive_repack",
|
|
83
|
+
"repack_archive": "archive_repack",
|
|
84
|
+
"archive_validate": "archive_validate",
|
|
85
|
+
"validate_archive": "archive_validate",
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def resolve_tool_name(name):
|
|
90
|
+
if not name:
|
|
91
|
+
return name
|
|
92
|
+
# normalize separators/case first so "Create File", "write-file",
|
|
93
|
+
# "RUN COMMAND" style names all resolve (requirement #2)
|
|
94
|
+
low = re.sub(r"[\s\-]+", "_", str(name).lower().strip())
|
|
95
|
+
if low in TOOL_ALIASES:
|
|
96
|
+
resolved = TOOL_ALIASES[low]
|
|
97
|
+
if resolved != low:
|
|
98
|
+
_log(f"Alias resolved: {name} -> {resolved}")
|
|
99
|
+
return resolved
|
|
100
|
+
return low
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class ToolCall:
|
|
105
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
106
|
+
name: str = ""
|
|
107
|
+
arguments: Dict[str, Any] = field(default_factory=dict)
|
|
108
|
+
thought: str = ""
|
|
109
|
+
source_format: str = ""
|
|
110
|
+
confidence: float = 1.0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _find_balanced_json(text: str, start: int = 0) -> Optional[str]:
|
|
114
|
+
if start >= len(text) or text[start] != "{":
|
|
115
|
+
return None
|
|
116
|
+
depth = 0
|
|
117
|
+
in_string = False
|
|
118
|
+
escape = False
|
|
119
|
+
for i in range(start, len(text)):
|
|
120
|
+
c = text[i]
|
|
121
|
+
if escape:
|
|
122
|
+
escape = False
|
|
123
|
+
continue
|
|
124
|
+
if c == "\\":
|
|
125
|
+
escape = True
|
|
126
|
+
continue
|
|
127
|
+
if c == '"':
|
|
128
|
+
in_string = not in_string
|
|
129
|
+
continue
|
|
130
|
+
if in_string:
|
|
131
|
+
continue
|
|
132
|
+
if c == "{":
|
|
133
|
+
depth += 1
|
|
134
|
+
elif c == "}":
|
|
135
|
+
depth -= 1
|
|
136
|
+
if depth == 0:
|
|
137
|
+
return text[start : i + 1]
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _parse_json_protocol(text: str, known_tools: set) -> List[ToolCall]:
|
|
142
|
+
calls = []
|
|
143
|
+
patterns = [
|
|
144
|
+
r'\{"action"\s*:\s*"tool"\s*,\s*"tool"\s*:\s*"([^"]+)"\s*,\s*"args"\s*:\s*(\{[^}]*\})\s*\}',
|
|
145
|
+
r'\{"action"\s*:\s*"tool"\s*,\s*"tool"\s*:\s*"([^"]+)"\s*,\s*"parameters"\s*:\s*(\{[^}]*\})\s*\}',
|
|
146
|
+
r'\{"tool"\s*:\s*"([^"]+)"\s*,\s*"args"\s*:\s*(\{[^}]*\})\s*\}',
|
|
147
|
+
r'\{"tool"\s*:\s*"([^"]+)"\s*,\s*"parameters"\s*:\s*(\{[^}]*\})\s*\}',
|
|
148
|
+
r'\{"name"\s*:\s*"([^"]+)"\s*,\s*"arguments"\s*:\s*(\{[^}]*\})\s*\}',
|
|
149
|
+
]
|
|
150
|
+
for pattern in patterns:
|
|
151
|
+
for match in re.finditer(pattern, text):
|
|
152
|
+
tool_name = match.group(1)
|
|
153
|
+
args_str = match.group(2)
|
|
154
|
+
if tool_name in known_tools:
|
|
155
|
+
try:
|
|
156
|
+
args = json.loads(args_str)
|
|
157
|
+
except json.JSONDecodeError:
|
|
158
|
+
args = {}
|
|
159
|
+
calls.append(ToolCall(
|
|
160
|
+
name=tool_name,
|
|
161
|
+
arguments=args,
|
|
162
|
+
source_format="json_protocol",
|
|
163
|
+
confidence=0.95,
|
|
164
|
+
))
|
|
165
|
+
idx = 0
|
|
166
|
+
while idx < len(text):
|
|
167
|
+
brace_pos = text.find("{", idx)
|
|
168
|
+
if brace_pos == -1:
|
|
169
|
+
break
|
|
170
|
+
obj_str = _find_balanced_json(text, brace_pos)
|
|
171
|
+
if not obj_str:
|
|
172
|
+
idx = brace_pos + 1
|
|
173
|
+
continue
|
|
174
|
+
try:
|
|
175
|
+
obj = json.loads(obj_str)
|
|
176
|
+
except json.JSONDecodeError:
|
|
177
|
+
idx = brace_pos + 1
|
|
178
|
+
continue
|
|
179
|
+
if isinstance(obj, dict):
|
|
180
|
+
action = obj.get("action", "")
|
|
181
|
+
tool = obj.get("tool", obj.get("name", ""))
|
|
182
|
+
args = obj.get("args", obj.get("parameters", obj.get("arguments", {})))
|
|
183
|
+
if action == "tool" and tool in known_tools:
|
|
184
|
+
calls.append(ToolCall(
|
|
185
|
+
name=tool,
|
|
186
|
+
arguments=args if isinstance(args, dict) else {},
|
|
187
|
+
source_format="json_protocol_action",
|
|
188
|
+
confidence=0.9,
|
|
189
|
+
))
|
|
190
|
+
elif action in known_tools:
|
|
191
|
+
calls.append(ToolCall(
|
|
192
|
+
name=action,
|
|
193
|
+
arguments={k: v for k, v in obj.items() if k != "action"},
|
|
194
|
+
source_format="json_protocol_action",
|
|
195
|
+
confidence=0.85,
|
|
196
|
+
))
|
|
197
|
+
elif tool in known_tools and tool:
|
|
198
|
+
calls.append(ToolCall(
|
|
199
|
+
name=tool,
|
|
200
|
+
arguments=args if isinstance(args, dict) else {},
|
|
201
|
+
source_format="json_protocol",
|
|
202
|
+
confidence=0.8,
|
|
203
|
+
))
|
|
204
|
+
idx = brace_pos + len(obj_str)
|
|
205
|
+
return calls
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _extract_xml_params(block):
|
|
209
|
+
params = {}
|
|
210
|
+
for m in re.finditer(r'<parameter\s+name="([^"]+)">(.*?)</parameter>', block, re.DOTALL):
|
|
211
|
+
params[m.group(1).strip()] = m.group(2).strip()
|
|
212
|
+
return params
|
|
213
|
+
|
|
214
|
+
def _parse_xml_tool_calls(text, known_tools):
|
|
215
|
+
calls = []; seen = set()
|
|
216
|
+
|
|
217
|
+
ire = re.compile(r'<invoke\s+name="([^"]+)">(.*?)</invoke>', re.DOTALL)
|
|
218
|
+
for m in ire.finditer(text):
|
|
219
|
+
rn = m.group(1).strip(); bl = m.group(2).strip()
|
|
220
|
+
tn = resolve_tool_name(rn); p = _extract_xml_params(bl)
|
|
221
|
+
if not p:
|
|
222
|
+
try: p = json.loads(bl)
|
|
223
|
+
except: p = {'raw': bl}
|
|
224
|
+
k = (tn, json.dumps(p, sort_keys=True))
|
|
225
|
+
if k in seen: continue; seen.add(k)
|
|
226
|
+
if tn in known_tools:
|
|
227
|
+
calls.append(ToolCall(name=tn, arguments=p, source_format='xml_invoke', confidence=0.9))
|
|
228
|
+
_log('TOOL_DETECTED: ' + tn + ' (xml_invoke)')
|
|
229
|
+
|
|
230
|
+
mmre = re.compile(r'<minimax:toolcall>(.*?)</minimax:toolcall>', re.DOTALL)
|
|
231
|
+
for m in mmre.finditer(text):
|
|
232
|
+
for im in ire.finditer(m.group(1)):
|
|
233
|
+
rn = im.group(1).strip(); bl = im.group(2).strip()
|
|
234
|
+
tn = resolve_tool_name(rn); p = _extract_xml_params(bl)
|
|
235
|
+
if not p:
|
|
236
|
+
try: p = json.loads(bl)
|
|
237
|
+
except: p = {'raw': bl}
|
|
238
|
+
k = (tn, json.dumps(p, sort_keys=True))
|
|
239
|
+
if k in seen: continue; seen.add(k)
|
|
240
|
+
if tn in known_tools:
|
|
241
|
+
calls.append(ToolCall(name=tn, arguments=p, source_format='xml_minimax', confidence=0.9))
|
|
242
|
+
|
|
243
|
+
tnre = re.compile(r'<tool_name>([^<]+)</tool_name>\s*(?:<parameters>|<arguments>)(.*?)(?:</parameters>|</arguments>)', re.DOTALL)
|
|
244
|
+
for m in tnre.finditer(text):
|
|
245
|
+
rn = m.group(1).strip(); ct = m.group(2).strip()
|
|
246
|
+
tn = resolve_tool_name(rn); k = (tn, ct)
|
|
247
|
+
if k in seen: continue; seen.add(k)
|
|
248
|
+
if tn in known_tools:
|
|
249
|
+
try: a = json.loads(ct)
|
|
250
|
+
except: a = _extract_xml_params(ct)
|
|
251
|
+
calls.append(ToolCall(name=tn, arguments=a if a else {'raw': ct}, source_format='xml_tool_name', confidence=0.85))
|
|
252
|
+
|
|
253
|
+
tare = re.compile(r'<tool>([^<]+)</tool>\s*<args>(.*?)</args>', re.DOTALL)
|
|
254
|
+
for m in tare.finditer(text):
|
|
255
|
+
rn = m.group(1).strip(); ct = m.group(2).strip()
|
|
256
|
+
tn = resolve_tool_name(rn); k = (tn, ct)
|
|
257
|
+
if k in seen: continue; seen.add(k)
|
|
258
|
+
if tn in known_tools:
|
|
259
|
+
try: a = json.loads(ct)
|
|
260
|
+
except: a = {'raw': ct}
|
|
261
|
+
calls.append(ToolCall(name=tn, arguments=a, source_format='xml_tool_args', confidence=0.85))
|
|
262
|
+
|
|
263
|
+
bre = re.compile(r'<invoke\s+name="([^"]+)">(.*?)$', re.DOTALL)
|
|
264
|
+
for m in bre.finditer(text):
|
|
265
|
+
rn = m.group(1).strip(); bl = m.group(2).strip()
|
|
266
|
+
tn = resolve_tool_name(rn); p = _extract_xml_params(bl)
|
|
267
|
+
if p:
|
|
268
|
+
k = (tn, json.dumps(p, sort_keys=True))
|
|
269
|
+
if k not in seen:
|
|
270
|
+
seen.add(k)
|
|
271
|
+
if tn in known_tools:
|
|
272
|
+
calls.append(ToolCall(name=tn, arguments=p, source_format='xml_bare_invoke', confidence=0.75))
|
|
273
|
+
|
|
274
|
+
# <tool_call><name>...</name><arguments>...</arguments></tool_call>
|
|
275
|
+
tc_tag_re = re.compile(r'<tool_call>\s*<name>([^<]+)</name>\s*<arguments>(.*?)</arguments>\s*</tool_call>', re.DOTALL)
|
|
276
|
+
for m in tc_tag_re.finditer(text):
|
|
277
|
+
rn = m.group(1).strip(); ct = m.group(2).strip()
|
|
278
|
+
tn = resolve_tool_name(rn); k = (tn, ct)
|
|
279
|
+
if k in seen: continue; seen.add(k)
|
|
280
|
+
if tn in known_tools:
|
|
281
|
+
try: a = json.loads(ct)
|
|
282
|
+
except: a = _extract_xml_params(ct)
|
|
283
|
+
calls.append(ToolCall(name=tn, arguments=a if a else {'raw': ct}, source_format='xml_tool_call_tag', confidence=0.9))
|
|
284
|
+
|
|
285
|
+
return calls
|
|
286
|
+
def _parse_function_call_syntax(text: str, known_tools: set) -> List[ToolCall]:
|
|
287
|
+
calls = []
|
|
288
|
+
pattern = r'(\w+)\s*\((.*?)\)'
|
|
289
|
+
for match in re.finditer(pattern, text, re.DOTALL):
|
|
290
|
+
func_name = match.group(1)
|
|
291
|
+
args_str = match.group(2).strip()
|
|
292
|
+
if func_name not in known_tools:
|
|
293
|
+
continue
|
|
294
|
+
args = {}
|
|
295
|
+
if args_str:
|
|
296
|
+
try:
|
|
297
|
+
args = json.loads("{" + args_str + "}")
|
|
298
|
+
except json.JSONDecodeError:
|
|
299
|
+
key_val_pattern = r'(\w+)\s*=\s*("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'|\d+(?:\.\d+)?|True|False|None|\[.*?\]|\{.*?\})'
|
|
300
|
+
for kv in re.finditer(key_val_pattern, args_str):
|
|
301
|
+
key = kv.group(1)
|
|
302
|
+
val_str = kv.group(2)
|
|
303
|
+
try:
|
|
304
|
+
val = json.loads(val_str)
|
|
305
|
+
except json.JSONDecodeError:
|
|
306
|
+
val = val_str.strip('"').strip("'")
|
|
307
|
+
args[key] = val
|
|
308
|
+
calls.append(ToolCall(
|
|
309
|
+
name=func_name,
|
|
310
|
+
arguments=args,
|
|
311
|
+
source_format="function_call",
|
|
312
|
+
confidence=0.75,
|
|
313
|
+
))
|
|
314
|
+
return calls
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _heuristic_extract(text: str, known_tools: set) -> List[ToolCall]:
|
|
318
|
+
calls = []
|
|
319
|
+
for tool in known_tools:
|
|
320
|
+
patterns = [
|
|
321
|
+
rf'(?:use|call|invoke|run|execute)\s+{re.escape(tool)}\s+(?:with|using|on)\s+(.+?)(?:\.|$|\n)',
|
|
322
|
+
rf'{re.escape(tool)}\s*\(([^)]*)\)',
|
|
323
|
+
rf'["\']?{re.escape(tool)}["\']?\s*:\s*(\{{[^}}]+\}})',
|
|
324
|
+
]
|
|
325
|
+
for pattern in patterns:
|
|
326
|
+
for match in re.finditer(pattern, text, re.IGNORECASE):
|
|
327
|
+
args_str = match.group(1).strip()
|
|
328
|
+
args = {}
|
|
329
|
+
try:
|
|
330
|
+
args = json.loads(args_str)
|
|
331
|
+
except (json.JSONDecodeError, ValueError):
|
|
332
|
+
if "=" in args_str:
|
|
333
|
+
for part in args_str.split(","):
|
|
334
|
+
if "=" in part:
|
|
335
|
+
k, v = part.split("=", 1)
|
|
336
|
+
args[k.strip()] = v.strip().strip("\"'")
|
|
337
|
+
calls.append(ToolCall(
|
|
338
|
+
name=tool,
|
|
339
|
+
arguments=args,
|
|
340
|
+
source_format="heuristic",
|
|
341
|
+
confidence=0.5,
|
|
342
|
+
))
|
|
343
|
+
return calls
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def normalize_tool_calls(response_text: str, known_tools: Optional[List[str]] = None) -> List[ToolCall]:
|
|
347
|
+
if not response_text or not response_text.strip():
|
|
348
|
+
return []
|
|
349
|
+
# Strip <think>...</think> and reasoning blocks before tool parsing so thoughts don't trigger false tools
|
|
350
|
+
response_text = re.sub(r"<think>.*?</think>", "", response_text, flags=re.DOTALL)
|
|
351
|
+
response_text = re.sub(r">\s*\*Thinking:\*.*?(?=\n\n|\Z)", "", response_text, flags=re.DOTALL)
|
|
352
|
+
tools_set = set(known_tools) if known_tools else set()
|
|
353
|
+
all_calls: List[ToolCall] = []
|
|
354
|
+
seen = set()
|
|
355
|
+
json_calls = _parse_json_protocol(response_text, tools_set)
|
|
356
|
+
for c in json_calls:
|
|
357
|
+
key = (c.name, json.dumps(c.arguments, sort_keys=True))
|
|
358
|
+
if key not in seen:
|
|
359
|
+
seen.add(key)
|
|
360
|
+
all_calls.append(c)
|
|
361
|
+
xml_calls = _parse_xml_tool_calls(response_text, tools_set)
|
|
362
|
+
for c in xml_calls:
|
|
363
|
+
key = (c.name, json.dumps(c.arguments, sort_keys=True))
|
|
364
|
+
if key not in seen:
|
|
365
|
+
seen.add(key)
|
|
366
|
+
all_calls.append(c)
|
|
367
|
+
func_calls = _parse_function_call_syntax(response_text, tools_set)
|
|
368
|
+
for c in func_calls:
|
|
369
|
+
key = (c.name, json.dumps(c.arguments, sort_keys=True))
|
|
370
|
+
if key not in seen:
|
|
371
|
+
seen.add(key)
|
|
372
|
+
all_calls.append(c)
|
|
373
|
+
if not all_calls and tools_set:
|
|
374
|
+
heuristic_calls = _heuristic_extract(response_text, tools_set)
|
|
375
|
+
for c in heuristic_calls:
|
|
376
|
+
key = (c.name, json.dumps(c.arguments, sort_keys=True))
|
|
377
|
+
if key not in seen:
|
|
378
|
+
seen.add(key)
|
|
379
|
+
all_calls.append(c)
|
|
380
|
+
all_calls.sort(key=lambda c: c.confidence, reverse=True)
|
|
381
|
+
return all_calls
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def extract_final_answer(response_text: str) -> Optional[str]:
|
|
385
|
+
if not response_text:
|
|
386
|
+
return None
|
|
387
|
+
patterns = [
|
|
388
|
+
r'(?:final|answer|result|output)\s*(?:is|:)\s*["\']?(.+?)["\']?\s*(?:\.|$|\n)',
|
|
389
|
+
r'(?:the)\s+(?:answer|result|final)\s+(?:is|:)\s*(.+?)(?:\.|$|\n)',
|
|
390
|
+
r'(?:ANSWER|RESULT|FINAL)\s*[:=]\s*(.+?)(?:\.|$|\n)',
|
|
391
|
+
]
|
|
392
|
+
for pattern in patterns:
|
|
393
|
+
match = re.search(pattern, response_text, re.IGNORECASE)
|
|
394
|
+
if match:
|
|
395
|
+
return match.group(1).strip()
|
|
396
|
+
return response_text.strip() if len(response_text.strip()) < 500 else None
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def is_tool_call_response(response_text: str, known_tools: Optional[List[str]] = None) -> bool:
|
|
400
|
+
if not response_text:
|
|
401
|
+
return False
|
|
402
|
+
tool_indicators = [
|
|
403
|
+
r'"action"\s*:\s*"tool"',
|
|
404
|
+
r'"tool"\s*:\s*"[^"]+"',
|
|
405
|
+
r'"name"\s*:\s*"[^"]+"\s*,\s*"arguments"',
|
|
406
|
+
r'<minimax:toolcall>',
|
|
407
|
+
r'<tool_call>',
|
|
408
|
+
r'<invoke\s+name=',
|
|
409
|
+
]
|
|
410
|
+
for indicator in tool_indicators:
|
|
411
|
+
if re.search(indicator, response_text):
|
|
412
|
+
return True
|
|
413
|
+
if known_tools:
|
|
414
|
+
tools_set = set(known_tools)
|
|
415
|
+
func_pattern = r'(\w+)\s*\('
|
|
416
|
+
for match in re.finditer(func_pattern, response_text):
|
|
417
|
+
if match.group(1) in tools_set:
|
|
418
|
+
return True
|
|
419
|
+
return False
|
calc_terminal/tui.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CCT TUI Dashboard — a real Textual application (actual widgets, actual
|
|
3
|
+
panes, actual layout engine — not raw print() dressed up to look like
|
|
4
|
+
one) offering a genuine split-pane view: a command sidebar, a scrolling
|
|
5
|
+
notebook-history feed, and a live status footer. This is optional and
|
|
6
|
+
additive: the primary interface stays the existing print()-based
|
|
7
|
+
terminal UI everywhere else, and /tui is one explicit command that
|
|
8
|
+
drops into this richer view and returns cleanly to the normal prompt
|
|
9
|
+
on quit.
|
|
10
|
+
|
|
11
|
+
Textual (like any full-screen TUI) needs a real attached terminal —
|
|
12
|
+
launching it against a pipe or non-tty stdin/stdout does not hang (it
|
|
13
|
+
fails fast), matching the same honesty precedent as keys.py's
|
|
14
|
+
stdin_is_interactive() guard for the raw-keypress live views.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from . import keys
|
|
18
|
+
|
|
19
|
+
TEXTUAL_AVAILABLE = True
|
|
20
|
+
try:
|
|
21
|
+
from textual.app import App, ComposeResult
|
|
22
|
+
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
23
|
+
from textual.widgets import Header, Footer, Static, Label
|
|
24
|
+
except Exception:
|
|
25
|
+
TEXTUAL_AVAILABLE = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if TEXTUAL_AVAILABLE:
|
|
29
|
+
|
|
30
|
+
class NotebookCard(Static):
|
|
31
|
+
def __init__(self, index, nb):
|
|
32
|
+
body = (
|
|
33
|
+
f"[b]{index}. {nb.get('topic', 'Notebook')}[/b]\n"
|
|
34
|
+
f"[dim]{nb.get('question', '')[:90]}[/dim]\n"
|
|
35
|
+
f"[green]{nb.get('final_answer', '')}[/green]"
|
|
36
|
+
)
|
|
37
|
+
super().__init__(body, classes="notebook-card")
|
|
38
|
+
|
|
39
|
+
class CCTDashboard(App):
|
|
40
|
+
"""Sidebar of quick actions + scrolling notebook history +
|
|
41
|
+
live status footer, all real Textual widgets/layout."""
|
|
42
|
+
|
|
43
|
+
CSS = """
|
|
44
|
+
Screen { background: $surface; }
|
|
45
|
+
#sidebar { width: 26; border-right: solid $accent; padding: 1; }
|
|
46
|
+
#main { padding: 1 2; }
|
|
47
|
+
.notebook-card { border: round $accent; padding: 1; margin-bottom: 1; }
|
|
48
|
+
#statusline { height: 1; color: $text-muted; }
|
|
49
|
+
"""
|
|
50
|
+
BINDINGS = [("q", "quit_dashboard", "Back to terminal"), ("r", "refresh", "Refresh")]
|
|
51
|
+
|
|
52
|
+
def __init__(self, history, stats):
|
|
53
|
+
super().__init__()
|
|
54
|
+
self._history = history
|
|
55
|
+
self._stats = stats
|
|
56
|
+
|
|
57
|
+
def compose(self) -> ComposeResult:
|
|
58
|
+
yield Header(show_clock=True)
|
|
59
|
+
with Horizontal():
|
|
60
|
+
with Vertical(id="sidebar"):
|
|
61
|
+
yield Label("[b]CAT[/b]")
|
|
62
|
+
yield Label("Chemistry Calc Terminal")
|
|
63
|
+
yield Label("")
|
|
64
|
+
yield Label("[b]Session[/b]")
|
|
65
|
+
yield Label(f"Solved: {self._stats.get('solved', 0)}")
|
|
66
|
+
yield Label(f"Provider: {self._stats.get('provider', 'none')}")
|
|
67
|
+
yield Label(f"Model: {self._stats.get('model', 'none')}")
|
|
68
|
+
yield Label("")
|
|
69
|
+
yield Label("[dim]q back to terminal[/dim]")
|
|
70
|
+
yield Label("[dim]r refresh[/dim]")
|
|
71
|
+
with VerticalScroll(id="main"):
|
|
72
|
+
if self._history:
|
|
73
|
+
for i, nb in enumerate(reversed(self._history), 1):
|
|
74
|
+
yield NotebookCard(len(self._history) - i + 1, nb)
|
|
75
|
+
else:
|
|
76
|
+
yield Static("No notebooks solved yet this session.")
|
|
77
|
+
yield Static(id="statusline")
|
|
78
|
+
yield Footer()
|
|
79
|
+
|
|
80
|
+
def on_mount(self):
|
|
81
|
+
self.query_one("#statusline", Static).update(
|
|
82
|
+
f" {len(self._history)} notebook(s) in session history")
|
|
83
|
+
|
|
84
|
+
def action_quit_dashboard(self):
|
|
85
|
+
self.exit()
|
|
86
|
+
|
|
87
|
+
def action_refresh(self):
|
|
88
|
+
self.refresh()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def launch_dashboard(history, stats):
|
|
92
|
+
"""Runs the Textual dashboard synchronously (blocks until the user
|
|
93
|
+
presses q or closes it), then returns control to the normal CLI
|
|
94
|
+
loop. Returns (ok, message)."""
|
|
95
|
+
if not TEXTUAL_AVAILABLE:
|
|
96
|
+
return False, "textual isn't installed — run: pip install textual"
|
|
97
|
+
if not keys.stdin_is_interactive():
|
|
98
|
+
return False, "The TUI dashboard needs a real attached terminal (not piped input)."
|
|
99
|
+
try:
|
|
100
|
+
app = CCTDashboard(history, stats)
|
|
101
|
+
app.run()
|
|
102
|
+
return True, ""
|
|
103
|
+
except Exception as e:
|
|
104
|
+
return False, f"Could not launch TUI dashboard: {e}"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""
|
|
2
|
+
calc_terminal.ui — the single owner of every interactive interface.
|
|
3
|
+
|
|
4
|
+
Contains only rendering + event dispatch: no chemistry logic, no AI
|
|
5
|
+
logic, no calculation logic. Business logic stays in the modules one
|
|
6
|
+
level up (aicore.py, permissions.py, session.py, engine.py, ...); this
|
|
7
|
+
package renders their state and turns user interaction into events.
|
|
8
|
+
"""
|