mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-08-28 05:25:04 +00:00
refactor(config): remove daily_cookbook and streamline plugin configs
- Delete the entire daily_cookbook.yaml standalone application config - Remove qwenpaw dependencies verification and related CI workflow steps - Simplify release workflows by removing qwenpaw verification and enforcing reme-ai >=0.4.1.9 - Update plugin start commands and examples to use 'default' or 'demo' configs instead of daily_cookbook - Adjust imports and tests related to daily_cookbook removal and injected_job_kwargs enhancements - Refactor agent wrapper to support injected_job_kwargs for job parameter injection in auto-fin and daily-paper - Improve daily_paper digest prompt to include configured daily directory and correct historical search constraints - Update dependency versions in pyproject.toml files to require reme-ai >=0.4.1.9 and remove qwenpaw optional dependencies - Clean up unused environment variables and obsolete test cases related to daily_cookbook and verification steps
This commit is contained in:
parent
34cc579b97
commit
fbbc5d2715
23 changed files with 114 additions and 539 deletions
21
.github/workflows/_build-python-packages.yml
vendored
21
.github/workflows/_build-python-packages.yml
vendored
|
|
@ -13,11 +13,6 @@ on:
|
|||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
verify_qwenpaw_dependencies:
|
||||
description: Verify that independently published qwenpaw plugins are installable
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -84,22 +79,6 @@ jobs:
|
|||
assert (static_dir() / "index.html").is_file()
|
||||
PY
|
||||
|
||||
# qwenpaw composes independently released plugins. Bootstrap releases may
|
||||
# skip this check to publish the reme-ai version required by those plugins.
|
||||
- name: Verify released qwenpaw dependencies
|
||||
if: inputs.expected_version != '' && inputs.verify_qwenpaw_dependencies
|
||||
run: |
|
||||
REME_WHEEL="$(pwd)/$(ls dist/reme/reme_ai-[0-9]*.whl)"
|
||||
python -m venv "${RUNNER_TEMP}/reme-qwenpaw-package-smoke"
|
||||
"${RUNNER_TEMP}/reme-qwenpaw-package-smoke/bin/python" -m pip install "${REME_WHEEL}[qwenpaw]"
|
||||
cd "${RUNNER_TEMP}"
|
||||
"${RUNNER_TEMP}/reme-qwenpaw-package-smoke/bin/python" - <<'PY'
|
||||
from importlib.metadata import distribution
|
||||
|
||||
assert distribution("reme-auto-fin")
|
||||
assert distribution("reme-daily-paper")
|
||||
PY
|
||||
|
||||
- name: Upload ReMe distributions
|
||||
if: inputs.upload_artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
|
|
|
|||
5
.github/workflows/release-auto-fin.yml
vendored
5
.github/workflows/release-auto-fin.yml
vendored
|
|
@ -75,6 +75,8 @@ jobs:
|
|||
reme_requirement = Requirement(requirements[0])
|
||||
if reme_requirement.name != "reme-ai" or reme_requirement.extras:
|
||||
raise SystemExit(f"Expected a base reme-ai dependency, found {requirements[0]!r}")
|
||||
if Version("0.4.1.8") in reme_requirement.specifier or Version("0.4.1.9") not in reme_requirement.specifier:
|
||||
raise SystemExit(f"Expected reme-ai>=0.4.1.9, found {requirements[0]!r}")
|
||||
with Path(os.environ["GITHUB_OUTPUT"]).open("a", encoding="utf-8") as output:
|
||||
print(f"reme_requirement={reme_requirement}", file=output)
|
||||
print(f"Publishing {project['name']} {actual}")
|
||||
|
|
@ -104,7 +106,8 @@ jobs:
|
|||
python -m zipfile -l "${AUTO_FIN_WHEEL}" | grep 'dist-info/licenses/LICENSE'
|
||||
python -m tarfile -l "${AUTO_FIN_SDIST}" | grep '/LICENSE'
|
||||
python -m venv "${RUNNER_TEMP}/reme-auto-fin-smoke"
|
||||
"${RUNNER_TEMP}/reme-auto-fin-smoke/bin/python" -m pip install "${AUTO_FIN_WHEEL}"
|
||||
"${RUNNER_TEMP}/reme-auto-fin-smoke/bin/python" -m pip install \
|
||||
"agentscope[model-ollama]==2.0.7" "${AUTO_FIN_WHEEL}"
|
||||
cd "${RUNNER_TEMP}"
|
||||
"${RUNNER_TEMP}/reme-auto-fin-smoke/bin/python" - <<'PY'
|
||||
from importlib.metadata import distribution
|
||||
|
|
|
|||
5
.github/workflows/release-daily-paper.yml
vendored
5
.github/workflows/release-daily-paper.yml
vendored
|
|
@ -72,6 +72,8 @@ jobs:
|
|||
reme_requirements = [requirement for requirement in requirements if requirement.name == "reme-ai"]
|
||||
if len(reme_requirements) != 1 or reme_requirements[0].extras:
|
||||
raise SystemExit(f"Expected one base reme-ai dependency, found {reme_requirements!r}")
|
||||
if Version("0.4.1.8") in reme_requirements[0].specifier or Version("0.4.1.9") not in reme_requirements[0].specifier:
|
||||
raise SystemExit(f"Expected reme-ai>=0.4.1.9, found {reme_requirements!r}")
|
||||
if sum(requirement.name == "pypdf" for requirement in requirements) != 1:
|
||||
raise SystemExit("Expected exactly one pypdf dependency")
|
||||
with Path(os.environ["GITHUB_OUTPUT"]).open("a", encoding="utf-8") as output:
|
||||
|
|
@ -105,7 +107,8 @@ jobs:
|
|||
python -m zipfile -l "${DAILY_PAPER_WHEEL}" | grep 'dist-info/licenses/LICENSE'
|
||||
python -m tarfile -l "${DAILY_PAPER_SDIST}" | grep '/LICENSE'
|
||||
python -m venv "${RUNNER_TEMP}/reme-daily-paper-smoke"
|
||||
"${RUNNER_TEMP}/reme-daily-paper-smoke/bin/python" -m pip install "${DAILY_PAPER_WHEEL}"
|
||||
"${RUNNER_TEMP}/reme-daily-paper-smoke/bin/python" -m pip install \
|
||||
"agentscope[model-ollama]==2.0.7" "${DAILY_PAPER_WHEEL}"
|
||||
cd "${RUNNER_TEMP}"
|
||||
"${RUNNER_TEMP}/reme-daily-paper-smoke/bin/python" - <<'PY'
|
||||
from importlib.metadata import distribution
|
||||
|
|
|
|||
9
.github/workflows/release-python.yml
vendored
9
.github/workflows/release-python.yml
vendored
|
|
@ -1,8 +1,5 @@
|
|||
name: Release / Python packages
|
||||
|
||||
# reme-ai[qwenpaw] is normally verified before publication. For a bootstrap
|
||||
# release where the plugins require this new reme-ai version, disable that
|
||||
# verification, publish reme-ai first, and then publish the plugins.
|
||||
# Configure a PyPI Trusted Publisher for this repository, workflow, and its
|
||||
# pypi environment before running the manual release.
|
||||
|
||||
|
|
@ -13,11 +10,6 @@ on:
|
|||
description: Release version
|
||||
required: true
|
||||
type: string
|
||||
verify_qwenpaw_dependencies:
|
||||
description: Verify already-published Auto Fin and Daily Paper packages
|
||||
required: true
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -33,7 +25,6 @@ jobs:
|
|||
with:
|
||||
expected_version: ${{ inputs.version }}
|
||||
upload_artifacts: true
|
||||
verify_qwenpaw_dependencies: ${{ inputs.verify_qwenpaw_dependencies }}
|
||||
|
||||
publish-reme:
|
||||
needs: build
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ reme plugins list --json
|
|||
To compare installed plugins with one application config:
|
||||
|
||||
```bash
|
||||
reme plugins list --config daily_cookbook
|
||||
reme plugins list --config default
|
||||
```
|
||||
|
||||
The optional `ENABLED` column reflects only the `plugins` list resolved from that config. A command-line override used
|
||||
|
|
@ -176,7 +176,7 @@ When the application uses an MCP service, service-enabled plugin Jobs appear as
|
|||
To add the plugin to another application config, select it explicitly:
|
||||
|
||||
```bash
|
||||
reme start config=daily_cookbook plugins='["auto-fin"]'
|
||||
reme start config=demo plugins='["auto-fin"]'
|
||||
```
|
||||
|
||||
## Uninstall a plugin
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ reme plugins list --json
|
|||
对照某个应用配置查看启用状态:
|
||||
|
||||
```bash
|
||||
reme plugins list --config daily_cookbook
|
||||
reme plugins list --config default
|
||||
```
|
||||
|
||||
可选的 `ENABLED` 列只反映该配置解析出的 `plugins` 列表。其他运行中进程使用的 CLI override 不是全局启用状态。
|
||||
|
|
@ -170,7 +170,7 @@ curl -s http://127.0.0.1:2333/auto_fin \
|
|||
如果需要将插件叠加到其他应用配置,则显式选择该配置:
|
||||
|
||||
```bash
|
||||
reme start config=daily_cookbook plugins='["auto-fin"]'
|
||||
reme start config=demo plugins='["auto-fin"]'
|
||||
```
|
||||
|
||||
## 卸载插件
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ reme start plugins='["auto-fin"]' \
|
|||
To add Auto Fin to another application instead, select that config explicitly, for example:
|
||||
|
||||
```bash
|
||||
reme start config=daily_cookbook plugins='["auto-fin"]'
|
||||
reme start config=demo plugins='["auto-fin"]'
|
||||
```
|
||||
|
||||
## Pipeline
|
||||
|
|
@ -109,7 +109,7 @@ refreshes the daily index. No JSONL, intermediate Markdown, or structured Agent
|
|||
| `request_interval` | `10` | Minimum delay in seconds after every CLS request attempt; may be zero |
|
||||
| `max_retries` | `3` | Maximum attempts for each CLS page request; must be at least one |
|
||||
|
||||
The three plugin cron Jobs start with the application and run daily at 09:30, 11:30, and 18:00 in `Asia/Shanghai`.
|
||||
The plugin cron Job starts with the application and runs daily at 18:00 in the application timezone.
|
||||
|
||||
## Output
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ reme start plugins='["auto-fin"]' \
|
|||
如果需要将 Auto Fin 叠加到其他应用,则显式选择相应配置,例如:
|
||||
|
||||
```bash
|
||||
reme start config=daily_cookbook plugins='["auto-fin"]'
|
||||
reme start config=demo plugins='["auto-fin"]'
|
||||
```
|
||||
|
||||
## 流程
|
||||
|
|
@ -100,7 +100,7 @@ workspace 的 Markdown 目标。不存在、绝对路径、越界、带反斜杠
|
|||
| `request_interval` | `10` | 每次财联社请求尝试后的最小等待秒数,可设为 0 |
|
||||
| `max_retries` | `3` | 每页财联社请求的最大尝试次数,至少为 1 |
|
||||
|
||||
插件的三个 cron Job 随应用启动,并按 `Asia/Shanghai` 时区在每天 09:30、11:30 和 18:00 运行。
|
||||
插件的 cron Job 随应用启动,并按应用配置的时区在每天 18:00 运行。
|
||||
|
||||
## 产物
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ license = "Apache-2.0"
|
|||
license-files = ["LICENSE"]
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"reme-ai",
|
||||
"reme-ai>=0.4.1.9",
|
||||
]
|
||||
|
||||
[project.entry-points."reme.plugins"]
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ class AutoFinStep(BaseStep):
|
|||
prompt_name: str,
|
||||
model: type[BaseModel],
|
||||
job_tools: list[str] | None = None,
|
||||
injected_job_kwargs: dict[str, Any] | None = None,
|
||||
**values: str,
|
||||
) -> BaseModel:
|
||||
if self.agent_wrapper is None:
|
||||
|
|
@ -89,6 +90,8 @@ class AutoFinStep(BaseStep):
|
|||
kwargs: dict[str, Any] = {"output_schema": model}
|
||||
if job_tools:
|
||||
kwargs["job_tools"] = job_tools
|
||||
if injected_job_kwargs:
|
||||
kwargs["injected_job_kwargs"] = injected_job_kwargs
|
||||
result = await self.agent_wrapper.reply(prompt, **kwargs)
|
||||
if not isinstance(result, dict) or result.get("structured_output") is None:
|
||||
raise ValueError(f"Auto Fin Agent returned no structured output: {self._preview(result)}")
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||
|
||||
import json
|
||||
import re
|
||||
from datetime import date
|
||||
from datetime import date, timedelta
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
|
@ -107,10 +107,17 @@ class AutoFinMergeStep(AutoFinStep):
|
|||
if self.context.get("auto_fin_skipped"):
|
||||
return self.context.response
|
||||
run_date = date.fromisoformat(str(self._required("auto_fin_date")))
|
||||
historical_search = {
|
||||
"limit": 5,
|
||||
"min_score": 0.0,
|
||||
"start_date": None,
|
||||
"end_date": (run_date - timedelta(days=1)).isoformat(),
|
||||
}
|
||||
output = await self._reply(
|
||||
"merge_user",
|
||||
AutoFinReportOutput,
|
||||
job_tools=list(self.kwargs.get("job_tools") or []),
|
||||
injected_job_kwargs=historical_search,
|
||||
decision_at=str(self._required("auto_fin_decision_at")),
|
||||
window_start=str(self._required("auto_fin_window_start")),
|
||||
topics=json.dumps(self._required("auto_fin_topics"), ensure_ascii=False),
|
||||
|
|
|
|||
|
|
@ -202,7 +202,16 @@ async def test_merge_writes_only_final_report_and_validates_historical_links(tmp
|
|||
assert "end_date" not in prompt
|
||||
assert "调用 `search`" in prompt
|
||||
assert "调用 `read`" in prompt
|
||||
assert kwargs == {"output_schema": AutoFinReportOutput, "job_tools": ["search", "read"]}
|
||||
assert kwargs == {
|
||||
"output_schema": AutoFinReportOutput,
|
||||
"job_tools": ["search", "read"],
|
||||
"injected_job_kwargs": {
|
||||
"limit": 5,
|
||||
"min_score": 0.0,
|
||||
"start_date": None,
|
||||
"end_date": "2026-08-09",
|
||||
},
|
||||
}
|
||||
report = (tmp_path / "daily" / "2026-08-10" / "auto_fin.md").read_text(encoding="utf-8")
|
||||
assert "[[daily/2026-08-01/auto_fin.md|历史黄金观察]]" in report
|
||||
assert "](daily/2026-08-01/auto_fin.md)" not in report
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ license-files = ["LICENSE"]
|
|||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"pypdf>=5.0.0",
|
||||
"reme-ai",
|
||||
"reme-ai>=0.4.1.9",
|
||||
]
|
||||
|
||||
[project.entry-points."reme.plugins"]
|
||||
|
|
|
|||
|
|
@ -80,14 +80,23 @@ class DailyPaperDigestStep(DailyPaperStep):
|
|||
|
||||
documents = [{"title": item.title, "desc": item.desc, "body": item.body} for item in analyses]
|
||||
wikilinks = [f"[[{item.note_path}]]" for item in analyses]
|
||||
run_day = dt.date.fromisoformat(self._run_day())
|
||||
daily_dir = str(self.config_value("daily_dir")).strip("/")
|
||||
self.logger.info(f"[{self.name}] agent start notes={len(analyses)}")
|
||||
result = await self.agent_wrapper.reply(
|
||||
self.prompt_format(
|
||||
"digest_user",
|
||||
documents=json.dumps(documents, ensure_ascii=False, indent=2),
|
||||
daily_dir=daily_dir,
|
||||
),
|
||||
output_schema=DailyPaperMarkdownOutput,
|
||||
job_tools=list(self.kwargs.get("job_tools") or []),
|
||||
injected_job_kwargs={
|
||||
"limit": 20,
|
||||
"min_score": 0.0,
|
||||
"start_date": None,
|
||||
"end_date": (run_day - dt.timedelta(days=1)).isoformat(),
|
||||
},
|
||||
)
|
||||
self.logger.info(f"[{self.name}] agent done notes={len(analyses)}")
|
||||
output = structured_output(result, DailyPaperMarkdownOutput)
|
||||
|
|
@ -95,8 +104,7 @@ class DailyPaperDigestStep(DailyPaperStep):
|
|||
if not output.desc.strip() or not body:
|
||||
raise ValueError("Agent returned an empty daily paper brief")
|
||||
|
||||
day = self._run_day()
|
||||
daily_dir = str(self.config_value("daily_dir")).strip("/")
|
||||
day = run_day.isoformat()
|
||||
title = normalize_chinese_title(output.title, f"每日论文简报-{day}")
|
||||
existing_rel = str(self._state("existing_digest_path") or "").strip()
|
||||
existing_path = self.workspace_path / existing_rel if existing_rel else None
|
||||
|
|
@ -109,7 +117,7 @@ class DailyPaperDigestStep(DailyPaperStep):
|
|||
existing=existing_path,
|
||||
)
|
||||
digest_rel = digest_path.relative_to(self.workspace_path).as_posix()
|
||||
body = self._validate_historical_wikilinks(body, dt.date.fromisoformat(day), digest_path)
|
||||
body = self._validate_historical_wikilinks(body, run_day, digest_path)
|
||||
body += "\n\n## 详细论文\n\n" + "\n".join(f"- {link}" for link in wikilinks)
|
||||
selected_ids = [item.arxiv_id for item in analyses]
|
||||
await write_markdown(
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ digest_user: |
|
|||
内容只能依据输入文档,不得补充文档中没有提供的事实。
|
||||
保留技术准确性,同时解释三篇论文为什么值得关注,以及它们之间有什么联系。
|
||||
|
||||
在写作前,先调用 `search` 检索以前的文章:围绕三篇论文的核心问题、方法、关键词和同义表达组织查询,
|
||||
使用 limit=20。主题跨度较大时可以多次检索。只把 `daily/` 下日期早于今天、
|
||||
在写作前,先调用 `search` 检索以前的文章:围绕三篇论文的核心问题、方法、关键词和同义表达组织查询。
|
||||
主题跨度较大时可以多次检索。只把 `{daily_dir}/` 下日期早于今天、
|
||||
且与本期内容确实相似或互补的 Markdown 文章作为候选;必要时调用 `read` 核验全文,不要仅凭标题判断。
|
||||
将确认相关的旧文章以 Wikilink 自然织入正文,并用句子说明关联(延续、对比、补充或方法相似);
|
||||
链接必须采用带 `.md` 的完整 workspace-relative 路径,例如
|
||||
`[[daily/2026-07-01/旧文章.md|此前的相关解读]]`。不要输出裸链接、独立关系字段,也不要虚构搜索未命中的路径。
|
||||
`[[{daily_dir}/2026-07-01/旧文章.md|此前的相关解读]]`。不要输出裸链接、独立关系字段,也不要虚构搜索未命中的路径。
|
||||
旧文章只用于判断关联和建立链接,不得用来补充本期事实。如果没有真正相关的旧文章,不要强行添加;
|
||||
当日三篇详细解读的链接会由系统统一附在文末。
|
||||
|
||||
|
|
|
|||
|
|
@ -627,6 +627,26 @@ def test_daily_paper_cron_hf_mirror_defaults_enabled_with_environment_override(m
|
|||
assert _plugin_config()["jobs"]["daily_paper_cron"]["use_hf_mirror"] is False
|
||||
|
||||
|
||||
def test_digest_prompt_uses_configured_daily_directory(tmp_path: Path):
|
||||
"""Use the host application's daily directory in historical-link guidance."""
|
||||
step = DailyPaperDigestStep(
|
||||
app_context=ApplicationContext(
|
||||
workspace_dir=str(tmp_path),
|
||||
daily_dir="memory",
|
||||
),
|
||||
)
|
||||
|
||||
prompt = step.prompt_format(
|
||||
"digest_user",
|
||||
documents="[]",
|
||||
daily_dir=str(step.config_value("daily_dir")).strip("/"),
|
||||
)
|
||||
|
||||
assert "`memory/`" in prompt
|
||||
assert "[[memory/2026-07-01/旧文章.md" in prompt
|
||||
assert "[[daily/2026-07-01/" not in prompt
|
||||
|
||||
|
||||
def test_paper_pick_list_uses_an_object_root_for_tool_output():
|
||||
"""AgentScope function arguments require an object-root JSON schema."""
|
||||
schema = PaperPickList.model_json_schema()
|
||||
|
|
@ -887,6 +907,12 @@ async def test_pipeline_filters_strict_yesterday_and_writes_outputs(
|
|||
assert cc_wrapper.calls[-1]["kwargs"] == {
|
||||
"output_schema": DailyPaperMarkdownOutput,
|
||||
"job_tools": ["search", "read"],
|
||||
"injected_job_kwargs": {
|
||||
"limit": 20,
|
||||
"min_score": 0.0,
|
||||
"start_date": None,
|
||||
"end_date": "2026-07-20",
|
||||
},
|
||||
}
|
||||
assert [call["kwargs"]["output_schema"] for call in cc_wrapper.calls] == [
|
||||
PaperPickList,
|
||||
|
|
@ -908,6 +934,7 @@ async def test_pipeline_filters_strict_yesterday_and_writes_outputs(
|
|||
assert "长期记忆" not in digest_prompt
|
||||
assert "先调用 `search` 检索以前的文章" in digest_prompt
|
||||
assert "end_date" not in digest_prompt
|
||||
assert "limit=" not in digest_prompt
|
||||
assert "Wikilink" in digest_prompt
|
||||
|
||||
rerun = RuntimeContext(date="2026-07-21")
|
||||
|
|
|
|||
|
|
@ -62,11 +62,6 @@ core = [
|
|||
"polars>=1.43.0",
|
||||
"reme_studio",
|
||||
]
|
||||
qwenpaw = [
|
||||
"reme-ai",
|
||||
"reme-auto-fin>=0.1.2",
|
||||
"reme-daily-paper>=0.1.2",
|
||||
]
|
||||
dev = [
|
||||
"packaging>=24.2",
|
||||
"pre-commit>=4.6.1",
|
||||
|
|
|
|||
|
|
@ -1,416 +0,0 @@
|
|||
app_name: ReMe Daily Cookbook
|
||||
workspace_dir: ${DAILY_PAPER_WORKSPACE_DIR:-reme_workspace}
|
||||
timezone: Asia/Shanghai
|
||||
language: zh
|
||||
|
||||
# This is a standalone application config. It intentionally does not inherit
|
||||
# default.yaml and listens on a separate port so it can run beside ReMe.
|
||||
service:
|
||||
backend: http
|
||||
host: ${DAILY_PAPER_HOST:-127.0.0.1}
|
||||
port: ${DAILY_PAPER_PORT:-8001}
|
||||
|
||||
jobs:
|
||||
index_update_loop:
|
||||
backend: background
|
||||
watch_dirs: [daily_dir, digest_dir]
|
||||
watch_suffixes: [md, jsonl]
|
||||
steps:
|
||||
- backend: init_changes_step
|
||||
monitor_type: file_store
|
||||
monitor_name: default
|
||||
dispatch_steps: [update_index_step]
|
||||
- backend: watch_changes_step
|
||||
dispatch_steps:
|
||||
- backend: update_index_step
|
||||
persist: false
|
||||
|
||||
auto_dream:
|
||||
backend: base
|
||||
description: "Auto-dream: consolidate recent daily notes into digest memory and interest topics."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
date:
|
||||
type: string
|
||||
description: "YYYY-MM-DD to scan; defaults to today in the configured timezone."
|
||||
default: ""
|
||||
hint:
|
||||
type: string
|
||||
description: "Optional guidance for extraction and integration."
|
||||
default: ""
|
||||
scan_days:
|
||||
type: integer
|
||||
description: "Number of recent daily directories to scan."
|
||||
default: 2
|
||||
max_units:
|
||||
type: integer
|
||||
description: "Maximum number of extracted memory units."
|
||||
default: 5
|
||||
topic_count:
|
||||
type: integer
|
||||
description: "Maximum number of interest topics to write."
|
||||
default: 3
|
||||
topic_diversity_days:
|
||||
type: integer
|
||||
description: "Previous interest-topic days used for de-duplication."
|
||||
default: 7
|
||||
steps:
|
||||
- backend: dream_extract_step
|
||||
file_catalog: dream
|
||||
topic_session_id: interests
|
||||
scan_days: 2
|
||||
max_units: 5
|
||||
- backend: dream_integrate_step
|
||||
- backend: dream_topics_step
|
||||
topic_count: 3
|
||||
topic_diversity_days: 7
|
||||
- backend: dream_finish_step
|
||||
file_catalog: dream
|
||||
|
||||
auto_memory:
|
||||
backend: base
|
||||
description: "Auto-memory: record conversation facts into a daily note."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
messages:
|
||||
type: array
|
||||
description: "Conversation messages."
|
||||
items:
|
||||
type: object
|
||||
session_id:
|
||||
type: string
|
||||
description: "Source conversation session identifier."
|
||||
default: ""
|
||||
memory_hint:
|
||||
type: string
|
||||
description: "Optional memory-writing guidance."
|
||||
date:
|
||||
type: string
|
||||
description: "YYYY-MM-DD daily-note date; empty infers it from messages or current time."
|
||||
default: ""
|
||||
required: [messages]
|
||||
steps:
|
||||
- backend: auto_memory_step
|
||||
|
||||
reindex:
|
||||
backend: base
|
||||
description: "Wipe the derived search store and rebuild it from memory files."
|
||||
watch_dirs: [daily_dir, digest_dir]
|
||||
watch_suffixes: [md, jsonl]
|
||||
parameters:
|
||||
type: object
|
||||
properties: {}
|
||||
steps:
|
||||
- backend: clear_store_step
|
||||
- backend: init_changes_step
|
||||
monitor_type: file_store
|
||||
monitor_name: default
|
||||
dispatch_steps: [update_index_step]
|
||||
|
||||
memory_search:
|
||||
backend: base
|
||||
description: "Long-term memory retrieval via hybrid workspace search (vector + BM25, RRF-fused)."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
query:
|
||||
type: string
|
||||
description: "Search query."
|
||||
limit:
|
||||
type: integer
|
||||
description: "Maximum number of results."
|
||||
default: 5
|
||||
min_score:
|
||||
type: number
|
||||
description: "Minimum fused score."
|
||||
default: 0.0
|
||||
start_date:
|
||||
type: string
|
||||
description: "Optional inclusive start date (YYYY-MM-DD)."
|
||||
end_date:
|
||||
type: string
|
||||
description: "Optional inclusive end date (YYYY-MM-DD)."
|
||||
required: [query]
|
||||
steps:
|
||||
- backend: search_step
|
||||
vector_weight: 0.7
|
||||
candidate_multiplier: 5.0
|
||||
expand_links: true
|
||||
max_links_per_direction: 10
|
||||
|
||||
node_search:
|
||||
backend: base
|
||||
description: "Recall digest nodes for auto-dream de-duplication and linking."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
query:
|
||||
type: string
|
||||
description: "Candidate memory-node name and description."
|
||||
limit:
|
||||
type: integer
|
||||
description: "Maximum number of digest nodes."
|
||||
default: 20
|
||||
required: [query]
|
||||
steps:
|
||||
- backend: node_search_step
|
||||
vector_weight: 0.7
|
||||
candidate_multiplier: 5.0
|
||||
|
||||
daily_list:
|
||||
backend: base
|
||||
description: "List notes under one day."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
date:
|
||||
type: string
|
||||
description: "YYYY-MM-DD; empty means today."
|
||||
default: ""
|
||||
steps:
|
||||
- backend: daily_list_step
|
||||
|
||||
frontmatter_read:
|
||||
backend: base
|
||||
description: "Read a file's frontmatter."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: "Workspace-relative path."
|
||||
required: [path]
|
||||
steps:
|
||||
- backend: frontmatter_read_step
|
||||
|
||||
frontmatter_update:
|
||||
backend: base
|
||||
description: "Merge key-values into a file's frontmatter."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: "Workspace-relative path."
|
||||
metadata:
|
||||
type: object
|
||||
description: "Key-values to merge."
|
||||
required: [path, metadata]
|
||||
steps:
|
||||
- backend: frontmatter_update_step
|
||||
|
||||
move:
|
||||
backend: base
|
||||
description: "Move or rename a workspace file and retarget inbound wikilinks."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
src_path:
|
||||
type: string
|
||||
description: "Workspace-relative source path."
|
||||
dst_path:
|
||||
type: string
|
||||
description: "Workspace-relative destination path."
|
||||
overwrite:
|
||||
type: boolean
|
||||
default: false
|
||||
retarget:
|
||||
type: boolean
|
||||
default: true
|
||||
required: [src_path, dst_path]
|
||||
steps:
|
||||
- backend: move_step
|
||||
|
||||
read:
|
||||
backend: base
|
||||
description: "Read a markdown file under the workspace."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: "Workspace-relative markdown path."
|
||||
start_line:
|
||||
type: integer
|
||||
end_line:
|
||||
type: integer
|
||||
required: [path]
|
||||
steps:
|
||||
- backend: read_step
|
||||
with_neighbors: false
|
||||
max_neighbors_per_direction: 10
|
||||
|
||||
write:
|
||||
backend: base
|
||||
description: "Create or overwrite a markdown file with frontmatter."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: "Workspace-relative markdown path."
|
||||
name:
|
||||
type: string
|
||||
description: "Frontmatter name."
|
||||
description:
|
||||
type: string
|
||||
description: "Frontmatter description."
|
||||
content:
|
||||
type: string
|
||||
description: "Markdown body."
|
||||
metadata:
|
||||
type: object
|
||||
description: "Optional extra frontmatter fields."
|
||||
required: [path, name, description, content]
|
||||
steps:
|
||||
- backend: write_step
|
||||
|
||||
daily_write:
|
||||
backend: base
|
||||
description: "Write a daily markdown note linked to its source conversation."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "Filename stem and frontmatter name."
|
||||
description:
|
||||
type: string
|
||||
description: "Frontmatter description."
|
||||
session_id:
|
||||
type: string
|
||||
description: "Source conversation session identifier."
|
||||
content:
|
||||
type: string
|
||||
description: "Markdown body."
|
||||
date:
|
||||
type: string
|
||||
description: "YYYY-MM-DD; empty means today."
|
||||
default: ""
|
||||
metadata:
|
||||
type: object
|
||||
description: "Optional extra frontmatter fields."
|
||||
required: [name, description, session_id, content]
|
||||
steps:
|
||||
- backend: daily_write_step
|
||||
|
||||
edit:
|
||||
backend: base
|
||||
description: "Find and replace text in a markdown file."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: "Workspace-relative path."
|
||||
old:
|
||||
type: string
|
||||
description: "Text to replace."
|
||||
new:
|
||||
type: string
|
||||
description: "Replacement text."
|
||||
default: ""
|
||||
required: [path, old, new]
|
||||
steps:
|
||||
- backend: edit_step
|
||||
|
||||
dingtalk_wait:
|
||||
backend: background
|
||||
supervisor: true
|
||||
close_timeout: 10
|
||||
steps:
|
||||
- backend: dingtalk_wait_step
|
||||
app_key: ${DINGTALK_APP_KEY:-}
|
||||
app_secret: ${DINGTALK_APP_SECRET:-}
|
||||
robot_code: ${DINGTALK_ROBOT_CODE:-}
|
||||
worker_count: 4
|
||||
builtin_tools: [bash]
|
||||
job_tools:
|
||||
- memory_search
|
||||
- read
|
||||
- write
|
||||
- edit
|
||||
- daily_list
|
||||
- daily_write
|
||||
- frontmatter_read
|
||||
- frontmatter_update
|
||||
|
||||
components:
|
||||
tokenizer:
|
||||
default:
|
||||
backend: regex
|
||||
|
||||
as_llm:
|
||||
default:
|
||||
backend: openai
|
||||
model: ${LLM_MODEL_NAME:-qwen3.7-plus}
|
||||
stream: true
|
||||
context_size: 200000
|
||||
max_retries: 3
|
||||
credential:
|
||||
api_key: ${LLM_API_KEY:-}
|
||||
base_url: ${LLM_BASE_URL:-}
|
||||
parameters:
|
||||
max_tokens: 65536
|
||||
thinking_enable: false
|
||||
|
||||
agent_wrapper:
|
||||
default:
|
||||
backend: agentscope
|
||||
as_llm: default
|
||||
builtin_tools: false
|
||||
|
||||
# as_embedding:
|
||||
# default:
|
||||
# backend: openai
|
||||
# model: ${EMBEDDING_MODEL_NAME:-text-embedding-v4}
|
||||
# dimensions: 1024
|
||||
# max_retries: 0
|
||||
# credential:
|
||||
# api_key: ${EMBEDDING_API_KEY:-}
|
||||
# base_url: ${EMBEDDING_BASE_URL:-https://dashscope.aliyuncs.com/compatible-mode/v1}
|
||||
# parameters: {}
|
||||
#
|
||||
# embedding_store:
|
||||
# default:
|
||||
# backend: local
|
||||
# as_embedding: default
|
||||
# max_retries: 3
|
||||
# quota_retry_delay: 60.0
|
||||
|
||||
file_graph:
|
||||
default:
|
||||
backend: local
|
||||
|
||||
file_catalog:
|
||||
dream:
|
||||
backend: local
|
||||
|
||||
file_chunker:
|
||||
markdown:
|
||||
backend: markdown
|
||||
supported_extensions: [md]
|
||||
embed_toc: true
|
||||
max_ast_sections: 100
|
||||
include_frontmatter_in_metadata: false
|
||||
include_frontmatter_keys_in_metadata: []
|
||||
jsonl:
|
||||
backend: jsonl
|
||||
supported_extensions: [jsonl]
|
||||
max_lines_per_chunk: 1
|
||||
|
||||
keyword_index:
|
||||
default:
|
||||
backend: bm25
|
||||
tokenizer: default
|
||||
|
||||
file_store:
|
||||
default:
|
||||
backend: local
|
||||
store_name: local
|
||||
# embedding_store: default
|
||||
embedding_store: ""
|
||||
keyword_index: default
|
||||
file_graph: default
|
||||
|
|
@ -123,14 +123,6 @@ def test_default_config_keeps_frontmatter_chunk_metadata_opt_in():
|
|||
) in (None, [])
|
||||
|
||||
|
||||
def test_daily_cookbook_chunks_jsonl_one_line_at_a_time():
|
||||
"""Daily cookbook keeps JSONL records as individually addressable chunks."""
|
||||
cfg = _load_config("daily_cookbook.yaml")
|
||||
|
||||
jsonl = cfg["components"]["file_chunker"]["jsonl"]
|
||||
assert jsonl["max_lines_per_chunk"] == 1
|
||||
|
||||
|
||||
def test_parse_args_rejects_non_key_value_extra_argument():
|
||||
"""Extra CLI arguments must use key=value syntax."""
|
||||
with pytest.raises(ValueError, match="expected key=value"):
|
||||
|
|
|
|||
|
|
@ -9,10 +9,8 @@ from unittest.mock import MagicMock
|
|||
|
||||
import pytest
|
||||
|
||||
from reme.components import ApplicationContext, R
|
||||
from reme.components import ApplicationContext
|
||||
from reme.components.agent_wrapper.base_agent_wrapper import BaseAgentWrapper
|
||||
from reme.config.config_parser import _load_config
|
||||
from reme.enumeration import ComponentEnum
|
||||
from reme.steps.cookbook.dingtalk.wait import DingTalkWaitStep, _session_key
|
||||
|
||||
|
||||
|
|
@ -203,54 +201,6 @@ async def test_final_reply_injects_only_configured_tools(tmp_path):
|
|||
]
|
||||
|
||||
|
||||
def test_daily_cookbook_registers_one_step_background_wait_job(monkeypatch):
|
||||
for name in ("DINGTALK_APP_KEY", "DINGTALK_APP_SECRET", "DINGTALK_ROBOT_CODE"):
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
config = _load_config("daily_cookbook")
|
||||
job = config["jobs"]["dingtalk_wait"]
|
||||
assert job["backend"] == "background"
|
||||
assert job["steps"] == [
|
||||
{
|
||||
"backend": "dingtalk_wait_step",
|
||||
"app_key": "",
|
||||
"app_secret": "",
|
||||
"robot_code": "",
|
||||
"worker_count": 4,
|
||||
"builtin_tools": ["bash"],
|
||||
"job_tools": [
|
||||
"memory_search",
|
||||
"read",
|
||||
"write",
|
||||
"edit",
|
||||
"daily_list",
|
||||
"daily_write",
|
||||
"frontmatter_read",
|
||||
"frontmatter_update",
|
||||
],
|
||||
},
|
||||
]
|
||||
assert config["components"]["agent_wrapper"] == {
|
||||
"default": {
|
||||
"backend": "agentscope",
|
||||
"as_llm": "default",
|
||||
"builtin_tools": False,
|
||||
},
|
||||
}
|
||||
assert R.get(ComponentEnum.STEP, "dingtalk_wait_step") is DingTalkWaitStep
|
||||
|
||||
|
||||
def test_daily_cookbook_passes_dingtalk_environment_to_step(monkeypatch):
|
||||
monkeypatch.setenv("DINGTALK_APP_KEY", "app-key")
|
||||
monkeypatch.setenv("DINGTALK_APP_SECRET", "app-secret")
|
||||
monkeypatch.setenv("DINGTALK_ROBOT_CODE", "robot-code")
|
||||
step = _load_config("daily_cookbook")["jobs"]["dingtalk_wait"]["steps"][0]
|
||||
assert (step["app_key"], step["app_secret"], step["robot_code"]) == (
|
||||
"app-key",
|
||||
"app-secret",
|
||||
"robot-code",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_client_closes_when_background_stop_is_set(monkeypatch):
|
||||
websocket = _WebSocket()
|
||||
|
|
|
|||
|
|
@ -65,6 +65,31 @@ def test_strip_injected_parameters_hides_keys_from_schema():
|
|||
assert "date" in job.parameters["properties"]
|
||||
|
||||
|
||||
def test_search_injection_exposes_only_query():
|
||||
parameters = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string"},
|
||||
"limit": {"type": "integer"},
|
||||
"min_score": {"type": "number"},
|
||||
"start_date": {"type": "string"},
|
||||
"end_date": {"type": "string"},
|
||||
},
|
||||
"required": ["query"],
|
||||
}
|
||||
injected = {
|
||||
"limit": 20,
|
||||
"min_score": 0.0,
|
||||
"start_date": None,
|
||||
"end_date": "2026-07-20",
|
||||
}
|
||||
|
||||
stripped = BaseAgentWrapper._strip_injected_parameters(parameters, injected)
|
||||
|
||||
assert stripped["properties"] == {"query": {"type": "string"}}
|
||||
assert stripped["required"] == ["query"]
|
||||
|
||||
|
||||
# -- AgentScope wrapper -----------------------------------------------------------
|
||||
|
||||
|
||||
|
|
@ -139,7 +164,7 @@ class _RecordingWrapper(BaseAgentWrapper):
|
|||
super().__init__(**kwargs)
|
||||
self.calls: list[dict] = []
|
||||
|
||||
async def reply(self, inputs, **kwargs) -> dict:
|
||||
async def reply(self, _inputs, **kwargs) -> dict:
|
||||
self.calls.append(kwargs)
|
||||
return {"session_id": "s-1", "last_message": {}, "result": "ok"}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import tomllib
|
|||
from types import ModuleType
|
||||
|
||||
from packaging.requirements import Requirement
|
||||
from packaging.version import Version
|
||||
import pytest
|
||||
|
||||
REPOSITORY = Path(__file__).resolve().parents[2]
|
||||
|
|
@ -45,11 +46,7 @@ def test_studio_packages_have_independent_identity() -> None:
|
|||
assert main_config["project"]["optional-dependencies"]["web"] == ["reme_studio"]
|
||||
assert main_config["project"]["optional-dependencies"]["core"].count("reme-ai[as]") == 1
|
||||
assert main_config["project"]["optional-dependencies"]["core"].count("reme_studio") == 1
|
||||
assert main_config["project"]["optional-dependencies"]["qwenpaw"] == [
|
||||
"reme-ai",
|
||||
"reme-auto-fin>=0.1.2",
|
||||
"reme-daily-paper>=0.1.2",
|
||||
]
|
||||
assert "qwenpaw" not in main_config["project"]["optional-dependencies"]
|
||||
assert auto_fin_config["project"]["version"] == "0.1.2"
|
||||
assert daily_paper_config["project"]["version"] == "0.1.2"
|
||||
assert main_config["tool"]["setuptools"]["packages"]["find"]["include"] == ["reme", "reme.*"]
|
||||
|
|
@ -161,7 +158,8 @@ def test_auto_fin_requires_reme_base() -> None:
|
|||
|
||||
assert len(reme_requirements) == 1
|
||||
assert not reme_requirements[0].extras
|
||||
assert not reme_requirements[0].specifier
|
||||
assert Version("0.4.1.8") not in reme_requirements[0].specifier
|
||||
assert Version("0.4.1.9") in reme_requirements[0].specifier
|
||||
|
||||
|
||||
def test_daily_paper_license_matches_repository() -> None:
|
||||
|
|
@ -178,7 +176,8 @@ def test_daily_paper_declares_runtime_dependencies() -> None:
|
|||
by_name = {requirement.name: requirement for requirement in requirements}
|
||||
|
||||
assert not by_name["reme-ai"].extras
|
||||
assert not by_name["reme-ai"].specifier
|
||||
assert Version("0.4.1.8") not in by_name["reme-ai"].specifier
|
||||
assert Version("0.4.1.9") in by_name["reme-ai"].specifier
|
||||
assert "pypdf" in by_name
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ def test_list_plugins_marks_configured_plugins(monkeypatch, tmp_path, capsys):
|
|||
)
|
||||
monkeypatch.setattr(plugin_cli_module, "_enabled_plugins", lambda _config: {"auto-fin"})
|
||||
|
||||
assert plugin_cli_module.plugin_cli(["list", "--config", "daily_cookbook"]) == 0
|
||||
assert plugin_cli_module.plugin_cli(["list", "--config", "default"]) == 0
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "ENABLED" in output
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue