From 3da1c7b58b69f55967ed3abfa7367e0415366267 Mon Sep 17 00:00:00 2001 From: mindfn Date: Wed, 11 Mar 2026 15:21:36 +0800 Subject: [PATCH 1/2] docs: add local packaging and install guidance Add a repository-level README that explains local editable installs, wheel builds, and downstream imports via dare_framework. Normalize client-facing command examples to use python -m pip/python -m client instead of hard-coded .venv paths so the docs work across environments. Wire pyproject readme metadata into the custom PEP 517 backend and add a regression test so built wheel metadata now includes the markdown README content. --- README.md | 72 ++++++++++++++++++++++++++++++++ _local_backend.py | 32 +++++++++++++- client/README.md | 66 +++++++++++++++-------------- docs/README.md | 20 +++++---- pyproject.toml | 1 + tests/unit/test_local_backend.py | 15 +++++++ 6 files changed, 165 insertions(+), 41 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..d9665881 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# DARE Framework + +`deterministic-agent-runtime-engine` 是当前仓库的发行包名,安装后在 Python 代码里使用的导入名是 `dare_framework`。 + +除非特别说明,下面的命令都默认在仓库根目录、且已激活目标 Python 虚拟环境后执行。 + +## 本地安装 + +### 方式 1:可编辑安装(联调推荐) + +```bash +python -m pip install -e . +``` + +如果你只想安装入口而暂时跳过依赖解析: + +```bash +python -m pip install -e . --no-deps +``` + +### 方式 2:先打本地 wheel,再安装(冻结版本推荐) + +```bash +python -m pip wheel . -w dist --no-deps +python -m pip install dist/deterministic_agent_runtime_engine-0.1.0-py3-none-any.whl +``` + +## 在另一个项目里引用当前仓库 + +如果下游项目和当前仓库在同一台机器上,不需要发布到公共镜像仓库。 + +### 直接从源码路径安装 + +```bash +python -m pip install -e /abs/path/to/Deterministic-Agent-Runtime-Engine +``` + +如果两个项目是同级目录,也可以用相对路径: + +```bash +python -m pip install -e ../Deterministic-Agent-Runtime-Engine +``` + +### 从本地 wheel 安装 + +先在当前仓库构建 wheel: + +```bash +python -m pip wheel . -w dist --no-deps +``` + +再在下游项目环境里安装: + +```bash +python -m pip install /abs/path/to/Deterministic-Agent-Runtime-Engine/dist/deterministic_agent_runtime_engine-0.1.0-py3-none-any.whl +``` + +## 导入方式 + +安装成功后,下游项目直接导入 `dare_framework`: + +```python +from dare_framework.agent import BaseAgent +from dare_framework.model import OpenAIModelAdapter +``` + +如果下游项目可以接受修改 import,推荐直接迁到 `dare_framework.*`,不要额外做 `agentscope` 兼容包。 + +## 相关文档 + +- `client/README.md`:CLI 使用方式与运行参数 +- `docs/README.md`:设计、治理和模块文档导航 diff --git a/_local_backend.py b/_local_backend.py index bf7fd239..7bc5198a 100644 --- a/_local_backend.py +++ b/_local_backend.py @@ -61,6 +61,31 @@ def _sdist_filename(project: dict) -> str: return f"{_sdist_root_dir(project)}.tar.gz" +def _infer_readme_content_type(path: Path) -> str: + if path.suffix.lower() == ".md": + return "text/markdown" + if path.suffix.lower() == ".rst": + return "text/x-rst" + return "text/plain" + + +def _readme_payload(project: dict) -> tuple[str, str] | None: + readme = project.get("readme") + if isinstance(readme, str): + path = _project_root() / readme + return path.read_text(encoding="utf-8"), _infer_readme_content_type(path) + if isinstance(readme, dict): + content_type = str(readme.get("content-type") or "text/plain") + if "file" in readme: + path = _project_root() / str(readme["file"]) + if "content-type" not in readme: + content_type = _infer_readme_content_type(path) + return path.read_text(encoding="utf-8"), content_type + if "text" in readme: + return str(readme["text"]), content_type + return None + + def _metadata_text(project: dict) -> str: lines = [ "Metadata-Version: 2.1", @@ -75,7 +100,12 @@ def _metadata_text(project: dict) -> str: lines.append(f"Requires-Python: {requires_python}") for dep in project.get("dependencies", []): lines.append(f"Requires-Dist: {dep}") - return "\n".join(lines) + "\n" + readme_payload = _readme_payload(project) + if readme_payload is None: + return "\n".join(lines) + "\n" + readme_text, content_type = readme_payload + lines.append(f"Description-Content-Type: {content_type}") + return "\n".join(lines) + "\n\n" + readme_text.rstrip() + "\n" def _wheel_text() -> str: diff --git a/client/README.md b/client/README.md index 22d19909..5d29eda6 100644 --- a/client/README.md +++ b/client/README.md @@ -2,69 +2,71 @@ 统一对外 CLI 入口,面向 `dare_framework` 的任务执行与运行时控制。 +除非特别说明,以下命令默认在仓库根目录、且已激活目标 Python 虚拟环境后执行。若你使用显式虚拟环境路径,可将 `python` 替换为 `/bin/python`。 + ## 运行方式 ```bash # 仓库根目录 -.venv/bin/python -m client --help +python -m client --help # 可编辑安装后使用 console script -.venv/bin/pip install -e . -.venv/bin/dare --help +python -m pip install -e . +dare --help ``` 如果是在离线或受限网络环境,可跳过依赖安装,仅安装 CLI 入口: ```bash -.venv/bin/pip install -e . --no-deps +python -m pip install -e . --no-deps ``` ## 常用命令 ```bash # 交互模式 -.venv/bin/python -m client chat +python -m client chat # 恢复最近一次会话 -.venv/bin/python -m client chat --resume +python -m client chat --resume # 恢复指定会话 -.venv/bin/python -m client chat --resume +python -m client chat --resume # 兼容入口:恢复指定会话 -.venv/bin/python -m client chat --session-id +python -m client chat --session-id # 列出当前 workspace 可恢复会话 -.venv/bin/python -m client sessions list +python -m client sessions list # 一次性执行 -.venv/bin/python -m client run --task "读取 README 并总结" +python -m client run --task "读取 README 并总结" # 在已有会话历史上继续执行一次任务 -.venv/bin/python -m client run --resume latest --task "继续上一轮,补充测试计划" +python -m client run --resume latest --task "继续上一轮,补充测试计划" # 兼容入口:基于指定 session 继续执行 -.venv/bin/python -m client run --session-id --task "继续上一轮" +python -m client run --session-id --task "继续上一轮" # 一次性执行(审批等待超时,默认 120s) -.venv/bin/python -m client run --task "读取 README 并总结" --approval-timeout-seconds 120 +python -m client run --task "读取 README 并总结" --approval-timeout-seconds 120 # 一次性执行(自动审批指定工具,例如 run_command) -.venv/bin/python -m client run --task "读取 README 并总结" --auto-approve-tool run_command +python -m client run --task "读取 README 并总结" --auto-approve-tool run_command # 脚本模式 -.venv/bin/python -m client script --file /abs/path/to/demo.txt +python -m client script --file /abs/path/to/demo.txt # 仓库内示例脚本 -.venv/bin/python -m client chat --script client/examples/basic.script.txt +python -m client chat --script client/examples/basic.script.txt # 在已有会话上继续跑脚本 -.venv/bin/python -m client script --resume latest --file /abs/path/to/demo.txt +python -m client script --resume latest --file /abs/path/to/demo.txt # 兼容入口:在指定会话上继续跑脚本 -.venv/bin/python -m client script --session-id --file /abs/path/to/demo.txt +python -m client script --session-id --file /abs/path/to/demo.txt # 审批控制 -.venv/bin/python -m client approvals list -.venv/bin/python -m client approvals poll --timeout-ms 30000 -.venv/bin/python -m client approvals grant --scope workspace --matcher exact_params [--session-id session-id] +python -m client approvals list +python -m client approvals poll --timeout-ms 30000 +python -m client approvals grant --scope workspace --matcher exact_params [--session-id session-id] # MCP 控制 -.venv/bin/python -m client mcp list -.venv/bin/python -m client mcp inspect -.venv/bin/python -m client mcp reload +python -m client mcp list +python -m client mcp inspect +python -m client mcp reload # 诊断(不要求模型可执行) -.venv/bin/python -m client doctor +python -m client doctor ``` ## 会话持久化与 Resume @@ -346,7 +348,7 @@ OpenAI-compatible / 自建模型网关: 临时切模型或切换 provider 时,可以直接用 CLI flags 覆盖文件配置: ```bash -.venv/bin/python -m client \ +python -m client \ --adapter openrouter \ --model qwen/qwen3-coder:free \ --api-key "$OPENROUTER_API_KEY" \ @@ -356,7 +358,7 @@ OpenAI-compatible / 自建模型网关: 或者只临时改 endpoint: ```bash -.venv/bin/python -m client \ +python -m client \ --endpoint http://127.0.0.1:8000/v1 \ run --task "读取 README 并总结" ``` @@ -364,7 +366,7 @@ OpenAI-compatible / 自建模型网关: 临时覆盖 system prompt(完整替换): ```bash -.venv/bin/python -m client \ +python -m client \ --system-prompt-mode replace \ --system-prompt-file .dare/prompts/strict_system.txt \ run --task "读取 README 并总结" @@ -373,7 +375,7 @@ OpenAI-compatible / 自建模型网关: 临时覆盖 system prompt(在默认提示词后追加): ```bash -.venv/bin/python -m client \ +python -m client \ --system-prompt-mode append \ --system-prompt-text "Always answer in Chinese unless user explicitly asks otherwise." \ chat @@ -390,13 +392,13 @@ OpenAI-compatible / 自建模型网关: ```bash # 查看最终生效的合并配置 -.venv/bin/python -m client config show +python -m client config show # 查看当前 runtime 选中的模型信息 -.venv/bin/python -m client model show +python -m client model show # 做环境与依赖诊断 -.venv/bin/python -m client doctor +python -m client doctor ``` 这三个命令分别用于: diff --git a/docs/README.md b/docs/README.md index 6695ac54..29c679f2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,35 +9,38 @@ ### 快速上手(最终架构设计以权威文档为准) ```text -1. 项目概览 +1. 项目入口与本地安装 + └── /README.md (发行包名、导入名、本地 editable/wheel 安装、下游项目引用方式) + +2. 项目概览 └── /openspec/project.md (项目上下文、技术栈、架构概览) -2. 核心架构(权威设计) +3. 核心架构(权威设计) └── design/Architecture.md -3. 接口设计(权威设计) +4. 接口设计(权威设计) └── design/Interfaces.md -4. 设计对齐与证据(可选但推荐) +5. 设计对齐与证据(可选但推荐) ├── design/DARE_alignment.md └── design/DARE_evidence.yaml -5. 代码实现(以权威设计文档为准) +6. 代码实现(以权威设计文档为准) └── dare_framework/(当前实现;若与权威设计不一致,需先修订设计并执行 gap 分析) -6. 示例实现 +7. 示例实现 ├── /examples/04-dare-coding-agent/ (五层循环示例 Agent) ├── /examples/06-dare-coding-agent-mcp/ (Config 驱动 MCP + 动态重载示例) ├── /examples/07-tool-approval-memory/ (工具审批记忆与自动放行示例) └── /examples/08-hook-governance/ (Hook 治理:patch + block 示例) -7. CLI 使用与配置 +8. CLI 使用与配置 ├── /client/README.md (命令入口、`.dare/config.json`、LLM 配置说明) ├── /.dare/config.json.example (OpenAI 最小配置) ├── /.dare/config.openrouter.example.json (OpenRouter 最小配置) └── /.dare/config.advanced.example.json (进阶配置示例) -8. 开发规范 +9. 开发规范 ├── /CONTRIBUTING_AI.md (AI Agent 协作规范) ├── guides/Development_Constraints.md (开发约束清单) ├── guides/Documentation_First_Development_SOP.md (文档先行 SOP,Bug/Feature/Refactor 必走) @@ -78,6 +81,7 @@ | 文档/目录 | 作用 | 状态 | |---|---|---| +| `/README.md` | 项目入口与本地安装说明,面向“如何在本机或下游项目中引用 `dare_framework`” | ✅ 仓库入口 | | `dare_framework/` | 框架实现主目录(目标收敛到单一架构;详见权威设计) | ✅ 实现入口 | | `client/README.md` | DARE Client CLI 用法与配置入口,含 `.dare/config.json` 和 LLM 配置说明 | ✅ CLI 入口 | | `.dare/config.json.example` | OpenAI 最小配置示例,可作为 workspace `.dare/config.json` 起点 | ✅ 配置示例 | diff --git a/pyproject.toml b/pyproject.toml index e2f361ae..73d07e6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ backend-path = ["."] name = "deterministic-agent-runtime-engine" version = "0.1.0" description = "DARE Framework and external CLI runtime" +readme = "README.md" requires-python = ">=3.12" dependencies = [ "anthropic", diff --git a/tests/unit/test_local_backend.py b/tests/unit/test_local_backend.py index 54977523..19518136 100644 --- a/tests/unit/test_local_backend.py +++ b/tests/unit/test_local_backend.py @@ -35,6 +35,21 @@ def test_local_backend_build_wheel_includes_cli_sources(tmp_path: Path) -> None: assert any(name.endswith(".dist-info/RECORD") for name in names) +def test_local_backend_wheel_metadata_includes_markdown_readme(tmp_path: Path) -> None: + backend = importlib.import_module("_local_backend") + wheel_name = backend.build_wheel(str(tmp_path)) + wheel_path = tmp_path / wheel_name + + with zipfile.ZipFile(wheel_path, "r") as archive: + metadata_name = next( + name for name in archive.namelist() if name.endswith(".dist-info/METADATA") + ) + metadata = archive.read(metadata_name).decode("utf-8") + + assert "Description-Content-Type: text/markdown" in metadata + assert "## 本地安装" in metadata + + def test_local_backend_build_sdist_includes_core_sources(tmp_path: Path) -> None: backend = importlib.import_module("_local_backend") sdist_name = backend.build_sdist(str(tmp_path)) From 4b5c859e1e5464592b5c2e82697929cf0a8b666d Mon Sep 17 00:00:00 2001 From: mindfn Date: Wed, 11 Mar 2026 16:45:25 +0800 Subject: [PATCH 2/2] build: include project readme in sdist metadata path Include the configured project readme file in sdists so metadata hooks and wheel builds from generated source archives can still load README.md. Tighten the local backend regression test to assert the root README is present in the sdist archive and keep the markdown metadata coverage intact. --- _local_backend.py | 13 +++++++++++++ tests/unit/test_local_backend.py | 2 ++ 2 files changed, 15 insertions(+) diff --git a/_local_backend.py b/_local_backend.py index 7bc5198a..8ea9f20c 100644 --- a/_local_backend.py +++ b/_local_backend.py @@ -86,6 +86,15 @@ def _readme_payload(project: dict) -> tuple[str, str] | None: return None +def _readme_file(project: dict) -> Path | None: + readme = project.get("readme") + if isinstance(readme, str): + return _project_root() / readme + if isinstance(readme, dict) and "file" in readme: + return _project_root() / str(readme["file"]) + return None + + def _metadata_text(project: dict) -> str: lines = [ "Metadata-Version: 2.1", @@ -159,6 +168,10 @@ def _yield(path: Path) -> Iterable[Path]: ): yield from _yield(candidate) + readme_file = _readme_file(_load_project_metadata()) + if readme_file is not None: + yield from _yield(readme_file) + for file_path in _package_files(): if file_path not in seen: seen.add(file_path) diff --git a/tests/unit/test_local_backend.py b/tests/unit/test_local_backend.py index 19518136..f9dbec3d 100644 --- a/tests/unit/test_local_backend.py +++ b/tests/unit/test_local_backend.py @@ -59,11 +59,13 @@ def test_local_backend_build_sdist_includes_core_sources(tmp_path: Path) -> None with tarfile.open(sdist_path, "r:gz") as archive: names = set(archive.getnames()) + root_dir = next(name.split("/", 1)[0] for name in names if name.endswith("/PKG-INFO")) assert any(name.endswith("/pyproject.toml") for name in names) assert any(name.endswith("/_local_backend.py") for name in names) assert any(name.endswith("/client/main.py") for name in names) assert any(name.endswith("/dare_framework/__init__.py") for name in names) assert any(name.endswith("/PKG-INFO") for name in names) + assert f"{root_dir}/README.md" in names def test_local_backend_reports_no_extra_requirements_for_build_sdist() -> None: