fix(proactive): expose topics in primary answer (#380)

This commit is contained in:
Sen Huang 2026-07-20 16:05:47 +08:00 committed by GitHub
parent cf22ef3b1d
commit 55ef4bd6ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 152 additions and 128 deletions

View file

@ -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/` 中维护对应副本。
- 网页文档、导航、版本和部署在统一文档仓库中维护。

View file

@ -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 严格校验。

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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."""