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..8ea9f20c 100644 --- a/_local_backend.py +++ b/_local_backend.py @@ -61,6 +61,40 @@ 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 _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", @@ -75,7 +109,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: @@ -129,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/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..f9dbec3d 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)) @@ -44,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: