aicd 0.0.9__tar.gz
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.
- aicd-0.0.9/PKG-INFO +39 -0
- aicd-0.0.9/README.md +97 -0
- aicd-0.0.9/pyproject.toml +51 -0
- aicd-0.0.9/setup.cfg +4 -0
- aicd-0.0.9/src/aicd/__init__.py +3 -0
- aicd-0.0.9/src/aicd/__main__.py +8 -0
- aicd-0.0.9/src/aicd/agent/__init__.py +3 -0
- aicd-0.0.9/src/aicd/agent/chat_context.py +203 -0
- aicd-0.0.9/src/aicd/agent/chat_serialize.py +62 -0
- aicd-0.0.9/src/aicd/agent/handlers/__init__.py +5 -0
- aicd-0.0.9/src/aicd/agent/handlers/agent_settings.py +37 -0
- aicd-0.0.9/src/aicd/agent/handlers/ai_viz.py +143 -0
- aicd-0.0.9/src/aicd/agent/handlers/chat_history.py +162 -0
- aicd-0.0.9/src/aicd/agent/handlers/data_kit.py +68 -0
- aicd-0.0.9/src/aicd/agent/handlers/db_remote.py +72 -0
- aicd-0.0.9/src/aicd/agent/handlers/doc_browser_code.py +203 -0
- aicd-0.0.9/src/aicd/agent/handlers/drawio_tools.py +105 -0
- aicd-0.0.9/src/aicd/agent/handlers/fs.py +54 -0
- aicd-0.0.9/src/aicd/agent/handlers/media_tools.py +99 -0
- aicd-0.0.9/src/aicd/agent/handlers/registry.py +47 -0
- aicd-0.0.9/src/aicd/agent/handlers/runtime_env.py +31 -0
- aicd-0.0.9/src/aicd/agent/handlers/tasks.py +479 -0
- aicd-0.0.9/src/aicd/agent/handlers/text_process.py +129 -0
- aicd-0.0.9/src/aicd/agent/handlers/validate_tools.py +34 -0
- aicd-0.0.9/src/aicd/agent/handlers/vision.py +122 -0
- aicd-0.0.9/src/aicd/agent/idle_pulse.py +29 -0
- aicd-0.0.9/src/aicd/agent/llm.py +305 -0
- aicd-0.0.9/src/aicd/agent/loop.py +639 -0
- aicd-0.0.9/src/aicd/agent/prompts.py +177 -0
- aicd-0.0.9/src/aicd/agent/tool_args_coerce.py +109 -0
- aicd-0.0.9/src/aicd/agent/tool_router.py +175 -0
- aicd-0.0.9/src/aicd/agent/tools_openai.py +1020 -0
- aicd-0.0.9/src/aicd/api_serve.py +119 -0
- aicd-0.0.9/src/aicd/app.py +192 -0
- aicd-0.0.9/src/aicd/bus/__init__.py +3 -0
- aicd-0.0.9/src/aicd/bus/event_bus.py +31 -0
- aicd-0.0.9/src/aicd/cli_encoding.py +32 -0
- aicd-0.0.9/src/aicd/config.py +401 -0
- aicd-0.0.9/src/aicd/config_settings.py +309 -0
- aicd-0.0.9/src/aicd/db/__init__.py +3 -0
- aicd-0.0.9/src/aicd/db/schema.py +54 -0
- aicd-0.0.9/src/aicd/db/store.py +483 -0
- aicd-0.0.9/src/aicd/env_probe.py +491 -0
- aicd-0.0.9/src/aicd/home.py +55 -0
- aicd-0.0.9/src/aicd/kit/__init__.py +14 -0
- aicd-0.0.9/src/aicd/kit/chart_spec.py +120 -0
- aicd-0.0.9/src/aicd/kit/crypto_codec.py +673 -0
- aicd-0.0.9/src/aicd/kit/drawio_spec.py +217 -0
- aicd-0.0.9/src/aicd/kit/format_data.py +357 -0
- aicd-0.0.9/src/aicd/kit/math_stats.py +117 -0
- aicd-0.0.9/src/aicd/kit/script_syntax.py +206 -0
- aicd-0.0.9/src/aicd/kit/script_templates.py +80 -0
- aicd-0.0.9/src/aicd/kit/text_regex.py +100 -0
- aicd-0.0.9/src/aicd/kit/time_log.py +83 -0
- aicd-0.0.9/src/aicd/kit/validate_viz.py +282 -0
- aicd-0.0.9/src/aicd/res/README.txt +8 -0
- aicd-0.0.9/src/aicd/res/chart.umd.min.js +20 -0
- aicd-0.0.9/src/aicd/res/mermaid.min.js +2314 -0
- aicd-0.0.9/src/aicd/res/vis-network.min.css +2 -0
- aicd-0.0.9/src/aicd/res/vis-network.min.js +34 -0
- aicd-0.0.9/src/aicd/runtime.py +343 -0
- aicd-0.0.9/src/aicd/runtime_task_context.py +7 -0
- aicd-0.0.9/src/aicd/session_log.py +881 -0
- aicd-0.0.9/src/aicd/setup_wizard.py +225 -0
- aicd-0.0.9/src/aicd/tasks/__init__.py +3 -0
- aicd-0.0.9/src/aicd/tasks/manager.py +588 -0
- aicd-0.0.9/src/aicd/tasks/safe_ids.py +27 -0
- aicd-0.0.9/src/aicd/tools/__init__.py +1 -0
- aicd-0.0.9/src/aicd/tools/browser_cdp.py +31 -0
- aicd-0.0.9/src/aicd/tools/code_intel.py +52 -0
- aicd-0.0.9/src/aicd/tools/db_multi.py +543 -0
- aicd-0.0.9/src/aicd/tools/docs_convert.py +468 -0
- aicd-0.0.9/src/aicd/tools/document_outline.py +116 -0
- aicd-0.0.9/src/aicd/tools/docx_compose.py +433 -0
- aicd-0.0.9/src/aicd/tools/docx_to_pdf.py +301 -0
- aicd-0.0.9/src/aicd/tools/drawio_embed_export.py +362 -0
- aicd-0.0.9/src/aicd/tools/fs_tool.py +185 -0
- aicd-0.0.9/src/aicd/tools/http_tool.py +53 -0
- aicd-0.0.9/src/aicd/tools/image_crop.py +71 -0
- aicd-0.0.9/src/aicd/tools/image_transform.py +861 -0
- aicd-0.0.9/src/aicd/tools/mermaid_png.py +155 -0
- aicd-0.0.9/src/aicd/tools/path_policy.py +51 -0
- aicd-0.0.9/src/aicd/tools/process_tool.py +280 -0
- aicd-0.0.9/src/aicd/tools/script_tool.py +97 -0
- aicd-0.0.9/src/aicd/tools/text_tool.py +50 -0
- aicd-0.0.9/src/aicd/tools/viz_assets.py +185 -0
- aicd-0.0.9/src/aicd/ui/__init__.py +3 -0
- aicd-0.0.9/src/aicd/ui/cli_headless.py +81 -0
- aicd-0.0.9/src/aicd/ui/slash_commands.py +143 -0
- aicd-0.0.9/src/aicd/ui/slash_suggester.py +82 -0
- aicd-0.0.9/src/aicd/ui/tui.py +1048 -0
- aicd-0.0.9/src/aicd/version.py +25 -0
- aicd-0.0.9/src/aicd.egg-info/PKG-INFO +39 -0
- aicd-0.0.9/src/aicd.egg-info/SOURCES.txt +138 -0
- aicd-0.0.9/src/aicd.egg-info/dependency_links.txt +1 -0
- aicd-0.0.9/src/aicd.egg-info/entry_points.txt +2 -0
- aicd-0.0.9/src/aicd.egg-info/requires.txt +40 -0
- aicd-0.0.9/src/aicd.egg-info/top_level.txt +1 -0
- aicd-0.0.9/tests/test_agent_loop_chat_strip.py +27 -0
- aicd-0.0.9/tests/test_api_serve.py +46 -0
- aicd-0.0.9/tests/test_app_cli.py +12 -0
- aicd-0.0.9/tests/test_chat_context_openai_valid.py +81 -0
- aicd-0.0.9/tests/test_chat_serialize.py +36 -0
- aicd-0.0.9/tests/test_chat_store.py +119 -0
- aicd-0.0.9/tests/test_cli_encoding.py +26 -0
- aicd-0.0.9/tests/test_config_agent_llm.py +79 -0
- aicd-0.0.9/tests/test_crypto_format_kit.py +108 -0
- aicd-0.0.9/tests/test_db_multi.py +100 -0
- aicd-0.0.9/tests/test_docs_convert.py +139 -0
- aicd-0.0.9/tests/test_document_outline.py +40 -0
- aicd-0.0.9/tests/test_docx_compose.py +56 -0
- aicd-0.0.9/tests/test_docx_style_rpr.py +37 -0
- aicd-0.0.9/tests/test_docx_to_pdf.py +79 -0
- aicd-0.0.9/tests/test_drawio_export_png.py +118 -0
- aicd-0.0.9/tests/test_drawio_spec.py +24 -0
- aicd-0.0.9/tests/test_env_probe.py +59 -0
- aicd-0.0.9/tests/test_home_session.py +16 -0
- aicd-0.0.9/tests/test_image_crop.py +46 -0
- aicd-0.0.9/tests/test_image_transform.py +115 -0
- aicd-0.0.9/tests/test_kit_chart_spec.py +28 -0
- aicd-0.0.9/tests/test_kit_math_stats.py +29 -0
- aicd-0.0.9/tests/test_kit_text_regex.py +27 -0
- aicd-0.0.9/tests/test_kit_time_log.py +18 -0
- aicd-0.0.9/tests/test_llm_retry.py +52 -0
- aicd-0.0.9/tests/test_mermaid_png.py +38 -0
- aicd-0.0.9/tests/test_process_shell.py +52 -0
- aicd-0.0.9/tests/test_session_log.py +64 -0
- aicd-0.0.9/tests/test_shell_python_c_warn.py +54 -0
- aicd-0.0.9/tests/test_slash_commands.py +19 -0
- aicd-0.0.9/tests/test_slash_suggester.py +26 -0
- aicd-0.0.9/tests/test_smoke.py +37 -0
- aicd-0.0.9/tests/test_startup_banner.py +59 -0
- aicd-0.0.9/tests/test_task_collab.py +56 -0
- aicd-0.0.9/tests/test_task_timing.py +20 -0
- aicd-0.0.9/tests/test_tool_args_coerce.py +53 -0
- aicd-0.0.9/tests/test_tool_router_kit.py +44 -0
- aicd-0.0.9/tests/test_validate_viz.py +50 -0
- aicd-0.0.9/tests/test_version.py +22 -0
- aicd-0.0.9/tests/test_vision_analyze.py +95 -0
- aicd-0.0.9/tests/test_viz_assets.py +48 -0
aicd-0.0.9/PKG-INFO
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aicd
|
|
3
|
+
Version: 0.0.9
|
|
4
|
+
Summary: CLI AI Agent for code and documentation
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: typer>=0.9.0
|
|
7
|
+
Requires-Dist: textual>=0.47.0
|
|
8
|
+
Requires-Dist: rich>=13.0.0
|
|
9
|
+
Requires-Dist: httpx>=0.27.0
|
|
10
|
+
Requires-Dist: openai>=1.40.0
|
|
11
|
+
Requires-Dist: pyyaml>=6.0.1
|
|
12
|
+
Requires-Dist: aiosqlite>=0.19.0
|
|
13
|
+
Requires-Dist: pymupdf>=1.24.0
|
|
14
|
+
Requires-Dist: python-docx>=1.1.0
|
|
15
|
+
Requires-Dist: openpyxl>=3.1.0
|
|
16
|
+
Requires-Dist: python-pptx>=0.6.23
|
|
17
|
+
Requires-Dist: Pillow>=10.0.0
|
|
18
|
+
Requires-Dist: numpy>=1.24.0
|
|
19
|
+
Requires-Dist: mammoth>=1.8.0
|
|
20
|
+
Requires-Dist: markdownify>=0.13.0
|
|
21
|
+
Requires-Dist: playwright>=1.45.0
|
|
22
|
+
Requires-Dist: cryptography>=42.0.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
25
|
+
Requires-Dist: build>=1.2.0; extra == "dev"
|
|
26
|
+
Requires-Dist: fastapi>=0.115.0; extra == "dev"
|
|
27
|
+
Requires-Dist: uvicorn[standard]>=0.32.0; extra == "dev"
|
|
28
|
+
Provides-Extra: db
|
|
29
|
+
Requires-Dist: asyncpg>=0.29.0; extra == "db"
|
|
30
|
+
Requires-Dist: aiomysql>=0.2.0; extra == "db"
|
|
31
|
+
Provides-Extra: api
|
|
32
|
+
Requires-Dist: fastapi>=0.115.0; extra == "api"
|
|
33
|
+
Requires-Dist: uvicorn[standard]>=0.32.0; extra == "api"
|
|
34
|
+
Provides-Extra: ocr
|
|
35
|
+
Requires-Dist: pytesseract>=0.3.10; extra == "ocr"
|
|
36
|
+
Provides-Extra: opencv
|
|
37
|
+
Requires-Dist: opencv-python-headless>=4.8.0; extra == "opencv"
|
|
38
|
+
Provides-Extra: pdf
|
|
39
|
+
Requires-Dist: docx2pdf>=0.1.8; extra == "pdf"
|
aicd-0.0.9/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# aicd
|
|
2
|
+
|
|
3
|
+
命令行 AI Agent:聊天驱动文件、进程、HTTP、任务与子 Agent。Windows 优先,预留 Linux。
|
|
4
|
+
|
|
5
|
+
本工具借助大模型,在对话中完成**由代码生成文档**与**由文档生成代码**的闭环,并对项目中的文档与源码做**深入分析与理解**,便于梳理设计、落地实现与持续维护。
|
|
6
|
+
|
|
7
|
+
## 环境
|
|
8
|
+
|
|
9
|
+
```powershell
|
|
10
|
+
cd <本仓库根目录>
|
|
11
|
+
python -m venv .venv
|
|
12
|
+
.\.venv\Scripts\Activate.ps1
|
|
13
|
+
pip install -e .
|
|
14
|
+
# 可选:Agent 访问 PostgreSQL / MySQL(db_inspect / db_query)
|
|
15
|
+
pip install -e ".[db]"
|
|
16
|
+
# 可选:只读聊天历史 HTTP API(aicd serve)
|
|
17
|
+
pip install -e ".[api]"
|
|
18
|
+
# 可选:PDF 整页 OCR(doc_extract use_ocr)
|
|
19
|
+
pip install -e ".[ocr]"
|
|
20
|
+
# 可选:浏览器自动化
|
|
21
|
+
playwright install chromium
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 配置(config.yaml)
|
|
25
|
+
|
|
26
|
+
- **`llm`**:`apiKey` / `baseUrl` / `modelId`;可选 **`visionModel`**(多模态专用,缺省沿用主模型);**`timeoutSec`**;**`maxRetries`**(单次请求失败后额外重试次数,0–20,默认 3);**`retryBaseDelaySec`**(退避基数秒,默认 1.0);采样字段 `temperature`、`topP` 等。
|
|
27
|
+
- **`agent`**:**`maxToolRounds`** / **`subAgentMaxToolRounds`** 各 **1–500**(超出会截断);**`contextBudgetTokens`**、**`chatHistoryMaxMessages`**(启动时从 SQLite 恢复尾部消息再按 token 预算裁剪)。
|
|
28
|
+
- **`paths`**:`full_disk`、`allowed_roots` 等(见代码内默认值)。
|
|
29
|
+
|
|
30
|
+
环境变量 **`AICD_HOME`**:应用数据目录。未设置且未使用 `--home` 时,使用 **`%USERPROFILE%\.aicd`**(Windows)。其下 **`logs/`** 每次 `aicd tui` 会新建时间戳日志;LLM 重试会写入会话日志。
|
|
31
|
+
|
|
32
|
+
## 命令行
|
|
33
|
+
|
|
34
|
+
| 命令 | 说明 |
|
|
35
|
+
|------|------|
|
|
36
|
+
| `aicd init [--home DIR]` | 初始化 HOME、`config.yaml`、SQLite |
|
|
37
|
+
| `aicd tui [--home DIR] [--work DIR]` | TUI;**`--work`** 启动后立刻将 AI 工作区设为该目录(等同 `/work`) |
|
|
38
|
+
| `aicd serve [--home DIR] [--host] [--port] [--api-key]` | 只读聊天 API(需 **`pip install -e ".[api]"`**);鉴权见下文 |
|
|
39
|
+
| `aicd version` | 打印版本 |
|
|
40
|
+
|
|
41
|
+
TUI 斜杠:**`/work`**、**`/doc`**、**`/agent`**、**`/llm`**(含 `maxRetries`、`retryBaseDelaySec`)、**`/task`**、**`/exit`**。
|
|
42
|
+
|
|
43
|
+
## Agent 能力与工具(摘要)
|
|
44
|
+
|
|
45
|
+
- **工作区**:主会话须用工具 **`ai_workspace_set`** 切换项目目录(**`run_command cd` 无效**);TUI 用 **`/work`**。
|
|
46
|
+
- **多模态**:**`vision_analyze_image`** — 将本地图片送视觉模型解析图表/图中文字;默认 **`save_report`** 把结果写入 **AI tmp**(`ai_output_dir/tmp`),便于后续 **`fs_read_file`**。发送前会对大图做长边与体积压缩(默认长边 ≤1920、编码目标 ≤1MiB 等,见 `llm.image_file_to_data_url`)。
|
|
47
|
+
- **文档**:**`doc_extract`**(PDF/DOCX/PPTX 等);**`document_outline`** — 对 `.md`/`.html`/`.txt` 提取标题树、**行号**与 **UTF-8 字节偏移**,默认写入 **`tmp/outlines/<stem>.json`**,便于大文档分块阅读。
|
|
48
|
+
- **超大上下文策略**:系统提示中的 **LARGE_CONTEXT** 约定:先检索/目录再 **`fs_read_file` offset/limit**、在 **`ai_tmp`** 写清单与 **`session_focus.md`**(稳定事实 vs 工作假设)、大输出落盘、必要时 **`task_ai`** / **`chat_save_summary`** / **`chat_history_*`**。
|
|
49
|
+
- **聊天持久化**:消息与摘要存 SQLite;**`aicd serve`** 提供只读 HTTP 拉取(与 TUI 共用同一库)。
|
|
50
|
+
- **主 / 子任务闭环**:复杂工作优先 **`task_ai`**。主会话可在旧子任务仍 **`running`** 时继续发话;若新意图属于同一子任务,主 AI 应 **`task_message_send`**(**`kind`**: **`directive`**,**`body`**: `{"text":"..."}`,主会话可省略 **`from_task_id`**,库内记为 **`main-<session_id>`**)。子 Agent 在每段 LLM 工具循环结束后**自动拉取收件箱**;有未读则合并为新的 **user** 轮次并继续,**无新消息则任务结束**。累计 LLM 轮次有上限(约 `max(subAgentMaxToolRounds*6, 300)`),防无限循环。相关工具:**`task_list`** / **`task_result`**(**timeline**)、**`task_thread_stats`**。
|
|
51
|
+
|
|
52
|
+
## 只读聊天 HTTP API(`aicd serve`)
|
|
53
|
+
|
|
54
|
+
需安装 **`aicd[api]`**。默认监听 **`127.0.0.1:8765`**(可用 `--host` / `--port` 修改)。
|
|
55
|
+
|
|
56
|
+
- **`GET /health`** → `{"ok": true}`
|
|
57
|
+
- **`GET /v1/sessions/{session_id}/messages?limit=&offset=`** — 分页消息(`limit` 1–2000)
|
|
58
|
+
- **`GET /v1/sessions/{session_id}/summaries?limit=`** — 会话摘要列表
|
|
59
|
+
|
|
60
|
+
若启动时传入 **`--api-key`** 或环境变量 **`AICD_API_KEY`**,则请求须带 **`X-API-Key`** 或 **`Authorization: Bearer <token>`**。
|
|
61
|
+
|
|
62
|
+
## 离线可视化(导出 HTML)
|
|
63
|
+
|
|
64
|
+
包内自带 `src/aicd/res/`:**Chart.js**、**Mermaid**、**vis-network**(含 CSS)。升级或重装依赖可执行:
|
|
65
|
+
|
|
66
|
+
```powershell
|
|
67
|
+
python scripts/download_viz_res.py
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Agent 工具:`viz_export_html`、`viz_data_chart`、`viz_copy_resources`、`viz_list_bundled_assets`;导出前可用 **`viz_spec_validate`**。
|
|
71
|
+
|
|
72
|
+
## 分析函数库(kit)
|
|
73
|
+
|
|
74
|
+
`src/aicd/kit/` 提供 JSON 友好的纯函数,并由聚合工具暴露:`data_stats`、`data_text`、`data_time`、`script_template`(骨架 + `script_run`)。实现与路由见 `src/aicd/agent/handlers/`。
|
|
75
|
+
|
|
76
|
+
## Cursor 技能(`.cursor/skills/`)
|
|
77
|
+
|
|
78
|
+
仓库内包含若干 **SKILL.md**(工作区、输出目录、聊天历史、大仓库索引 **aicd-code-index**、Draw.io、可视化校验、Analytics、DB 等),便于在 Cursor 中改代码时对齐行为。
|
|
79
|
+
|
|
80
|
+
## 版本号
|
|
81
|
+
|
|
82
|
+
- 当前版本见 `src/aicd/version.py` 中的 **`VERSION`**(`pyproject.toml` 动态读取)。
|
|
83
|
+
- **每次合入/发布前**在仓库根目录执行:`python scripts/bump_version.py`。
|
|
84
|
+
- **上传 PyPI 前**先构建再自检:`python scripts/package.py --clean`,然后 `python scripts/release_precheck.py`(可加 `--strict` 做更严的 apiKey 字面量检查);通过后再 `python -m twine upload dist\aicd-*`。
|
|
85
|
+
|
|
86
|
+
## 代码结构(速查)
|
|
87
|
+
|
|
88
|
+
| 路径 | 作用 |
|
|
89
|
+
|------|------|
|
|
90
|
+
| `src/aicd/app.py` | Typer 入口:`tui` / `init` / `serve` / `version` |
|
|
91
|
+
| `src/aicd/runtime.py` | `build_runtime`、工作区/输出目录、`set_vision_llm` / `attach_session_log` |
|
|
92
|
+
| `src/aicd/agent/loop.py` | 主对话与子 Agent 工具循环 |
|
|
93
|
+
| `src/aicd/agent/llm.py` | OpenAI 兼容客户端、**重试**、**vision 图片编码** |
|
|
94
|
+
| `src/aicd/agent/prompts.py` | 系统提示(含 **LARGE_CONTEXT**) |
|
|
95
|
+
| `src/aicd/api_serve.py` | FastAPI 只读聊天 API |
|
|
96
|
+
| `src/aicd/tools/document_outline.py` | 文档标题索引 |
|
|
97
|
+
| `src/aicd/config.py` | **`MAX_AGENT_TOOL_ROUNDS`**、**`LLMConfig`** |
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aicd"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "CLI AI Agent for code and documentation"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"typer>=0.9.0",
|
|
12
|
+
"textual>=0.47.0",
|
|
13
|
+
"rich>=13.0.0",
|
|
14
|
+
"httpx>=0.27.0",
|
|
15
|
+
"openai>=1.40.0",
|
|
16
|
+
"pyyaml>=6.0.1",
|
|
17
|
+
"aiosqlite>=0.19.0",
|
|
18
|
+
"pymupdf>=1.24.0",
|
|
19
|
+
"python-docx>=1.1.0",
|
|
20
|
+
"openpyxl>=3.1.0",
|
|
21
|
+
"python-pptx>=0.6.23",
|
|
22
|
+
"Pillow>=10.0.0",
|
|
23
|
+
"numpy>=1.24.0",
|
|
24
|
+
"mammoth>=1.8.0",
|
|
25
|
+
"markdownify>=0.13.0",
|
|
26
|
+
"playwright>=1.45.0",
|
|
27
|
+
"cryptography>=42.0.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
aicd = "aicd.app:main"
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = ["pytest>=8", "build>=1.2.0", "fastapi>=0.115.0", "uvicorn[standard]>=0.32.0"]
|
|
35
|
+
db = ["asyncpg>=0.29.0", "aiomysql>=0.2.0"]
|
|
36
|
+
api = ["fastapi>=0.115.0", "uvicorn[standard]>=0.32.0"]
|
|
37
|
+
ocr = ["pytesseract>=0.3.10"]
|
|
38
|
+
opencv = ["opencv-python-headless>=4.8.0"]
|
|
39
|
+
pdf = ["docx2pdf>=0.1.8"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.dynamic]
|
|
42
|
+
version = { attr = "aicd.version.VERSION" }
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ["src"]
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.package-data]
|
|
48
|
+
aicd = ["res/*.js", "res/*.css", "res/*.txt"]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
pythonpath = ["src"]
|
aicd-0.0.9/setup.cfg
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""按消息条数与粗算 token 预算裁剪历史,用于启动时恢复上下文。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from aicd.agent.chat_serialize import estimate_message_tokens
|
|
9
|
+
from aicd.db.store import ChatMessageRow
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _tool_call_ids_from_assistant(msg: dict[str, Any]) -> list[str]:
|
|
13
|
+
tcs = msg.get("tool_calls") or []
|
|
14
|
+
if not isinstance(tcs, list):
|
|
15
|
+
return []
|
|
16
|
+
ids: list[str] = []
|
|
17
|
+
for tc in tcs:
|
|
18
|
+
if not isinstance(tc, dict):
|
|
19
|
+
continue
|
|
20
|
+
tid = tc.get("id")
|
|
21
|
+
if tid:
|
|
22
|
+
ids.append(str(tid))
|
|
23
|
+
return ids
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def openai_message_list_valid(msgs: list[dict[str, Any]]) -> bool:
|
|
27
|
+
"""
|
|
28
|
+
检查消息序列是否满足 OpenAI 兼容 API 约定:
|
|
29
|
+
无首部孤立 ``tool``;每条带 ``tool_calls`` 的 ``assistant`` 后须紧跟对应数量的 ``tool``,
|
|
30
|
+
且 ``tool_call_id`` 与 ``tool_calls[].id`` 一致(多重集)。
|
|
31
|
+
"""
|
|
32
|
+
if not msgs:
|
|
33
|
+
return True
|
|
34
|
+
if msgs[0].get("role") == "tool":
|
|
35
|
+
return False
|
|
36
|
+
i = 0
|
|
37
|
+
while i < len(msgs):
|
|
38
|
+
role = msgs[i].get("role")
|
|
39
|
+
if role == "tool":
|
|
40
|
+
return False
|
|
41
|
+
if role == "system":
|
|
42
|
+
i += 1
|
|
43
|
+
continue
|
|
44
|
+
if role == "user":
|
|
45
|
+
i += 1
|
|
46
|
+
continue
|
|
47
|
+
if role == "assistant":
|
|
48
|
+
ids = _tool_call_ids_from_assistant(msgs[i])
|
|
49
|
+
if not ids:
|
|
50
|
+
i += 1
|
|
51
|
+
continue
|
|
52
|
+
need = len(ids)
|
|
53
|
+
want = sorted(ids)
|
|
54
|
+
got: list[str] = []
|
|
55
|
+
for k in range(need):
|
|
56
|
+
j = i + 1 + k
|
|
57
|
+
if j >= len(msgs):
|
|
58
|
+
return False
|
|
59
|
+
tm = msgs[j]
|
|
60
|
+
if tm.get("role") != "tool":
|
|
61
|
+
return False
|
|
62
|
+
got.append(str(tm.get("tool_call_id") or ""))
|
|
63
|
+
if sorted(got) != want:
|
|
64
|
+
return False
|
|
65
|
+
i += 1 + need
|
|
66
|
+
continue
|
|
67
|
+
return False
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def repair_openai_chat_messages(messages: list[dict[str, Any]]) -> None:
|
|
72
|
+
"""
|
|
73
|
+
原地修复:去掉首部孤立 ``tool``;去掉不完整 tool 轮次(含仅跟了部分 ``tool`` 的 ``assistant``);
|
|
74
|
+
去掉尾部仍含 ``tool_calls`` 且无对应 ``tool`` 的 ``assistant``。
|
|
75
|
+
"""
|
|
76
|
+
changed = True
|
|
77
|
+
while changed and messages:
|
|
78
|
+
changed = False
|
|
79
|
+
while messages and messages[0].get("role") == "tool":
|
|
80
|
+
messages.pop(0)
|
|
81
|
+
changed = True
|
|
82
|
+
i = 0
|
|
83
|
+
while i < len(messages):
|
|
84
|
+
m = messages[i]
|
|
85
|
+
if m.get("role") != "assistant":
|
|
86
|
+
i += 1
|
|
87
|
+
continue
|
|
88
|
+
ids = _tool_call_ids_from_assistant(m)
|
|
89
|
+
if not ids:
|
|
90
|
+
i += 1
|
|
91
|
+
continue
|
|
92
|
+
need = len(ids)
|
|
93
|
+
j = i + 1
|
|
94
|
+
tool_roles = 0
|
|
95
|
+
while j < len(messages) and tool_roles < need:
|
|
96
|
+
if messages[j].get("role") != "tool":
|
|
97
|
+
break
|
|
98
|
+
tool_roles += 1
|
|
99
|
+
j += 1
|
|
100
|
+
if tool_roles < need:
|
|
101
|
+
del messages[i:j]
|
|
102
|
+
changed = True
|
|
103
|
+
continue
|
|
104
|
+
got = [str(messages[i + 1 + k].get("tool_call_id") or "") for k in range(need)]
|
|
105
|
+
if sorted(got) != sorted(ids):
|
|
106
|
+
del messages[i:j]
|
|
107
|
+
changed = True
|
|
108
|
+
continue
|
|
109
|
+
i = j
|
|
110
|
+
while messages:
|
|
111
|
+
last = messages[-1]
|
|
112
|
+
if last.get("role") == "assistant" and _tool_call_ids_from_assistant(last):
|
|
113
|
+
messages.pop()
|
|
114
|
+
changed = True
|
|
115
|
+
continue
|
|
116
|
+
break
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def longest_valid_openai_suffix(
|
|
120
|
+
body: list[dict[str, Any]], max_len: int
|
|
121
|
+
) -> list[dict[str, Any]]:
|
|
122
|
+
"""
|
|
123
|
+
取 ``body`` 的尾部子列表,长度不超过 ``max_len``,且对 API 合法;优先更长、更近的尾部。
|
|
124
|
+
"""
|
|
125
|
+
n = len(body)
|
|
126
|
+
if n == 0:
|
|
127
|
+
return []
|
|
128
|
+
cap = min(max_len, n)
|
|
129
|
+
for length in range(cap, 0, -1):
|
|
130
|
+
candidate = body[-length:]
|
|
131
|
+
if openai_message_list_valid(candidate):
|
|
132
|
+
return list(candidate)
|
|
133
|
+
for j in range(n - 1, -1, -1):
|
|
134
|
+
one = [body[j]]
|
|
135
|
+
if openai_message_list_valid(one):
|
|
136
|
+
return one
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def chat_row_to_openai_message(row: ChatMessageRow) -> dict[str, Any] | None:
|
|
141
|
+
if row.role == "tool":
|
|
142
|
+
return {
|
|
143
|
+
"role": "tool",
|
|
144
|
+
"tool_call_id": row.tool_call_id or "",
|
|
145
|
+
"content": row.content or "",
|
|
146
|
+
}
|
|
147
|
+
if row.role == "assistant":
|
|
148
|
+
msg: dict[str, Any] = {"role": "assistant", "content": row.content or ""}
|
|
149
|
+
if row.tool_calls_json:
|
|
150
|
+
try:
|
|
151
|
+
tc = json.loads(row.tool_calls_json)
|
|
152
|
+
if isinstance(tc, list):
|
|
153
|
+
msg["tool_calls"] = tc
|
|
154
|
+
except json.JSONDecodeError:
|
|
155
|
+
pass
|
|
156
|
+
return msg
|
|
157
|
+
if row.role in ("user", "system"):
|
|
158
|
+
return {"role": row.role, "content": row.content or ""}
|
|
159
|
+
return {"role": row.role, "content": row.content or ""}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def rows_to_messages(rows: list[ChatMessageRow]) -> list[dict[str, Any]]:
|
|
163
|
+
out: list[dict[str, Any]] = []
|
|
164
|
+
for r in rows:
|
|
165
|
+
m = chat_row_to_openai_message(r)
|
|
166
|
+
if m:
|
|
167
|
+
out.append(m)
|
|
168
|
+
return out
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def trim_messages_to_token_budget(
|
|
172
|
+
messages: list[dict[str, Any]], max_tokens: int
|
|
173
|
+
) -> list[dict[str, Any]]:
|
|
174
|
+
"""从头部丢弃消息直至估算 token 不超过 ``max_tokens``(至少保留最后一条)。"""
|
|
175
|
+
if not messages:
|
|
176
|
+
return messages
|
|
177
|
+
m = list(messages)
|
|
178
|
+
while len(m) > 1:
|
|
179
|
+
total = sum(estimate_message_tokens(x) for x in m)
|
|
180
|
+
if total <= max_tokens:
|
|
181
|
+
break
|
|
182
|
+
m.pop(0)
|
|
183
|
+
repair_openai_chat_messages(m)
|
|
184
|
+
return m
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def build_hydrated_messages(
|
|
188
|
+
*,
|
|
189
|
+
rows: list[ChatMessageRow],
|
|
190
|
+
truncated_by_count: bool,
|
|
191
|
+
latest_summary: str | None,
|
|
192
|
+
context_budget_tokens: int,
|
|
193
|
+
) -> list[dict[str, Any]]:
|
|
194
|
+
msgs = rows_to_messages(rows)
|
|
195
|
+
if truncated_by_count and latest_summary:
|
|
196
|
+
msgs.insert(
|
|
197
|
+
0,
|
|
198
|
+
{
|
|
199
|
+
"role": "user",
|
|
200
|
+
"content": "[历史摘记 — 更早轮次未载入上下文]\n" + latest_summary.strip(),
|
|
201
|
+
},
|
|
202
|
+
)
|
|
203
|
+
return trim_messages_to_token_budget(msgs, context_budget_tokens)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""OpenAI 消息与数据库存储之间的序列化。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
# 多模态计费/上下文粗估:业界常见按每张图约 2k–8k token(分辨率与模型有关)
|
|
9
|
+
VISION_IMAGE_TOKENS_MIN = 2000
|
|
10
|
+
VISION_IMAGE_TOKENS_MAX = 8000
|
|
11
|
+
VISION_IMAGE_TOKENS_ESTIMATE = (VISION_IMAGE_TOKENS_MIN + VISION_IMAGE_TOKENS_MAX) // 2
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def tool_calls_to_jsonable(tool_calls: Any) -> list[dict[str, Any]]:
|
|
15
|
+
if not tool_calls:
|
|
16
|
+
return []
|
|
17
|
+
out: list[dict[str, Any]] = []
|
|
18
|
+
for tc in tool_calls:
|
|
19
|
+
fn = getattr(tc, "function", None)
|
|
20
|
+
name = getattr(fn, "name", "") if fn else ""
|
|
21
|
+
args = getattr(fn, "arguments", None) if fn else None
|
|
22
|
+
out.append(
|
|
23
|
+
{
|
|
24
|
+
"id": getattr(tc, "id", "") or "",
|
|
25
|
+
"type": getattr(tc, "type", None) or "function",
|
|
26
|
+
"function": {
|
|
27
|
+
"name": name,
|
|
28
|
+
"arguments": args if isinstance(args, str) else (args or "{}"),
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
return out
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def estimate_message_tokens(msg: dict[str, Any]) -> int:
|
|
36
|
+
"""
|
|
37
|
+
粗估 token:纯文本约 4 字符/token;含 ``tool_calls`` JSON。
|
|
38
|
+
若 ``content`` 为 OpenAI 多模态列表(``text`` + ``image_url``),
|
|
39
|
+
每张图按 ``VISION_IMAGE_TOKENS_ESTIMATE``(2k–8k 的中值)加计,便于与 ``context_budget_tokens`` 对齐。
|
|
40
|
+
"""
|
|
41
|
+
raw_c = msg.get("content")
|
|
42
|
+
if isinstance(raw_c, list):
|
|
43
|
+
text_chars = 0
|
|
44
|
+
image_n = 0
|
|
45
|
+
for part in raw_c:
|
|
46
|
+
if not isinstance(part, dict):
|
|
47
|
+
continue
|
|
48
|
+
if part.get("type") == "text":
|
|
49
|
+
text_chars += len(str(part.get("text") or ""))
|
|
50
|
+
elif part.get("type") == "image_url":
|
|
51
|
+
image_n += 1
|
|
52
|
+
text_tok = (text_chars // 4 + 1) if text_chars else 0
|
|
53
|
+
vision_tok = image_n * VISION_IMAGE_TOKENS_ESTIMATE
|
|
54
|
+
tc = msg.get("tool_calls")
|
|
55
|
+
extra = len(json.dumps(tc, ensure_ascii=False)) // 4 + 1 if tc else 0
|
|
56
|
+
return max(1, text_tok + vision_tok + extra)
|
|
57
|
+
|
|
58
|
+
n = len(str(raw_c or ""))
|
|
59
|
+
tc = msg.get("tool_calls")
|
|
60
|
+
if tc:
|
|
61
|
+
n += len(json.dumps(tc, ensure_ascii=False))
|
|
62
|
+
return max(1, n // 4 + 1)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from aicd.config import save_config
|
|
6
|
+
from aicd.config_settings import apply_nested_settings_patch, format_settings_summary
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from aicd.agent.tool_router import ToolRouter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _agent_settings_update(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
|
|
13
|
+
patch = args.get("patch")
|
|
14
|
+
if patch is None:
|
|
15
|
+
return {
|
|
16
|
+
"ok": True,
|
|
17
|
+
"message": "no patch; current summary",
|
|
18
|
+
"summary": format_settings_summary(router._cfg),
|
|
19
|
+
}
|
|
20
|
+
if not isinstance(patch, dict):
|
|
21
|
+
return {"ok": False, "error": "patch must be an object with optional llm / agent keys"}
|
|
22
|
+
ok, msg = apply_nested_settings_patch(router._cfg, patch)
|
|
23
|
+
if not ok:
|
|
24
|
+
return {"ok": False, "error": msg}
|
|
25
|
+
if bool(args.get("persist", True)):
|
|
26
|
+
save_config(router._layout["config"], router._cfg)
|
|
27
|
+
return {
|
|
28
|
+
"ok": True,
|
|
29
|
+
"message": msg,
|
|
30
|
+
"summary": format_settings_summary(router._cfg),
|
|
31
|
+
"persisted": bool(args.get("persist", True)),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
HANDLERS: dict[str, Any] = {
|
|
36
|
+
"agent_settings_update": _agent_settings_update,
|
|
37
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
import aicd.tools.viz_assets as viz_assets
|
|
6
|
+
from aicd.kit import chart_spec
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from aicd.agent.tool_router import ToolRouter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _ai_output_info(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
|
|
13
|
+
return {
|
|
14
|
+
"ok": True,
|
|
15
|
+
"workspace": str(router._cwd),
|
|
16
|
+
"ai_output_dir": str(router._ai_output),
|
|
17
|
+
"ai_tmp_dir": str(router._ai_tmp),
|
|
18
|
+
"note": "Write scratch/intermediate analysis under ai_tmp_dir; clean up when done. "
|
|
19
|
+
"Durable artifacts go under ai_output_dir (not inside tmp unless explicitly temporary).",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _ai_ensure_layout(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
|
|
24
|
+
router._ai_output.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
router._ai_tmp.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
return {
|
|
27
|
+
"ok": True,
|
|
28
|
+
"ai_output_dir": str(router._ai_output),
|
|
29
|
+
"ai_tmp_dir": str(router._ai_tmp),
|
|
30
|
+
"created": True,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _ai_workspace_set(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
|
|
35
|
+
fn = router._ai_workspace_setter
|
|
36
|
+
if fn is None:
|
|
37
|
+
return {
|
|
38
|
+
"ok": False,
|
|
39
|
+
"error": "ai_workspace_set is only available in the main chat session; use TUI /work <path>.",
|
|
40
|
+
}
|
|
41
|
+
raw = args.get("path")
|
|
42
|
+
if raw is None or (isinstance(raw, str) and not str(raw).strip()):
|
|
43
|
+
ok, msg = fn(None)
|
|
44
|
+
else:
|
|
45
|
+
ok, msg = fn(str(raw).strip())
|
|
46
|
+
if not ok:
|
|
47
|
+
return {"ok": False, "error": msg}
|
|
48
|
+
return {
|
|
49
|
+
"ok": True,
|
|
50
|
+
"workspace": msg,
|
|
51
|
+
"note": "fs_* relative paths and default run_command cwd use this directory.",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _viz_list_bundled_assets(
|
|
56
|
+
router: ToolRouter, args: dict[str, Any]
|
|
57
|
+
) -> dict[str, Any]:
|
|
58
|
+
return {
|
|
59
|
+
"ok": True,
|
|
60
|
+
"bundled_dir": str(viz_assets.bundled_res_dir()),
|
|
61
|
+
"files": viz_assets.list_bundled_viz_files(),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _viz_copy_resources(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
|
|
66
|
+
dest = router._resolve_path(args["target_directory"])
|
|
67
|
+
if not dest.is_dir():
|
|
68
|
+
return {"ok": False, "error": "target_directory must be an existing directory"}
|
|
69
|
+
return viz_assets.copy_viz_resources(dest)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _viz_export_html(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
|
|
73
|
+
out = router._resolve_path(args["html_path"])
|
|
74
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
title = str(args.get("title") or "Analysis")
|
|
76
|
+
raw_body = args.get("body_html")
|
|
77
|
+
body = (raw_body if raw_body is not None else "").strip()
|
|
78
|
+
if not body:
|
|
79
|
+
body = viz_assets.demo_body_snippet()
|
|
80
|
+
boot = True
|
|
81
|
+
else:
|
|
82
|
+
body = str(raw_body)
|
|
83
|
+
boot = bool(args.get("include_demo_chart_vis_bootstrap", False))
|
|
84
|
+
html = viz_assets.build_viz_html_document(
|
|
85
|
+
title,
|
|
86
|
+
body,
|
|
87
|
+
extra_head=str(args.get("extra_head") or ""),
|
|
88
|
+
extra_scripts=str(args.get("extra_scripts") or ""),
|
|
89
|
+
include_default_chart_vis_init=boot,
|
|
90
|
+
)
|
|
91
|
+
html = viz_assets.reorder_body_scripts_before_vis_usage(html)
|
|
92
|
+
out.write_text(html, encoding="utf-8")
|
|
93
|
+
copied: dict[str, Any] = {"skipped": True}
|
|
94
|
+
if bool(args.get("copy_resources", True)):
|
|
95
|
+
copied = viz_assets.copy_viz_resources(out.parent)
|
|
96
|
+
return {
|
|
97
|
+
"ok": True,
|
|
98
|
+
"html_path": str(out),
|
|
99
|
+
"copy_resources": copied,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _viz_data_chart(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
|
|
104
|
+
out = router._resolve_path(args["html_path"])
|
|
105
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
title = str(args.get("title") or "Chart")
|
|
107
|
+
chart_type = str(args.get("chart_type") or "bar")
|
|
108
|
+
labels = list(args.get("labels") or [])
|
|
109
|
+
values = list(args.get("values") or [])
|
|
110
|
+
dataset_label = str(args.get("dataset_label") or "")
|
|
111
|
+
html = chart_spec.build_html_with_chart(
|
|
112
|
+
title,
|
|
113
|
+
chart_type,
|
|
114
|
+
labels,
|
|
115
|
+
values,
|
|
116
|
+
dataset_label=dataset_label or "Series",
|
|
117
|
+
)
|
|
118
|
+
out.write_text(html, encoding="utf-8")
|
|
119
|
+
copied: dict[str, Any] = {"skipped": True}
|
|
120
|
+
if bool(args.get("copy_resources", True)):
|
|
121
|
+
copied = viz_assets.copy_viz_resources(out.parent)
|
|
122
|
+
return {
|
|
123
|
+
"ok": True,
|
|
124
|
+
"html_path": str(out),
|
|
125
|
+
"copy_resources": copied,
|
|
126
|
+
"ascii_preview": chart_spec.mermaid_bar_from_counts(
|
|
127
|
+
[str(x) for x in labels],
|
|
128
|
+
[int(round(float(v))) for v in values],
|
|
129
|
+
)
|
|
130
|
+
if len(labels) == len(values)
|
|
131
|
+
else "",
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
HANDLERS: dict[str, Any] = {
|
|
136
|
+
"ai_output_info": _ai_output_info,
|
|
137
|
+
"ai_ensure_layout": _ai_ensure_layout,
|
|
138
|
+
"ai_workspace_set": _ai_workspace_set,
|
|
139
|
+
"viz_list_bundled_assets": _viz_list_bundled_assets,
|
|
140
|
+
"viz_copy_resources": _viz_copy_resources,
|
|
141
|
+
"viz_export_html": _viz_export_html,
|
|
142
|
+
"viz_data_chart": _viz_data_chart,
|
|
143
|
+
}
|