mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-08-28 05:25:04 +00:00
fix(proactive): expose topics in primary answer (#380)
This commit is contained in:
parent
cf22ef3b1d
commit
55ef4bd6ad
9 changed files with 152 additions and 128 deletions
|
|
@ -1,23 +0,0 @@
|
|||
# ReMe 仓库文档
|
||||
|
||||
本目录保存 ReMe 仓库 README 直接引用的中英文补充说明和图片资源,不作为文档站点的构建或部署来源。
|
||||
|
||||
面向用户发布的中英文文档位于 [agentscope-ai/docs](https://github.com/agentscope-ai/docs) 仓库,并由该仓库统一完成版本管理和 Mintlify 部署。
|
||||
|
||||
## 目录用途
|
||||
|
||||
```text
|
||||
docs/
|
||||
├── README.md 本目录的维护说明
|
||||
├── doc.md 当前文档设计与维护边界
|
||||
├── en/ README 引用的英文补充说明
|
||||
├── zh/ README 引用的中文补充说明
|
||||
└── figure/ ReMe README 使用的图片资源
|
||||
```
|
||||
|
||||
## 维护原则
|
||||
|
||||
- `en/` 和 `zh/` 保持精简,服务 README 中需要进一步解释的功能与场景;修改路径时同步更新 README 链接。
|
||||
- 具体实现以源码、schema、测试和运行时帮助为准,避免维护重复且容易过期的开发手册。
|
||||
- README 引用的图片保留在 `figure/`;发布文档需要图片时,在统一文档仓库的 `images/reme/` 中维护对应副本。
|
||||
- 网页文档、导航、版本和部署在统一文档仓库中维护。
|
||||
85
docs/doc.md
85
docs/doc.md
|
|
@ -1,85 +0,0 @@
|
|||
# ReMe 文档设计
|
||||
|
||||
本文定义 ReMe 文档的内容边界和维护方式。目标是让文档保持精简、稳定,并适合用户与 AI coding agent 快速理解。
|
||||
|
||||
## 两类文档,两种职责
|
||||
|
||||
| 位置 | 用途 | 是否部署 |
|
||||
|---|---|---|
|
||||
| `ReMe/docs/` | README 引用的中英文补充说明和图片 | 否 |
|
||||
| `agentscope-ai/docs/reme/<version>/` | 面向用户的中英文产品文档 | 是 |
|
||||
|
||||
ReMe 仓库维护 `docs/en/`、`docs/zh/` 中供 README 直接引用的页面,但不把它们作为网页部署来源。网站内容、发布、版本选择、
|
||||
导航和重定向都由统一文档仓库负责。
|
||||
|
||||
## 内容原则
|
||||
|
||||
### Concepts 只讲理念
|
||||
|
||||
Concepts 应解释 ReMe 为什么这样设计,而不是逐项描述组件和流水线实现。核心判断包括:
|
||||
|
||||
- **Memory as File**:记忆首先是用户拥有、可读写和可迁移的文件。
|
||||
- **Memory from Experience**:长期记忆来自经验的提炼、修正和合并,而不是无限累积上下文。
|
||||
- **Human-Agent Shared Memory**:用户和 Agent 共同读写同一份可见记忆。
|
||||
- **Connected and Traceable**:长期结论可以通过链接回到来源和上下文。
|
||||
|
||||
算法、索引、Job、Step 和存储实现只有在帮助解释理念取舍时才进入 Concepts。
|
||||
|
||||
### Development 保持轻量
|
||||
|
||||
现代开发主要由 AI 直接阅读源码、schema 和测试完成。Development 只需要提供:
|
||||
|
||||
- 开发环境和最小验证命令;
|
||||
- 代码目录入口;
|
||||
- 兼容性与贡献要求;
|
||||
- 哪些源码或 schema 是权威依据。
|
||||
|
||||
不为每个类、组件或扩展点编写重复的开发手册,也不维护 `generic_agent` 一类泛化教程。
|
||||
|
||||
### Reference 只记录稳定契约
|
||||
|
||||
Reference 记录 workspace、配置入口、CLI、HTTP、MCP 和文件格式的稳定语义。精确参数交给运行时帮助、Pydantic schema 和源码,避免文档复制一份容易失真的接口定义。
|
||||
|
||||
### Guides 只保留已验证路径
|
||||
|
||||
接入文档应对应真实、可验证的工作流。目前优先维护 Claude Code、QwenPaw,以及 Skill、CLI、MCP、HTTP、Python 的选择说明。没有可验证实现的框架不提前创建占位页。
|
||||
|
||||
## 发布文档结构
|
||||
|
||||
ReMe 参考 AgentScope 的版本目录和导航方式:
|
||||
|
||||
```text
|
||||
agentscope-ai/docs/
|
||||
├── reme/
|
||||
│ └── 0.4.0.6/
|
||||
│ ├── en/
|
||||
│ └── zh/
|
||||
└── images/
|
||||
└── reme/
|
||||
```
|
||||
|
||||
每个语言版本保持三组导航:
|
||||
|
||||
1. **Get Started / 快速开始**:Index、Overview、Quick Start、Concepts。
|
||||
2. **Integrate / 接入**:接入选择、Claude Code、QwenPaw。
|
||||
3. **Reference / 查阅与参与**:Reference、Support、Contributing。
|
||||
|
||||
ReMe 使用项目级别的 `/reme/latest/` 和 `/reme/stable/` 别名,不影响 AgentScope 自己的 `/latest/` 与 `/stable/`。
|
||||
|
||||
## 变更应该写在哪里
|
||||
|
||||
| 变更类型 | ReMe 仓库 | 统一文档仓库 |
|
||||
|---|---|---|
|
||||
| 产品理念或长期设计判断 | 更新 `docs/doc.md` 或相关设计记录 | 必要时同步 Concepts |
|
||||
| 用户可见的安装、配置或行为 | 源码、schema、测试;影响 README 时同步 `docs/en/`、`docs/zh/` | 更新对应版本的用户文档 |
|
||||
| 内部重构或组件调整 | 以代码和测试表达 | 稳定契约未变时无需更新 |
|
||||
| README 图片 | 更新 `docs/figure/` | 发布页使用时同步到 `images/reme/` |
|
||||
| 新版本发布 | 更新版本号和代码 | 新建版本目录、双语导航与 ReMe 别名 |
|
||||
|
||||
## 质量要求
|
||||
|
||||
- 每个用户流程必须能够在当前版本运行和验证。
|
||||
- 文档不复制能够从代码可靠获得的细节。
|
||||
- 删除过期内容优先于继续叠加补丁说明。
|
||||
- 中英文页面保持信息等价,不要求逐句直译。
|
||||
- 发布前在统一文档仓库运行 Mintlify 严格校验。
|
||||
|
|
@ -37,7 +37,7 @@ Parameters:
|
|||
| Parameter | Purpose |
|
||||
|---|---|
|
||||
| `date` | Date to read in `YYYY-MM-DD` format. When empty, use today in the application's timezone. |
|
||||
| `include_content` | Whether to return the raw YAML in metadata. Defaults to `true`. |
|
||||
| `include_content` | Whether to return the raw YAML in the answer and metadata. Defaults to `true`. |
|
||||
|
||||
## Input Contract
|
||||
|
||||
|
|
@ -63,7 +63,9 @@ Only the `topics` list is parsed into structured results. Every topic requires a
|
|||
|
||||
## Return Value
|
||||
|
||||
When the file is read successfully, `proactive_step` writes these values to standard response metadata:
|
||||
When the file is read successfully, `proactive_step` returns `summary` and `topics` in the primary answer. When
|
||||
`include_content=true`, the answer also contains `content`. The same result fields remain available in standard response
|
||||
metadata:
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
|
|
@ -75,12 +77,27 @@ When the file is read successfully, `proactive_step` writes these values to stan
|
|||
| `error` | Read or parse error. |
|
||||
| `summary` | Short summary. |
|
||||
|
||||
When the file exists and parses successfully, the answer looks like:
|
||||
When the file exists and parses successfully, the answer is structured data. For example:
|
||||
|
||||
```text
|
||||
Read 3 proactive topic(s) from daily/2026-06-20/interests.yaml
|
||||
```json
|
||||
{
|
||||
"summary": "Read 1 proactive topic(s) from daily/2026-06-20/interests.yaml",
|
||||
"topics": [
|
||||
{
|
||||
"title": "Quality regression in the memory retrieval pipeline",
|
||||
"reason": "The user has recently made repeated changes to search, node_search, and dream integration.",
|
||||
"evidence": "daily/2026-06-20/session.md",
|
||||
"keywords": ["memory search", "auto dream"],
|
||||
"paths": ["daily/2026-06-20/session.md"]
|
||||
}
|
||||
],
|
||||
"content": "date: 2026-06-20\n..."
|
||||
}
|
||||
```
|
||||
|
||||
With `include_content=false`, the `content` field is omitted from the answer. Missing-file and read-error answers remain
|
||||
explicit `Skipped: ...` and `Error: ...` messages, respectively.
|
||||
|
||||
A missing file is not an error. The call succeeds with a skipped result:
|
||||
|
||||
```text
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ proactive:
|
|||
| 参数 | 作用 |
|
||||
|-------------------|----------------------------------------|
|
||||
| `date` | 要读取的日期,格式为 `YYYY-MM-DD`。为空时使用应用时区中的今天。 |
|
||||
| `include_content` | 是否在 metadata 中返回 YAML 原文,默认 `true`。 |
|
||||
| `include_content` | 是否在 answer 和 metadata 中返回 YAML 原文,默认 `true`。 |
|
||||
|
||||
## 输入契约
|
||||
|
||||
|
|
@ -59,7 +59,8 @@ topics:
|
|||
|
||||
## 返回结果
|
||||
|
||||
成功读取时,`proactive_step` 会把结果写入标准 response metadata:
|
||||
成功读取时,`proactive_step` 会在主要 answer 中返回 `summary` 和 `topics`;当 `include_content=true` 时还会返回
|
||||
`content`。相同的结果字段也会保留在标准 response metadata 中:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|-----------|----------------------------------------|
|
||||
|
|
@ -71,12 +72,27 @@ topics:
|
|||
| `error` | 读取或解析异常。 |
|
||||
| `summary` | 简短摘要。 |
|
||||
|
||||
文件存在且解析成功时,answer 类似:
|
||||
文件存在且解析成功时,answer 是结构化数据,例如:
|
||||
|
||||
```text
|
||||
Read 3 proactive topic(s) from daily/2026-06-20/interests.yaml
|
||||
```json
|
||||
{
|
||||
"summary": "Read 1 proactive topic(s) from daily/2026-06-20/interests.yaml",
|
||||
"topics": [
|
||||
{
|
||||
"title": "记忆检索链路的质量回归",
|
||||
"reason": "用户最近反复修改了 search、node_search 和 dream integration。",
|
||||
"evidence": "daily/2026-06-20/session.md",
|
||||
"keywords": ["memory search", "auto dream"],
|
||||
"paths": ["daily/2026-06-20/session.md"]
|
||||
}
|
||||
],
|
||||
"content": "date: 2026-06-20\n..."
|
||||
}
|
||||
```
|
||||
|
||||
当 `include_content=false` 时,answer 不包含 `content` 字段。文件缺失和读取失败仍分别返回明确的
|
||||
`Skipped: ...` 和 `Error: ...` 消息。
|
||||
|
||||
文件不存在时不会报错,而是成功返回 skipped:
|
||||
|
||||
```text
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ jobs:
|
|||
default: ""
|
||||
include_content:
|
||||
type: boolean
|
||||
description: "whether to include the raw YAML content in response metadata"
|
||||
description: "whether to include the raw YAML content in the response answer and metadata"
|
||||
default: true
|
||||
steps:
|
||||
- backend: proactive_step
|
||||
|
|
|
|||
|
|
@ -6,10 +6,14 @@ from pydantic import BaseModel, ConfigDict, Field
|
|||
|
||||
|
||||
class Response(BaseModel):
|
||||
"""Standard response envelope; extra fields allowed for endpoint-specific output."""
|
||||
"""Standard response envelope; extra fields allowed for endpoint-specific output.
|
||||
|
||||
``answer`` is the primary tool result and must be sufficient for the next LLM action.
|
||||
``metadata`` is auxiliary request context for programmatic clients and diagnostics.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
answer: str | Any = Field(default="", description="Response content or result data")
|
||||
answer: str | Any = Field(default="", description="Primary response content or result data exposed to tool callers")
|
||||
success: bool = Field(default=True, description="Whether the operation succeeded")
|
||||
metadata: dict = Field(default_factory=dict, description="Additional context and diagnostics")
|
||||
metadata: dict = Field(default_factory=dict, description="Auxiliary request context and diagnostics")
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class ProactiveStep(BaseStep):
|
|||
if not abs_path.is_file():
|
||||
result.skipped, result.summary = True, f"Skipped: interests file not found at {rel_path}"
|
||||
self.logger.info(f"[{self.name}] skip missing path={rel_path}")
|
||||
return self._finish(True, result)
|
||||
return self._finish(True, result, include_content=include_content)
|
||||
try:
|
||||
self.logger.info(f"[{self.name}] read start path={rel_path}")
|
||||
result.content = abs_path.read_text(encoding="utf-8") if include_content else ""
|
||||
|
|
@ -37,15 +37,24 @@ class ProactiveStep(BaseStep):
|
|||
except Exception as e: # noqa: BLE001
|
||||
result.error, result.summary = f"{type(e).__name__}: {e}", ""
|
||||
self.logger.error(f"[{self.name}] read failed path={rel_path}: {result.error}")
|
||||
return self._finish(False, result)
|
||||
return self._finish(False, result, include_content=include_content)
|
||||
|
||||
result.summary = f"Read {len(result.topics)} proactive topic(s) from {rel_path}"
|
||||
return self._finish(True, result)
|
||||
return self._finish(True, result, include_content=include_content)
|
||||
|
||||
def _finish(self, success: bool, result: ProactiveResult):
|
||||
def _finish(self, success: bool, result: ProactiveResult, *, include_content: bool):
|
||||
assert self.context is not None
|
||||
self.context.response.success = success
|
||||
self.context.response.answer = result.summary if success else f"Error: {result.error}"
|
||||
if not success:
|
||||
self.context.response.answer = f"Error: {result.error}"
|
||||
elif result.skipped:
|
||||
self.context.response.answer = result.summary
|
||||
else:
|
||||
self.context.response.answer = {
|
||||
"summary": result.summary,
|
||||
"topics": result.topics,
|
||||
**({"content": result.content} if include_content else {}),
|
||||
}
|
||||
self.context.response.metadata.update(result.model_dump())
|
||||
self.logger.info(f"[{self.name}] finish success={success} answer={self.context.response.answer!r}")
|
||||
return self.context.response
|
||||
|
|
|
|||
|
|
@ -88,7 +88,8 @@ Use `proactive` to read interest topics generated by `auto_dream`:
|
|||
reme proactive date="<YYYY-MM-DD>"
|
||||
```
|
||||
|
||||
`proactive` only returns topics; the Agent decides whether and how to mention them to the user.
|
||||
`proactive` returns structured topics and, by default, the source YAML content. Pass `include_content=false` when the raw
|
||||
content is not needed. The Agent decides whether and how to mention the topics to the user.
|
||||
|
||||
## Integration Rules
|
||||
|
||||
|
|
|
|||
|
|
@ -3,15 +3,18 @@
|
|||
import asyncio
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import yaml
|
||||
|
||||
from reme.components.application_context import ApplicationContext
|
||||
from reme.components.file_catalog import BaseFileCatalog
|
||||
from reme.components.file_store import BaseFileStore
|
||||
from reme.components.runtime_context import RuntimeContext
|
||||
from reme.schema import DreamState
|
||||
from reme.steps.evolve.dream.extract import DreamExtractStep
|
||||
from reme.steps.evolve.dream.finish import DreamFinishStep
|
||||
from reme.steps.evolve.dream.proactive import ProactiveStep
|
||||
from reme.steps.evolve.dream.topics import DreamTopicsStep
|
||||
from reme.steps.evolve.dream.utils import parse_structured_reply, recent_dates, scan_day_files
|
||||
|
||||
|
|
@ -176,6 +179,88 @@ def test_topics_step_writes_only_target_date_interests():
|
|||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_proactive_answer_includes_topics_and_requested_content(tmp_path):
|
||||
"""Successful proactive reads expose useful data through the primary answer."""
|
||||
|
||||
async def run():
|
||||
content = (
|
||||
"date: 2026-05-28\n"
|
||||
"topics:\n"
|
||||
" - title: Retrieval quality\n"
|
||||
" reason: Search behavior changed repeatedly.\n"
|
||||
" evidence: daily/2026-05-28/session.md\n"
|
||||
)
|
||||
_touch(tmp_path / "daily" / "2026-05-28" / "interests.yaml", content)
|
||||
step = ProactiveStep(app_context=ApplicationContext(workspace_dir=str(tmp_path)))
|
||||
|
||||
response = await step(
|
||||
RuntimeContext(date="2026-05-28", include_content=True, file_store=_FileStore(tmp_path)),
|
||||
)
|
||||
|
||||
assert response.success is True
|
||||
assert response.answer == {
|
||||
"summary": "Read 1 proactive topic(s) from daily/2026-05-28/interests.yaml",
|
||||
"topics": [
|
||||
{
|
||||
"title": "Retrieval quality",
|
||||
"reason": "Search behavior changed repeatedly.",
|
||||
"evidence": "daily/2026-05-28/session.md",
|
||||
"keywords": [],
|
||||
"paths": [],
|
||||
},
|
||||
],
|
||||
"content": content,
|
||||
}
|
||||
assert response.metadata["topics"] == response.answer["topics"]
|
||||
assert response.metadata["content"] == content
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_proactive_answer_omits_unrequested_content(tmp_path):
|
||||
"""Raw YAML is absent from the primary answer when include_content is false."""
|
||||
|
||||
async def run():
|
||||
_touch(
|
||||
tmp_path / "daily" / "2026-05-28" / "interests.yaml",
|
||||
"topics:\n - title: Topic\n reason: Reason\n",
|
||||
)
|
||||
step = ProactiveStep(app_context=ApplicationContext(workspace_dir=str(tmp_path)))
|
||||
|
||||
response = await step(
|
||||
RuntimeContext(date="2026-05-28", include_content=False, file_store=_FileStore(tmp_path)),
|
||||
)
|
||||
|
||||
assert response.success is True
|
||||
assert "content" not in response.answer
|
||||
assert response.answer["topics"][0]["title"] == "Topic"
|
||||
assert response.metadata["content"] == ""
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_proactive_keeps_skipped_and_error_answers_explicit(tmp_path):
|
||||
"""Empty and failure outcomes remain distinguishable without reading metadata."""
|
||||
|
||||
async def run():
|
||||
step = ProactiveStep(app_context=ApplicationContext(workspace_dir=str(tmp_path)))
|
||||
skipped = await step(RuntimeContext(date="2026-05-28", file_store=_FileStore(tmp_path)))
|
||||
|
||||
assert skipped.success is True
|
||||
assert skipped.answer == "Skipped: interests file not found at daily/2026-05-28/interests.yaml"
|
||||
assert skipped.metadata["skipped"] is True
|
||||
|
||||
_touch(tmp_path / "daily" / "2026-05-28" / "interests.yaml", "topics: []\n")
|
||||
with patch("reme.steps.evolve.dream.proactive.load_yaml_topics", side_effect=ValueError("bad topics")):
|
||||
failed = await step(RuntimeContext(date="2026-05-28", file_store=_FileStore(tmp_path)))
|
||||
|
||||
assert failed.success is False
|
||||
assert failed.answer == "Error: ValueError: bad topics"
|
||||
assert failed.metadata["error"] == "ValueError: bad topics"
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_finish_does_not_checkpoint_failed_changed_paths():
|
||||
"""Finish does not checkpoint failed changed paths."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue