mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-08-28 05:25:04 +00:00
Compare commits
No commits in common. "main" and "v0.4.1.8" have entirely different histories.
58 changed files with 778 additions and 804 deletions
12
.github/workflows/_build-docs.yml
vendored
12
.github/workflows/_build-docs.yml
vendored
|
|
@ -26,14 +26,12 @@ jobs:
|
|||
working-directory: github-pages
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22.22.3'
|
||||
node-version: '22.13'
|
||||
cache: npm
|
||||
cache-dependency-path: github-pages/package-lock.json
|
||||
|
||||
|
|
@ -49,10 +47,10 @@ jobs:
|
|||
|
||||
- name: Configure Pages
|
||||
if: inputs.upload_pages_artifact
|
||||
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6
|
||||
uses: actions/configure-pages@v6
|
||||
|
||||
- name: Upload Pages artifact
|
||||
if: inputs.upload_pages_artifact
|
||||
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: github-pages/dist
|
||||
|
|
|
|||
24
.github/workflows/_build-python-packages.yml
vendored
24
.github/workflows/_build-python-packages.yml
vendored
|
|
@ -23,12 +23,10 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
|
|
@ -79,9 +77,25 @@ jobs:
|
|||
assert (static_dir() / "index.html").is_file()
|
||||
PY
|
||||
|
||||
# qwenpaw composes independently released plugins. Keep this before the
|
||||
# artifact upload so a core release cannot advertise an unavailable extra.
|
||||
- name: Verify released qwenpaw dependencies
|
||||
if: inputs.expected_version != ''
|
||||
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
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: reme-distributions
|
||||
path: dist/reme/
|
||||
|
|
|
|||
2
.github/workflows/ci-docs.yml
vendored
2
.github/workflows/ci-docs.yml
vendored
|
|
@ -16,6 +16,7 @@ on:
|
|||
- 'typescript/README*.md'
|
||||
- 'plugins/*/README*.md'
|
||||
- 'benchmark/*/README*.md'
|
||||
- 'skills/reme_memory/SKILL.md'
|
||||
pull_request:
|
||||
branches: [main, master, dev, develop]
|
||||
paths:
|
||||
|
|
@ -31,6 +32,7 @@ on:
|
|||
- 'typescript/README*.md'
|
||||
- 'plugins/*/README*.md'
|
||||
- 'benchmark/*/README*.md'
|
||||
- 'skills/reme_memory/SKILL.md'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
|
|
|
|||
10
.github/workflows/ci-packages.yml
vendored
10
.github/workflows/ci-packages.yml
vendored
|
|
@ -8,10 +8,9 @@ on:
|
|||
- '.github/workflows/_build-python-packages.yml'
|
||||
- '.github/workflows/release-python.yml'
|
||||
- 'pyproject.toml'
|
||||
- 'README.md'
|
||||
- 'reme/**'
|
||||
- 'reme/__init__.py'
|
||||
- 'reme/utils/web_static.py'
|
||||
- 'scripts/bump_version.py'
|
||||
- 'tests/unit/test_package_versions.py'
|
||||
- 'LICENSE'
|
||||
pull_request:
|
||||
branches: [main, master, dev, develop]
|
||||
|
|
@ -20,10 +19,9 @@ on:
|
|||
- '.github/workflows/_build-python-packages.yml'
|
||||
- '.github/workflows/release-python.yml'
|
||||
- 'pyproject.toml'
|
||||
- 'README.md'
|
||||
- 'reme/**'
|
||||
- 'reme/__init__.py'
|
||||
- 'reme/utils/web_static.py'
|
||||
- 'scripts/bump_version.py'
|
||||
- 'tests/unit/test_package_versions.py'
|
||||
- 'LICENSE'
|
||||
workflow_dispatch:
|
||||
|
||||
|
|
|
|||
6
.github/workflows/ci-python-quality.yml
vendored
6
.github/workflows/ci-python-quality.yml
vendored
|
|
@ -17,12 +17,10 @@ jobs:
|
|||
name: Pre-commit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: pip
|
||||
|
|
|
|||
6
.github/workflows/ci-python-tests.yml
vendored
6
.github/workflows/ci-python-tests.yml
vendored
|
|
@ -24,12 +24,10 @@ jobs:
|
|||
python-version: ["3.11", "3.12", "3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
|
|
|
|||
23
.github/workflows/ci-reme-studio.yml
vendored
23
.github/workflows/ci-reme-studio.yml
vendored
|
|
@ -38,14 +38,12 @@ jobs:
|
|||
working-directory: reme_studio
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22.22.3"
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
cache-dependency-path: reme_studio/package-lock.json
|
||||
|
||||
|
|
@ -62,12 +60,10 @@ jobs:
|
|||
run: npm test
|
||||
|
||||
- name: Verify npm package
|
||||
run: |
|
||||
npm pack --pack-destination "${RUNNER_TEMP}"
|
||||
tar -tzf "${RUNNER_TEMP}"/agentscope-ai-reme_studio-*.tgz | grep '^package/dist-static/index.html$'
|
||||
run: npm pack --dry-run
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
|
@ -79,12 +75,3 @@ jobs:
|
|||
python scripts/package_studio.py
|
||||
python -m build reme_studio --outdir dist/studio
|
||||
python -m twine check dist/studio/*
|
||||
STUDIO_WHEEL="$(pwd)/$(ls dist/studio/reme_studio-*.whl)"
|
||||
python -m venv "${RUNNER_TEMP}/reme-studio-package-smoke"
|
||||
"${RUNNER_TEMP}/reme-studio-package-smoke/bin/python" -m pip install "${STUDIO_WHEEL}"
|
||||
cd "${RUNNER_TEMP}"
|
||||
"${RUNNER_TEMP}/reme-studio-package-smoke/bin/python" - <<'PY'
|
||||
from reme_studio import static_dir
|
||||
|
||||
assert (static_dir() / "index.html").is_file()
|
||||
PY
|
||||
|
|
|
|||
6
.github/workflows/ci-typescript.yml
vendored
6
.github/workflows/ci-typescript.yml
vendored
|
|
@ -31,11 +31,9 @@ jobs:
|
|||
working-directory: typescript
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22.22.3'
|
||||
cache: npm
|
||||
|
|
|
|||
6
.github/workflows/ci-windows.yml
vendored
6
.github/workflows/ci-windows.yml
vendored
|
|
@ -24,12 +24,10 @@ jobs:
|
|||
python-version: ["3.11"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
|
|
|
|||
10
.github/workflows/deploy-docs.yml
vendored
10
.github/workflows/deploy-docs.yml
vendored
|
|
@ -13,6 +13,7 @@ on:
|
|||
- "typescript/README*.md"
|
||||
- "plugins/*/README*.md"
|
||||
- "benchmark/*/README*.md"
|
||||
- "skills/reme_memory/SKILL.md"
|
||||
- "AGENTS.md"
|
||||
- ".github/workflows/deploy-docs.yml"
|
||||
- ".github/workflows/_build-docs.yml"
|
||||
|
|
@ -20,6 +21,8 @@ on:
|
|||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
|
|
@ -30,7 +33,7 @@ jobs:
|
|||
name: Build documentation
|
||||
uses: ./.github/workflows/_build-docs.yml
|
||||
with:
|
||||
run_tests: true
|
||||
run_tests: false
|
||||
upload_pages_artifact: true
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -43,10 +46,7 @@ jobs:
|
|||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Deploy
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
|
||||
uses: actions/deploy-pages@v5
|
||||
|
|
|
|||
2
.github/workflows/policy-pr-title.yml
vendored
2
.github/workflows/policy-pr-title.yml
vendored
|
|
@ -14,7 +14,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR title format
|
||||
uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
|
||||
uses: amannn/action-semantic-pull-request@v6.1.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
|
|
|||
51
.github/workflows/release-auto-fin.yml
vendored
51
.github/workflows/release-auto-fin.yml
vendored
|
|
@ -1,7 +1,7 @@
|
|||
# 发布操作手册:
|
||||
# 1. 先将 plugins/auto-fin/pyproject.toml 中的 project.version 更新为待发布版本并合入目标分支。
|
||||
# 2. 确认插件依赖的 reme-ai 版本已经发布到 PyPI;本工作流会在构建阶段验证该依赖可下载。
|
||||
# 3. 确认 PyPI Trusted Publisher 已绑定本仓库、此工作流和 pypi environment,且 PyPI 上不存在相同版本。
|
||||
# 3. 确认仓库 Actions Secret 已配置 PYPI_API_TOKEN,且 PyPI 上不存在相同版本。
|
||||
# 4. 在 GitHub 仓库的 Actions 页面选择“Release / Auto Fin plugin”,点击“Run workflow”。
|
||||
# 5. 输入与 project.version 完全一致的版本号(例如 0.1.0)后运行;版本也可以带 v 前缀。
|
||||
#
|
||||
|
|
@ -34,12 +34,10 @@ jobs:
|
|||
RELEASE_VERSION: ${{ inputs.version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
|
|
@ -73,10 +71,8 @@ jobs:
|
|||
if len(requirements) != 1:
|
||||
raise SystemExit(f"Expected one reme-ai dependency, found {requirements!r}")
|
||||
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}")
|
||||
if reme_requirement.name != "reme-ai" or set(reme_requirement.extras) != {"core"}:
|
||||
raise SystemExit(f"Expected a reme-ai[core] dependency, 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}")
|
||||
|
|
@ -86,12 +82,10 @@ jobs:
|
|||
run: python -m pytest plugins/auto-fin -q
|
||||
|
||||
- name: Require the plugin-enabled ReMe release on PyPI
|
||||
env:
|
||||
REME_REQUIREMENT: ${{ steps.package.outputs.reme_requirement }}
|
||||
run: |
|
||||
python -m pip download --no-deps \
|
||||
--dest "${RUNNER_TEMP}/reme-auto-fin-base" \
|
||||
"${REME_REQUIREMENT}"
|
||||
--dest "${RUNNER_TEMP}/reme-auto-fin-core" \
|
||||
"${{ steps.package.outputs.reme_requirement }}"
|
||||
|
||||
- name: Build and check distributions
|
||||
run: |
|
||||
|
|
@ -106,31 +100,20 @@ 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 \
|
||||
"agentscope[model-ollama]==2.0.7" "${AUTO_FIN_WHEEL}"
|
||||
"${RUNNER_TEMP}/reme-auto-fin-smoke/bin/python" -m pip install "${AUTO_FIN_WHEEL}"
|
||||
cd "${RUNNER_TEMP}"
|
||||
"${RUNNER_TEMP}/reme-auto-fin-smoke/bin/python" - <<'PY'
|
||||
from importlib.metadata import distribution
|
||||
|
||||
from reme.plugin_manifest import load_package_manifest
|
||||
|
||||
package = distribution("reme-auto-fin")
|
||||
plugins = {entry.name: entry for entry in package.entry_points if entry.group == "reme.plugins"}
|
||||
assert plugins["auto-fin"].value == "reme_auto_fin"
|
||||
manifest = load_package_manifest("reme_auto_fin", plugin_name="auto-fin")
|
||||
assert set(manifest.backends) == {
|
||||
"auto_fin_data_step",
|
||||
"auto_fin_topic_step",
|
||||
"auto_fin_merge_step",
|
||||
}
|
||||
assert set(manifest.application_defaults["jobs"]) == {
|
||||
"auto_fin",
|
||||
"auto_fin_cron",
|
||||
}
|
||||
configs = {entry.name: entry for entry in package.entry_points if entry.group == "reme.configs"}
|
||||
assert plugins["auto-fin"].load().name == "auto-fin"
|
||||
assert configs["auto-fin"].load().is_file()
|
||||
PY
|
||||
|
||||
- name: Upload distributions
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: reme-auto-fin-${{ inputs.version }}
|
||||
path: dist/auto-fin/
|
||||
|
|
@ -139,19 +122,17 @@ jobs:
|
|||
publish:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment: pypi
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Download distributions
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: reme-auto-fin-${{ inputs.version }}
|
||||
path: dist/auto-fin
|
||||
|
||||
- name: Publish reme-auto-fin
|
||||
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages-dir: dist/auto-fin
|
||||
|
|
|
|||
35
.github/workflows/release-daily-paper.yml
vendored
35
.github/workflows/release-daily-paper.yml
vendored
|
|
@ -1,7 +1,7 @@
|
|||
# Release checklist:
|
||||
# 1. Update project.version in plugins/daily_paper/pyproject.toml and merge it into the target branch.
|
||||
# 2. Publish the required reme-ai version before this plugin; the build verifies that dependency on PyPI.
|
||||
# 3. Configure PyPI Trusted Publishing for this repository/workflow and its pypi environment.
|
||||
# 3. Confirm PYPI_API_TOKEN is configured and the version does not already exist on PyPI.
|
||||
# 4. Run "Release / Daily Paper plugin" from GitHub Actions with the exact project version (a v prefix is accepted).
|
||||
#
|
||||
# Recommended order: reme-ai -> reme-daily-paper -> downstream applications enabling plugins: [daily-paper].
|
||||
|
|
@ -33,12 +33,10 @@ jobs:
|
|||
RELEASE_VERSION: ${{ inputs.version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
|
|
@ -70,10 +68,8 @@ jobs:
|
|||
raise SystemExit(f"Package version is {actual}, but workflow input is {expected}")
|
||||
requirements = [Requirement(value) for value in project["dependencies"]]
|
||||
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 len(reme_requirements) != 1 or set(reme_requirements[0].extras) != {"core"}:
|
||||
raise SystemExit(f"Expected one reme-ai[core] dependency, 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:
|
||||
|
|
@ -85,12 +81,10 @@ jobs:
|
|||
run: python -m pytest plugins/daily_paper -q
|
||||
|
||||
- name: Require the plugin-enabled ReMe release on PyPI
|
||||
env:
|
||||
REME_REQUIREMENT: ${{ steps.package.outputs.reme_requirement }}
|
||||
run: |
|
||||
python -m pip download --no-deps \
|
||||
--dest "${RUNNER_TEMP}/reme-daily-paper-base" \
|
||||
"${REME_REQUIREMENT}"
|
||||
--dest "${RUNNER_TEMP}/reme-daily-paper-core" \
|
||||
"${{ steps.package.outputs.reme_requirement }}"
|
||||
|
||||
- name: Build and check distributions
|
||||
run: |
|
||||
|
|
@ -107,8 +101,7 @@ 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 \
|
||||
"agentscope[model-ollama]==2.0.7" "${DAILY_PAPER_WHEEL}"
|
||||
"${RUNNER_TEMP}/reme-daily-paper-smoke/bin/python" -m pip install "${DAILY_PAPER_WHEEL}"
|
||||
cd "${RUNNER_TEMP}"
|
||||
"${RUNNER_TEMP}/reme-daily-paper-smoke/bin/python" - <<'PY'
|
||||
from importlib.metadata import distribution
|
||||
|
|
@ -130,7 +123,7 @@ jobs:
|
|||
PY
|
||||
|
||||
- name: Upload distributions
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: reme-daily-paper-${{ inputs.version }}
|
||||
path: dist/daily-paper/
|
||||
|
|
@ -139,19 +132,17 @@ jobs:
|
|||
publish:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment: pypi
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Download distributions
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: reme-daily-paper-${{ inputs.version }}
|
||||
path: dist/daily-paper
|
||||
|
||||
- name: Publish reme-daily-paper
|
||||
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages-dir: dist/daily-paper
|
||||
|
|
|
|||
22
.github/workflows/release-python.yml
vendored
22
.github/workflows/release-python.yml
vendored
|
|
@ -1,7 +1,7 @@
|
|||
name: Release / Python packages
|
||||
|
||||
# Configure a PyPI Trusted Publisher for this repository, workflow, and its
|
||||
# pypi environment before running the manual release.
|
||||
# reme-ai[qwenpaw] is verified before publication. Publish the independently
|
||||
# versioned reme-auto-fin and reme-daily-paper requirements first.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
|
@ -10,38 +10,34 @@ on:
|
|||
description: Release version
|
||||
required: true
|
||||
type: string
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: publish-reme-ai
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and verify distributions
|
||||
uses: ./.github/workflows/_build-python-packages.yml
|
||||
with:
|
||||
expected_version: ${{ inputs.version }}
|
||||
expected_version: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.version }}
|
||||
upload_artifacts: true
|
||||
|
||||
publish-reme:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment: pypi
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Download ReMe distributions
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: reme-distributions
|
||||
path: dist/reme
|
||||
|
||||
- name: Publish ReMe
|
||||
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages-dir: dist/reme
|
||||
skip-existing: true
|
||||
|
|
|
|||
41
.github/workflows/release-reme-studio.yml
vendored
41
.github/workflows/release-reme-studio.yml
vendored
|
|
@ -1,6 +1,6 @@
|
|||
# Release checklist:
|
||||
# 1. Update reme_studio/pyproject.toml, package.json, and package-lock.json to the same Studio version.
|
||||
# 2. Configure npm Trusted Publishing and PyPI Trusted Publishing with the pypi environment.
|
||||
# 2. Configure PyPI and npm publishing credentials for this workflow.
|
||||
# 3. Run this workflow manually with the exact Studio version.
|
||||
|
||||
name: Release / ReMe Studio
|
||||
|
|
@ -38,17 +38,15 @@ jobs:
|
|||
NPM_TAG: ${{ inputs.npm_tag }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22.22.3"
|
||||
node-version: "22.13"
|
||||
cache: npm
|
||||
cache-dependency-path: reme_studio/package-lock.json
|
||||
|
||||
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
|
@ -95,20 +93,7 @@ jobs:
|
|||
python -m build reme_studio --outdir dist/studio-python
|
||||
python -m twine check dist/studio-python/*
|
||||
|
||||
- name: Verify Studio distributions and isolated installation
|
||||
run: |
|
||||
STUDIO_WHEEL="$(pwd)/$(ls dist/studio-python/reme_studio-*.whl)"
|
||||
tar -tzf dist/studio-npm/*.tgz | grep '^package/dist-static/index.html$'
|
||||
python -m venv "${RUNNER_TEMP}/reme-studio-package-smoke"
|
||||
"${RUNNER_TEMP}/reme-studio-package-smoke/bin/python" -m pip install "${STUDIO_WHEEL}"
|
||||
cd "${RUNNER_TEMP}"
|
||||
"${RUNNER_TEMP}/reme-studio-package-smoke/bin/python" - <<'PY'
|
||||
from reme_studio import static_dir
|
||||
|
||||
assert (static_dir() / "index.html").is_file()
|
||||
PY
|
||||
|
||||
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: reme-studio-${{ inputs.version }}
|
||||
path: |
|
||||
|
|
@ -119,19 +104,17 @@ jobs:
|
|||
publish-python:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment: pypi
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: reme-studio-${{ inputs.version }}
|
||||
path: dist
|
||||
|
||||
- name: Publish ReMe Studio to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages-dir: dist/studio-python
|
||||
skip-existing: true
|
||||
|
||||
|
|
@ -142,12 +125,12 @@ jobs:
|
|||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: reme-studio-${{ inputs.version }}
|
||||
path: dist
|
||||
|
|
|
|||
14
.github/workflows/release-typescript.yml
vendored
14
.github/workflows/release-typescript.yml
vendored
|
|
@ -47,12 +47,10 @@ jobs:
|
|||
NPM_TAG: ${{ inputs.npm_tag }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22.22.3'
|
||||
|
||||
|
|
@ -103,7 +101,7 @@ jobs:
|
|||
npm pack --pack-destination "${RUNNER_TEMP}/reme-typescript-package"
|
||||
|
||||
- name: Upload npm tarball
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: agentscope-ai-reme-${{ inputs.version }}
|
||||
path: ${{ runner.temp }}/reme-typescript-package/*.tgz
|
||||
|
|
@ -118,13 +116,13 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Set up Node for npm
|
||||
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24'
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Download npm tarball
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: agentscope-ai-reme-${{ inputs.version }}
|
||||
path: dist/typescript
|
||||
|
|
@ -151,7 +149,7 @@ jobs:
|
|||
actions: read
|
||||
contents: read
|
||||
id-token: write
|
||||
uses: openclaw/clawhub/.github/workflows/package-publish.yml@87ca030c30f3cfb78ab15c8e66b5ff1469c8f9c8 # v0.23.3
|
||||
uses: openclaw/clawhub/.github/workflows/package-publish.yml@v0.23.3
|
||||
with:
|
||||
owner: agentscope-ai
|
||||
family: code-plugin
|
||||
|
|
|
|||
8
.github/workflows/security-codeql.yml
vendored
8
.github/workflows/security-codeql.yml
vendored
|
|
@ -30,17 +30,15 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: none
|
||||
|
||||
- name: Perform CodeQL analysis
|
||||
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: /language:${{ matrix.language }}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ reme plugins list --json
|
|||
To compare installed plugins with one application config:
|
||||
|
||||
```bash
|
||||
reme plugins list --config default
|
||||
reme plugins list --config daily_cookbook
|
||||
```
|
||||
|
||||
The optional `ENABLED` column reflects only the `plugins` list resolved from that config. A command-line override used
|
||||
|
|
@ -173,8 +173,11 @@ curl -s http://127.0.0.1:2333/auto_fin \
|
|||
|
||||
When the application uses an MCP service, service-enabled plugin Jobs appear as MCP tools instead.
|
||||
|
||||
Custom application configs must provide the plugin's runtime dependencies, including an `agent_wrapper.default` and
|
||||
the `search` and `read` Jobs used by Auto Fin.
|
||||
To add the plugin to another application config, select it explicitly:
|
||||
|
||||
```bash
|
||||
reme start config=daily_cookbook plugins='["auto-fin"]'
|
||||
```
|
||||
|
||||
## Uninstall a plugin
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ reme plugins list --json
|
|||
对照某个应用配置查看启用状态:
|
||||
|
||||
```bash
|
||||
reme plugins list --config default
|
||||
reme plugins list --config daily_cookbook
|
||||
```
|
||||
|
||||
可选的 `ENABLED` 列只反映该配置解析出的 `plugins` 列表。其他运行中进程使用的 CLI override 不是全局启用状态。
|
||||
|
|
@ -167,7 +167,11 @@ curl -s http://127.0.0.1:2333/auto_fin \
|
|||
|
||||
当应用使用 MCP service 时,允许对外服务的插件 Job 会显示为 MCP tool。
|
||||
|
||||
自定义应用配置需要提供插件的运行依赖,包括 `agent_wrapper.default`,以及 Auto Fin 使用的 `search` 和 `read` Jobs。
|
||||
如果需要将插件叠加到其他应用配置,则显式选择该配置:
|
||||
|
||||
```bash
|
||||
reme start config=daily_cookbook plugins='["auto-fin"]'
|
||||
```
|
||||
|
||||
## 卸载插件
|
||||
|
||||
|
|
|
|||
|
|
@ -243,6 +243,11 @@ for (const product of productDocuments) {
|
|||
await cp(path.join(repoDir, product.source, filename), path.join(outputDir, product.source, filename));
|
||||
}
|
||||
}
|
||||
await mkdir(path.join(outputDir, "reme_studio", "public"), { recursive: true });
|
||||
await cp(
|
||||
path.join(repoDir, "reme_studio", "public", "og.jpg"),
|
||||
path.join(outputDir, "reme_studio", "public", "og.jpg"),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(outputDir, "manifest.json"),
|
||||
`${JSON.stringify({ documents: await buildManifest() }, null, 2)}\n`,
|
||||
|
|
|
|||
|
|
@ -40,13 +40,13 @@ server means one set of background watchers / dream cron across all your Claude
|
|||
LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
```
|
||||
|
||||
3. Start the ReMe HTTP server (one time, leave it running):
|
||||
3. Start the ReMe MCP server (one time, leave it running):
|
||||
|
||||
```bash
|
||||
reme start service.backend=http
|
||||
reme start service.backend=mcp service.transport=streamable-http
|
||||
```
|
||||
|
||||
The same process serves the JSON Job API and MCP at `http://127.0.0.1:2333/mcp`. To use a different port, start with
|
||||
It serves `http://127.0.0.1:2333/mcp`. To use a different port, start with
|
||||
`service.port=<port>` and update the `url` in `.mcp.json` to match.
|
||||
|
||||
## Install the plugin
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ The recall tools come from the `reme` MCP server (surfaced as `mcp__reme__…`):
|
|||
running:
|
||||
|
||||
```
|
||||
reme start service.backend=http
|
||||
reme start service.backend=mcp service.transport=streamable-http
|
||||
```
|
||||
|
||||
If the tools are missing, that server is not running — tell the user the command above instead of
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ through `plugins=["auto-fin"]`.
|
|||
### 1. Install ReMe and Auto Fin
|
||||
|
||||
```bash
|
||||
python -m pip install "reme-ai[core]>=0.4.1.9"
|
||||
python -m pip install "reme-ai[core]>=0.4.1.8"
|
||||
reme plugins install reme-auto-fin
|
||||
```
|
||||
|
||||
|
|
@ -49,17 +49,21 @@ curl -s http://127.0.0.1:2333/auto_fin \
|
|||
-d '{"topics":"黄金,AI,存储芯片"}'
|
||||
```
|
||||
|
||||
The HTTP service also exposes the same Job as the `auto_fin` MCP tool at `/mcp`. The default topics are
|
||||
When enabled on an MCP service, the same Job is exposed as the `auto_fin` MCP tool. The default topics are
|
||||
`黄金,机器人,半导体`; an empty value also uses these defaults.
|
||||
|
||||
To host the application with both JSON and MCP access:
|
||||
To host the same application as an MCP service instead:
|
||||
|
||||
```bash
|
||||
reme start plugins='["auto-fin"]' \
|
||||
service.backend=http
|
||||
service.backend=mcp service.transport=streamable-http
|
||||
```
|
||||
|
||||
Custom application configs must provide `agent_wrapper.default` and the `search` and `read` Jobs used by 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"]'
|
||||
```
|
||||
|
||||
## Pipeline
|
||||
|
||||
|
|
@ -70,7 +74,7 @@ normalize and deduplicate in RuntimeContext
|
|||
↓
|
||||
topic Agent selects real news IDs in bounded batches
|
||||
↓
|
||||
research Agent uses search + read on historical memory
|
||||
research Agent uses memory_search + read on historical memory
|
||||
↓
|
||||
validate historical wikilinks in code
|
||||
↓
|
||||
|
|
@ -85,8 +89,8 @@ records outside the window are discarded.
|
|||
IDs and deduplicates repeated IDs, then preserves the source-news order. If nothing is relevant, the job succeeds as a
|
||||
skip without writing or sending a report.
|
||||
|
||||
`auto_fin_merge_step` receives only selected current news. It exposes `search` and `read`, and keeps current CLS IDs,
|
||||
times, and titles as plain evidence. The prompt limits
|
||||
`auto_fin_merge_step` receives only selected current news. It exposes `memory_search` and `read`, instructs the Agent to
|
||||
search no later than yesterday, and keeps current CLS IDs, times, and titles as plain evidence. The prompt limits
|
||||
wikilinks to historical Markdown actually used by the Agent; the code-level boundary independently keeps only existing,
|
||||
workspace-relative Markdown targets. Missing, absolute, escaping, backslash, and self-referential targets are degraded
|
||||
to their readable aliases.
|
||||
|
|
@ -105,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 plugin cron Job starts with the application and runs daily at 18:00 in the application timezone.
|
||||
The three plugin cron Jobs start with the application and run daily at 09:30, 11:30, and 18:00 in `Asia/Shanghai`.
|
||||
|
||||
## Output
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ distribution:单个 `reme.plugins` entry point 暴露 `plugin.yaml`,其中
|
|||
### 1. 安装 ReMe 和 Auto Fin
|
||||
|
||||
```bash
|
||||
python -m pip install "reme-ai[core]>=0.4.1.9"
|
||||
python -m pip install "reme-ai[core]>=0.4.1.8"
|
||||
reme plugins install reme-auto-fin
|
||||
```
|
||||
|
||||
|
|
@ -44,17 +44,21 @@ curl -s http://127.0.0.1:2333/auto_fin \
|
|||
-d '{"topics":"黄金,AI,存储芯片"}'
|
||||
```
|
||||
|
||||
HTTP service 也会在 `/mcp` 中将同一个 Job 暴露为 `auto_fin` MCP tool。默认 topics 是 `黄金,机器人,半导体`,
|
||||
在 MCP service 中启用插件时,同一个 Job 会暴露为 `auto_fin` MCP tool。默认 topics 是 `黄金,机器人,半导体`,
|
||||
传入空值也会使用默认值。
|
||||
|
||||
如果需要同时通过 JSON 和 MCP 访问同一个应用:
|
||||
如果需要将同一个应用作为 MCP service 启动:
|
||||
|
||||
```bash
|
||||
reme start plugins='["auto-fin"]' \
|
||||
service.backend=http
|
||||
service.backend=mcp service.transport=streamable-http
|
||||
```
|
||||
|
||||
自定义应用配置需要提供 `agent_wrapper.default`,以及 Auto Fin 使用的 `search` 和 `read` Jobs。
|
||||
如果需要将 Auto Fin 叠加到其他应用,则显式选择相应配置,例如:
|
||||
|
||||
```bash
|
||||
reme start config=daily_cookbook plugins='["auto-fin"]'
|
||||
```
|
||||
|
||||
## 流程
|
||||
|
||||
|
|
@ -65,7 +69,7 @@ reme start plugins='["auto-fin"]' \
|
|||
↓
|
||||
Topic Agent 分批选择真实 news_id
|
||||
↓
|
||||
Research Agent 使用 search + read 检索历史记忆
|
||||
Research Agent 使用 memory_search + read 检索历史记忆
|
||||
↓
|
||||
代码校验历史 wikilink
|
||||
↓
|
||||
|
|
@ -78,8 +82,8 @@ daily/YYYY-MM-DD/auto_fin.md
|
|||
`auto_fin_topic_step` 分批接收当前新闻,只返回相关的 `news_id`。代码会忽略未知 ID、去除重复 ID,并保持源新闻顺序。如果没有相关新闻,Job
|
||||
会成功跳过,不写报告也不发送通知。
|
||||
|
||||
`auto_fin_merge_step` 只接收筛选后的当前新闻,并向 Agent 开放 `search` 和 `read`。当前新闻以 CLS ID、时间和标题作为普通证据。
|
||||
Prompt 要求 Agent 只链接实际使用过的历史 Markdown;代码边界则独立保证只保留真实存在、相对
|
||||
`auto_fin_merge_step` 只接收筛选后的当前新闻,并向 Agent 开放 `memory_search` 和 `read`。历史检索截止到昨天;当前新闻以
|
||||
CLS ID、时间和标题作为普通证据。Prompt 要求 Agent 只链接实际使用过的历史 Markdown;代码边界则独立保证只保留真实存在、相对
|
||||
workspace 的 Markdown 目标。不存在、绝对路径、越界、带反斜杠和自引用的目标都会降级为可读 alias。
|
||||
|
||||
同日重跑会参考当天已有报告并覆盖为修订结果。最终写入使用原子替换并刷新当天索引;流程不会写入 JSONL、中间 Markdown 或 Agent
|
||||
|
|
@ -96,7 +100,7 @@ workspace 的 Markdown 目标。不存在、绝对路径、越界、带反斜杠
|
|||
| `request_interval` | `10` | 每次财联社请求尝试后的最小等待秒数,可设为 0 |
|
||||
| `max_retries` | `3` | 每页财联社请求的最大尝试次数,至少为 1 |
|
||||
|
||||
插件的 cron Job 随应用启动,并按应用配置的时区在每天 18:00 运行。
|
||||
插件的三个 cron Job 随应用启动,并按 `Asia/Shanghai` 时区在每天 09:30、11:30 和 18:00 运行。
|
||||
|
||||
## 产物
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
[project]
|
||||
name = "reme-auto-fin"
|
||||
version = "0.1.2"
|
||||
version = "0.1.1"
|
||||
description = "Auto Fin example plugin for ReMe."
|
||||
readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE"]
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"reme-ai>=0.4.1.9",
|
||||
"reme-ai[core]>=0.4.1.8",
|
||||
]
|
||||
|
||||
[project.entry-points."reme.plugins"]
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@ 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:
|
||||
|
|
@ -90,8 +89,6 @@ 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)}")
|
||||
|
|
|
|||
|
|
@ -102,24 +102,17 @@ class AutoFinMergeStep(AutoFinStep):
|
|||
)
|
||||
|
||||
async def execute(self):
|
||||
"""Research the selected news and persist the validated report."""
|
||||
assert self.context is not None
|
||||
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")),
|
||||
historical_end=(run_date - timedelta(days=1)).isoformat(),
|
||||
topics=json.dumps(self._required("auto_fin_topics"), ensure_ascii=False),
|
||||
news=json.dumps(self._required("auto_fin_selected_news"), ensure_ascii=False),
|
||||
current_report=self._current_report(run_date),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
merge_user: |
|
||||
你是主题新闻研究 Agent。当前新闻已经按 topics 做过语义筛选。你可以使用 `search` 搜索历史记忆,
|
||||
你是主题新闻研究 Agent。当前新闻已经按 topics 做过语义筛选。你可以使用 `memory_search` 搜索历史记忆,
|
||||
并使用 `read` 阅读可能相关的完整 Markdown。不得使用外部搜索,不得虚构行情、收益、价格或未提供的数据。
|
||||
|
||||
研究窗口:{window_start} 至 {decision_at}
|
||||
|
|
@ -8,7 +8,7 @@ merge_user: |
|
|||
今天早些时段的报告(如有,请保留仍成立的判断,只修订变化部分):
|
||||
{current_report}
|
||||
|
||||
先围绕 topics 和当前重要事件多次调用 `search` 检索历史记忆。
|
||||
先围绕 topics 和当前重要事件多次调用 `memory_search`,并将 end_date 设为 {historical_end},避免召回今天的旧报告。
|
||||
只对明显相关的结果调用 `read`。说明历史事件与当前事件的相同点、关键差异,以及旧判断是否仍适用。
|
||||
给出值得回顾的新闻、应继续观察的信息,以及哪些条件会强化或推翻判断,但不要给出投资建议。
|
||||
|
||||
|
|
|
|||
|
|
@ -42,9 +42,19 @@ application_defaults:
|
|||
- backend: auto_fin_data_step
|
||||
- backend: auto_fin_topic_step
|
||||
- backend: auto_fin_merge_step
|
||||
job_tools: [search, read]
|
||||
job_tools: [memory_search, read]
|
||||
|
||||
auto_fin_cron:
|
||||
auto_fin_0930_cron:
|
||||
backend: cron
|
||||
cron: "30 9 * * *"
|
||||
steps: *auto_fin_steps
|
||||
|
||||
auto_fin_1130_cron:
|
||||
backend: cron
|
||||
cron: "30 11 * * *"
|
||||
steps: *auto_fin_steps
|
||||
|
||||
auto_fin_1800_cron:
|
||||
backend: cron
|
||||
cron: "0 18 * * *"
|
||||
steps: *auto_fin_steps
|
||||
|
|
|
|||
|
|
@ -195,23 +195,14 @@ async def test_merge_writes_only_final_report_and_validates_historical_links(tmp
|
|||
response = await AutoFinMergeStep(
|
||||
app_context=app_context,
|
||||
agent_wrapper=agent,
|
||||
job_tools=["search", "read"],
|
||||
job_tools=["memory_search", "read"],
|
||||
)(context)
|
||||
|
||||
prompt, kwargs = agent.calls[0]
|
||||
assert "end_date" not in prompt
|
||||
assert "调用 `search`" in prompt
|
||||
assert "end_date 设为 2026-08-09" in prompt
|
||||
assert "调用 `memory_search`" in prompt
|
||||
assert "调用 `read`" in prompt
|
||||
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",
|
||||
},
|
||||
}
|
||||
assert kwargs == {"output_schema": AutoFinReportOutput, "job_tools": ["memory_search", "read"]}
|
||||
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
|
||||
|
|
@ -263,17 +254,14 @@ def test_plugin_config_has_default_topics_and_no_intermediate_index_step():
|
|||
"auto_fin_topic_step",
|
||||
"auto_fin_merge_step",
|
||||
]
|
||||
assert job["steps"][2]["job_tools"] == ["search", "read"]
|
||||
assert jobs["auto_fin_cron"]["cron"] == "0 18 * * *"
|
||||
assert jobs["auto_fin_cron"]["steps"] == job["steps"]
|
||||
assert (
|
||||
not {
|
||||
"auto_fin_0930_cron",
|
||||
"auto_fin_1130_cron",
|
||||
"auto_fin_1800_cron",
|
||||
}
|
||||
& jobs.keys()
|
||||
)
|
||||
assert job["steps"][2]["job_tools"] == ["memory_search", "read"]
|
||||
for name, schedule in {
|
||||
"auto_fin_0930_cron": "30 9 * * *",
|
||||
"auto_fin_1130_cron": "30 11 * * *",
|
||||
"auto_fin_1800_cron": "0 18 * * *",
|
||||
}.items():
|
||||
assert jobs[name]["cron"] == schedule
|
||||
assert jobs[name]["steps"] == job["steps"]
|
||||
|
||||
|
||||
def test_agent_schemas_are_small_and_required():
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ their Job configuration under `application_defaults`. Enable the installed plugi
|
|||
### 1. Install ReMe and Daily Paper
|
||||
|
||||
```bash
|
||||
python -m pip install "reme-ai[core]>=0.4.1.9"
|
||||
python -m pip install "reme-ai[core]>=0.4.1.8"
|
||||
reme plugins install reme-daily-paper
|
||||
```
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ rank with RRF and let an Agent select three papers
|
|||
↓
|
||||
download and parse arXiv PDFs, then write three Chinese analyses
|
||||
↓
|
||||
use search + read to connect prior memory and generate a brief
|
||||
use memory_search + read to connect prior memory and generate a brief
|
||||
↓
|
||||
refresh the daily index and optionally send the brief to DingTalk
|
||||
```
|
||||
|
|
@ -80,7 +80,7 @@ the configured page, character, and file-size limits. It writes the three Chines
|
|||
PDFs and files without a text layer fail explicitly.
|
||||
|
||||
`daily_paper_digest_step` treats those three analyses as the factual source and receives only the read-only
|
||||
`search` and `read` tools for linking earlier memory. Code validates historical wikilinks, appends links to all
|
||||
`memory_search` and `read` tools for linking earlier memory. Code validates historical wikilinks, appends links to all
|
||||
three source notes, and rebuilds the daily index. The optional `dingtalk_markdown_send_step` sends the final brief when
|
||||
conversation IDs are configured and otherwise skips without side effects.
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Step backend,并在 `application_defaults` 下提供 Job 配置;通过 `plug
|
|||
### 1. 安装 ReMe 和每日论文插件
|
||||
|
||||
```bash
|
||||
python -m pip install "reme-ai[core]>=0.4.1.9"
|
||||
python -m pip install "reme-ai[core]>=0.4.1.8"
|
||||
reme plugins install reme-daily-paper
|
||||
```
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ RRF 排序后由 Agent 精选三篇
|
|||
↓
|
||||
下载并解析 arXiv PDF,生成三篇中文解读
|
||||
↓
|
||||
使用 search + read 关联历史记忆并生成简报
|
||||
使用 memory_search + read 关联历史记忆并生成简报
|
||||
↓
|
||||
写入当日索引,并按需发送到钉钉
|
||||
```
|
||||
|
|
@ -72,7 +72,7 @@ RRF 排序后由 Agent 精选三篇
|
|||
`daily_paper_analyze_step` 下载 PDF 到 `resource/papers/`,复用已有的有效文件,并在页数、字符数和文件大小限制内提取
|
||||
文本。三篇中文解读按精选顺序写入当天目录;扫描版或没有文本层的 PDF 会明确失败。
|
||||
|
||||
`daily_paper_digest_step` 以本次生成的三篇解读为事实来源,只开放只读的 `search` 和 `read` 来关联较早记忆。
|
||||
`daily_paper_digest_step` 以本次生成的三篇解读为事实来源,只开放只读的 `memory_search` 和 `read` 来关联较早记忆。
|
||||
代码会校验历史 wikilink、追加三篇源笔记链接,并重建当日索引。可选的 `dingtalk_markdown_send_step` 在配置群会话后
|
||||
发送最终简报;未配置时无副作用跳过。
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "reme-daily-paper"
|
||||
version = "0.1.2"
|
||||
version = "0.1.1"
|
||||
description = "Daily Paper research and reading-note plugin for ReMe."
|
||||
readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
|
|
@ -8,7 +8,7 @@ license-files = ["LICENSE"]
|
|||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"pypdf>=5.0.0",
|
||||
"reme-ai>=0.4.1.9",
|
||||
"reme-ai[core]>=0.4.1.8",
|
||||
]
|
||||
|
||||
[project.entry-points."reme.plugins"]
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ class DailyPaperDigestStep(DailyPaperStep):
|
|||
return _WIKILINK_RE.sub(replace, body)
|
||||
|
||||
async def execute(self):
|
||||
"""Generate and persist the final brief from analyzed papers."""
|
||||
assert self.context is not None
|
||||
if self._skip():
|
||||
self.logger.info(f"[{self.name}] skip existing digest")
|
||||
|
|
@ -80,23 +79,16 @@ 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("/")
|
||||
previous_day = (dt.date.fromisoformat(self._run_day()) - dt.timedelta(days=1)).isoformat()
|
||||
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,
|
||||
previous_day=previous_day,
|
||||
),
|
||||
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)
|
||||
|
|
@ -104,7 +96,8 @@ class DailyPaperDigestStep(DailyPaperStep):
|
|||
if not output.desc.strip() or not body:
|
||||
raise ValueError("Agent returned an empty daily paper brief")
|
||||
|
||||
day = run_day.isoformat()
|
||||
day = self._run_day()
|
||||
daily_dir = str(self.config_value("daily_dir")).strip("/")
|
||||
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
|
||||
|
|
@ -117,7 +110,7 @@ class DailyPaperDigestStep(DailyPaperStep):
|
|||
existing=existing_path,
|
||||
)
|
||||
digest_rel = digest_path.relative_to(self.workspace_path).as_posix()
|
||||
body = self._validate_historical_wikilinks(body, run_day, digest_path)
|
||||
body = self._validate_historical_wikilinks(body, dt.date.fromisoformat(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,13 +4,12 @@ digest_user: |
|
|||
内容只能依据输入文档,不得补充文档中没有提供的事实。
|
||||
保留技术准确性,同时解释三篇论文为什么值得关注,以及它们之间有什么联系。
|
||||
|
||||
在写作前,先调用 `search` 检索已有记忆:围绕三篇论文的核心问题、方法、关键词和同义表达组织查询。
|
||||
主题跨度较大时可以多次检索,搜索结果不必局限于 `{daily_dir}/`。只有 `{daily_dir}/` 下日期早于今天、
|
||||
且与本期内容确实相似或互补的 Markdown 文章才可作为正文中的历史链接候选;必要时调用 `read` 核验全文,
|
||||
不要仅凭标题判断。
|
||||
在写作前,先调用 `memory_search` 检索以前的文章:围绕三篇论文的核心问题、方法、关键词和同义表达组织查询,
|
||||
使用 end_date={previous_day}、limit=20。主题跨度较大时可以多次检索。只把 `daily/` 下日期早于今天、
|
||||
且与本期内容确实相似或互补的 Markdown 文章作为候选;必要时调用 `read` 核验全文,不要仅凭标题判断。
|
||||
将确认相关的旧文章以 Wikilink 自然织入正文,并用句子说明关联(延续、对比、补充或方法相似);
|
||||
链接必须采用带 `.md` 的完整 workspace-relative 路径,例如
|
||||
`[[{daily_dir}/2026-07-01/旧文章.md|此前的相关解读]]`。不要输出裸链接、独立关系字段,也不要虚构搜索未命中的路径。
|
||||
`[[daily/2026-07-01/旧文章.md|此前的相关解读]]`。不要输出裸链接、独立关系字段,也不要虚构搜索未命中的路径。
|
||||
旧文章只用于判断关联和建立链接,不得用来补充本期事实。如果没有真正相关的旧文章,不要强行添加;
|
||||
当日三篇详细解读的链接会由系统统一附在文末。
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ application_defaults:
|
|||
- backend: daily_paper_select_step
|
||||
- backend: daily_paper_analyze_step
|
||||
- backend: daily_paper_digest_step
|
||||
job_tools: [search, read]
|
||||
job_tools: [memory_search, read]
|
||||
- backend: dingtalk_markdown_send_step
|
||||
input_mapping:
|
||||
daily_paper_digest_path: markdown_path
|
||||
|
|
|
|||
|
|
@ -627,27 +627,6 @@ 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/`" 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()
|
||||
|
|
@ -867,7 +846,7 @@ async def test_pipeline_filters_strict_yesterday_and_writes_outputs(
|
|||
await DailyPaperDigestStep(
|
||||
app_context=app_context,
|
||||
agent_wrapper=cc_wrapper,
|
||||
job_tools=["search", "read"],
|
||||
job_tools=["memory_search", "read"],
|
||||
)(context)
|
||||
|
||||
assert _FakeHfClient.requested_daily == ["2026-07-20"]
|
||||
|
|
@ -907,13 +886,7 @@ async def test_pipeline_filters_strict_yesterday_and_writes_outputs(
|
|||
assert all(call["kwargs"] == {"output_schema": DailyPaperMarkdownOutput} for call in cc_wrapper.calls[1:-1])
|
||||
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",
|
||||
},
|
||||
"job_tools": ["memory_search", "read"],
|
||||
}
|
||||
assert [call["kwargs"]["output_schema"] for call in cc_wrapper.calls] == [
|
||||
PaperPickList,
|
||||
|
|
@ -933,10 +906,8 @@ async def test_pipeline_filters_strict_yesterday_and_writes_outputs(
|
|||
assert "调用 Read" not in digest_prompt
|
||||
assert "daily/2026-07-21" not in digest_prompt
|
||||
assert "长期记忆" not in digest_prompt
|
||||
assert "先调用 `search` 检索已有记忆" in digest_prompt
|
||||
assert "搜索结果不必局限于 `daily/`" in digest_prompt
|
||||
assert "end_date" not in digest_prompt
|
||||
assert "limit=" not in digest_prompt
|
||||
assert "先调用 `memory_search` 检索以前的文章" in digest_prompt
|
||||
assert "end_date=2026-07-20" in digest_prompt
|
||||
assert "Wikilink" in digest_prompt
|
||||
|
||||
rerun = RuntimeContext(date="2026-07-21")
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ dependencies = [
|
|||
|
||||
[project.optional-dependencies]
|
||||
as = [
|
||||
"agentscope[model-ollama]==2.0.7",
|
||||
"agentscope[model-ollama]==2.0.6",
|
||||
]
|
||||
web = [
|
||||
"reme_studio",
|
||||
|
|
@ -62,6 +62,11 @@ core = [
|
|||
"polars>=1.43.0",
|
||||
"reme_studio",
|
||||
]
|
||||
qwenpaw = [
|
||||
"reme-ai[core]",
|
||||
"reme-auto-fin>=0.1.1",
|
||||
"reme-daily-paper>=0.1.1",
|
||||
]
|
||||
dev = [
|
||||
"packaging>=24.2",
|
||||
"pre-commit>=4.6.1",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""ReMe CLI package."""
|
||||
|
||||
__version__ = "0.4.1.9"
|
||||
__version__ = "0.4.1.8"
|
||||
|
||||
from . import config
|
||||
from . import constants
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ from ..component_registry import R
|
|||
from ..as_embedding import BaseAsEmbedding
|
||||
|
||||
Miss = tuple[int, str, str] # (result_index, text, cache_key)
|
||||
_MAX_VECTOR_SPACE_ATTEMPTS = 3
|
||||
|
||||
|
||||
@R.register("local")
|
||||
|
|
@ -104,25 +103,12 @@ class LocalEmbeddingStore(BaseEmbeddingStore):
|
|||
# -- Public API --
|
||||
|
||||
async def get_embeddings(self, input_text: list[str], **kwargs) -> list[np.ndarray | None]:
|
||||
await self._sync_cache_space()
|
||||
texts = [self._truncate(t) for t in input_text]
|
||||
for attempt in range(1, _MAX_VECTOR_SPACE_ATTEMPTS + 1):
|
||||
await self._sync_cache_space()
|
||||
vector_space_id = self._cache_space
|
||||
results, misses = self._partition_by_cache(texts)
|
||||
stable = not misses or await self._fill_misses(misses, results, vector_space_id, **kwargs)
|
||||
if stable and vector_space_id == self.vector_space_id == self._cache_space:
|
||||
return results
|
||||
if attempt == _MAX_VECTOR_SPACE_ATTEMPTS:
|
||||
self.logger.warning(
|
||||
f"Embedding vector space kept changing while computing a request; "
|
||||
f"discarding all result(s) after {attempt} attempts",
|
||||
)
|
||||
else:
|
||||
self.logger.info(
|
||||
f"Embedding vector space changed while computing a request; "
|
||||
f"discarding all result(s) and retrying ({attempt}/{_MAX_VECTOR_SPACE_ATTEMPTS})",
|
||||
)
|
||||
return [None] * len(texts)
|
||||
results, misses = self._partition_by_cache(texts)
|
||||
if misses:
|
||||
await self._fill_misses(misses, results, **kwargs)
|
||||
return results
|
||||
|
||||
# -- Batching --
|
||||
|
||||
|
|
@ -138,26 +124,15 @@ class LocalEmbeddingStore(BaseEmbeddingStore):
|
|||
misses.append((idx, text, key))
|
||||
return results, misses
|
||||
|
||||
async def _fill_misses(
|
||||
self,
|
||||
misses: list[Miss],
|
||||
results: list[np.ndarray | None],
|
||||
vector_space_id: str,
|
||||
**kwargs,
|
||||
) -> bool:
|
||||
"""Fill every miss only while the request remains in one vector space."""
|
||||
async def _fill_misses(self, misses: list[Miss], results: list[np.ndarray | None], **kwargs) -> None:
|
||||
vector_space_id = self._cache_space
|
||||
size = self.max_batch_size
|
||||
for start in range(0, len(misses), size):
|
||||
if vector_space_id != self.vector_space_id or vector_space_id != self._cache_space:
|
||||
return False
|
||||
batch = misses[start : start + size]
|
||||
computed = await self._compute_batch(batch, **kwargs)
|
||||
if vector_space_id != self.vector_space_id or vector_space_id != self._cache_space:
|
||||
return False
|
||||
for idx, key, emb in computed:
|
||||
for idx, key, emb in await self._compute_batch(batch, **kwargs):
|
||||
results[idx] = emb
|
||||
self._cache_put(key, emb)
|
||||
return True
|
||||
if vector_space_id == self.vector_space_id == self._cache_space:
|
||||
self._cache_put(key, emb)
|
||||
|
||||
async def _compute_batch(self, batch: list[Miss], **kwargs) -> list[tuple[int, str, np.ndarray]]:
|
||||
texts = [text for _, text, _ in batch]
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
"""HTTP service: expose jobs through JSON/SSE endpoints and MCP tools."""
|
||||
"""HTTP service: exposes jobs as FastAPI endpoints (JSON, or SSE for stream jobs)."""
|
||||
|
||||
import asyncio
|
||||
import warnings
|
||||
from collections.abc import AsyncGenerator
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from starlette.routing import Route
|
||||
|
||||
from .base_service import BaseService
|
||||
from ..component_registry import R
|
||||
|
|
@ -19,7 +18,6 @@ from ..job import BaseJob, StreamJob
|
|||
from ...constants import REME_DEFAULT_HOST, REME_DEFAULT_PORT
|
||||
from ...schema import Request, Response
|
||||
from ...utils import execute_stream_task, resolve_web_static_dir
|
||||
from .mcp_tools import add_mcp_job
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...application import Application
|
||||
|
|
@ -35,7 +33,7 @@ _WEBSOCKET_DEPRECATION_PATTERNS = (
|
|||
|
||||
@R.register("http")
|
||||
class HttpService(BaseService):
|
||||
"""Expose jobs through JSON/SSE endpoints and streamable HTTP MCP."""
|
||||
"""Map non-stream jobs to JSON POST endpoints and StreamJobs to SSE endpoints."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -43,11 +41,6 @@ class HttpService(BaseService):
|
|||
port: int = REME_DEFAULT_PORT,
|
||||
web_enabled: bool = True,
|
||||
web_static_dir: str | None = None,
|
||||
mcp_enabled: bool = True,
|
||||
mcp_path: str = "/mcp",
|
||||
mcp_stateless_http: bool = False,
|
||||
injected_job_kwargs: dict[str, Any] | None = None,
|
||||
tool_error_on_failure: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
|
|
@ -55,34 +48,14 @@ class HttpService(BaseService):
|
|||
self.port: int = port
|
||||
self.web_enabled = web_enabled
|
||||
self.web_static_dir = web_static_dir
|
||||
self.mcp_enabled = mcp_enabled
|
||||
self.mcp_path = self._validate_mcp_path(mcp_path)
|
||||
self.mcp_stateless_http = mcp_stateless_http
|
||||
self.injected_job_kwargs = dict(injected_job_kwargs or {})
|
||||
self.tool_error_on_failure = tool_error_on_failure
|
||||
self.mcp_server = None
|
||||
self.mcp_app = None
|
||||
|
||||
# ----- BaseService contract ------------------------------------------
|
||||
|
||||
def build_service(self, app: "Application") -> None:
|
||||
"""Create one FastAPI app containing JSON/SSE and optional MCP routes."""
|
||||
lifespan = self._lifespan(app, self.host, self.port)
|
||||
if self.mcp_enabled:
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.utilities.lifespan import combine_lifespans
|
||||
|
||||
self.mcp_server = FastMCP(name=app.config.app_name)
|
||||
self.mcp_app = self.mcp_server.http_app(
|
||||
path=self.mcp_path,
|
||||
transport="streamable-http",
|
||||
stateless_http=self.mcp_stateless_http,
|
||||
)
|
||||
lifespan = combine_lifespans(lifespan, self.mcp_app.lifespan)
|
||||
|
||||
"""Create the FastAPI app with permissive CORS and an app-managed lifespan."""
|
||||
self.service = FastAPI(
|
||||
title=app.config.app_name,
|
||||
lifespan=lifespan,
|
||||
lifespan=self._lifespan(app, self.host, self.port),
|
||||
)
|
||||
cors_origins = ["*"]
|
||||
self.service.add_middleware(
|
||||
|
|
@ -92,60 +65,19 @@ class HttpService(BaseService):
|
|||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
if self.mcp_app is not None:
|
||||
# Forward the exact path to the complete FastMCP ASGI app. Copying
|
||||
# only its routes would bypass its middleware and application state;
|
||||
# mounting it would make the trailing-slash path canonical instead.
|
||||
self.service.router.routes.append(
|
||||
Route(
|
||||
self.mcp_path,
|
||||
endpoint=self.mcp_app,
|
||||
include_in_schema=False,
|
||||
),
|
||||
)
|
||||
|
||||
def add_jobs(self, app: "Application") -> None:
|
||||
"""Validate reserved routes before the shared tolerant registration loop."""
|
||||
if self.mcp_enabled:
|
||||
conflicts = sorted(
|
||||
job.name
|
||||
for name, job in app.context.jobs.items()
|
||||
if job.enable_serve and (self.jobs is None or name in self.jobs) and f"/{job.name}" == self.mcp_path
|
||||
)
|
||||
if conflicts:
|
||||
names = ", ".join(conflicts)
|
||||
raise ValueError(
|
||||
f"Job name conflicts with the MCP endpoint {self.mcp_path!r}: {names}",
|
||||
)
|
||||
super().add_jobs(app)
|
||||
|
||||
def add_job(self, job: BaseJob) -> bool:
|
||||
"""Register HTTP routes for every job and MCP tools for non-stream jobs."""
|
||||
if self.mcp_enabled and f"/{job.name}" == self.mcp_path:
|
||||
raise ValueError(
|
||||
f"Job name '{job.name}' conflicts with the MCP endpoint {self.mcp_path!r}",
|
||||
)
|
||||
"""Dispatch to streaming or non-streaming registration based on job type."""
|
||||
if isinstance(job, StreamJob):
|
||||
self._add_stream_job(job)
|
||||
else:
|
||||
self._add_json_job(job)
|
||||
if self.mcp_server is not None:
|
||||
add_mcp_job(
|
||||
self.mcp_server,
|
||||
job,
|
||||
injected_job_kwargs=self.injected_job_kwargs,
|
||||
tool_error_on_failure=self.tool_error_on_failure,
|
||||
)
|
||||
return True
|
||||
|
||||
def start_service(self, app: "Application") -> None:
|
||||
"""Run uvicorn, suppressing unrelated websocket deprecation noise."""
|
||||
for pattern in _WEBSOCKET_DEPRECATION_PATTERNS:
|
||||
warnings.filterwarnings(
|
||||
"ignore",
|
||||
category=DeprecationWarning,
|
||||
message=pattern,
|
||||
)
|
||||
warnings.filterwarnings("ignore", category=DeprecationWarning, message=pattern)
|
||||
uvicorn.run(self.service, host=self.host, port=self.port, **self.kwargs)
|
||||
|
||||
def finalize_service(self, app: "Application") -> None:
|
||||
|
|
@ -196,23 +128,6 @@ class HttpService(BaseService):
|
|||
|
||||
# ----- Endpoint factories --------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _validate_mcp_path(path: str) -> str:
|
||||
"""Return a canonical, non-reserved absolute path for the MCP endpoint."""
|
||||
if not path.startswith("/") or path == "/" or path.endswith("/"):
|
||||
raise ValueError(
|
||||
"mcp_path must start with '/', must not be '/', and must not end with '/'",
|
||||
)
|
||||
if "//" in path or any(segment in {".", ".."} for segment in path.split("/")):
|
||||
raise ValueError("mcp_path must use non-empty literal path segments")
|
||||
if any(char in path for char in "{}?#%\\") or any(
|
||||
char.isspace() or ord(char) < 32 or ord(char) == 127 for char in path
|
||||
):
|
||||
raise ValueError("mcp_path must be a literal URL path without route, query, or fragment syntax")
|
||||
if path in {"/assets", "/docs", "/redoc", "/openapi.json"}:
|
||||
raise ValueError(f"mcp_path conflicts with reserved HTTP path {path!r}")
|
||||
return path
|
||||
|
||||
def _add_json_job(self, job: BaseJob) -> None:
|
||||
"""Register a job as POST /{job.name} returning a JSON Response."""
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@ from typing import TYPE_CHECKING, Any
|
|||
|
||||
from .base_service import BaseService
|
||||
from ..component_registry import R
|
||||
from ..job import BaseJob
|
||||
from ..job import BaseJob, StreamJob
|
||||
from ...constants import REME_DEFAULT_HOST, REME_DEFAULT_PORT
|
||||
from .mcp_tools import add_mcp_job
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastmcp.server.server import Transport
|
||||
|
|
@ -46,12 +45,41 @@ class MCPService(BaseService):
|
|||
|
||||
def add_job(self, job: BaseJob) -> bool:
|
||||
"""Register a non-stream job as an MCP tool; StreamJobs are unsupported."""
|
||||
return add_mcp_job(
|
||||
self.service,
|
||||
job,
|
||||
injected_job_kwargs=self.injected_job_kwargs,
|
||||
tool_error_on_failure=self.tool_error_on_failure,
|
||||
from fastmcp.exceptions import ToolError
|
||||
from fastmcp.tools import FunctionTool
|
||||
|
||||
if isinstance(job, StreamJob):
|
||||
return False
|
||||
|
||||
async def execute_tool(**kwargs):
|
||||
conflicts = sorted(self.injected_job_kwargs.keys() & kwargs.keys())
|
||||
if conflicts:
|
||||
names = ", ".join(conflicts)
|
||||
raise ToolError(f"{names} injected by the MCP server and cannot be provided by the caller")
|
||||
kwargs.update(self.injected_job_kwargs)
|
||||
response = await job(**kwargs)
|
||||
if self.tool_error_on_failure and not response.success:
|
||||
raise ToolError(str(response.answer))
|
||||
return response.answer
|
||||
|
||||
parameters = dict(job.parameters or {})
|
||||
injected_names = self.injected_job_kwargs.keys()
|
||||
if "properties" in parameters:
|
||||
parameters["properties"] = {
|
||||
name: schema for name, schema in parameters["properties"].items() if name not in injected_names
|
||||
}
|
||||
if "required" in parameters:
|
||||
parameters["required"] = [name for name in parameters["required"] if name not in injected_names]
|
||||
|
||||
self.service.add_tool(
|
||||
FunctionTool(
|
||||
name=job.name,
|
||||
description=job.description,
|
||||
fn=execute_tool,
|
||||
parameters=parameters,
|
||||
),
|
||||
)
|
||||
return True
|
||||
|
||||
def start_service(self, app: "Application") -> None:
|
||||
"""Run the MCP server; bind host/port only for network transports."""
|
||||
|
|
@ -59,8 +87,4 @@ class MCPService(BaseService):
|
|||
if self.transport != "stdio":
|
||||
transport_kwargs["host"] = self.host
|
||||
transport_kwargs["port"] = self.port
|
||||
self.service.run(
|
||||
transport=self.transport,
|
||||
show_banner=False,
|
||||
**transport_kwargs,
|
||||
)
|
||||
self.service.run(transport=self.transport, show_banner=False, **transport_kwargs)
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
"""Shared MCP tool registration for services that expose ReMe jobs."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ..job import BaseJob, StreamJob
|
||||
|
||||
|
||||
def add_mcp_job(
|
||||
server: Any,
|
||||
job: BaseJob,
|
||||
*,
|
||||
injected_job_kwargs: dict[str, Any],
|
||||
tool_error_on_failure: bool,
|
||||
) -> bool:
|
||||
"""Register a non-stream job as an MCP tool on ``server``."""
|
||||
from fastmcp.exceptions import ToolError
|
||||
from fastmcp.tools import FunctionTool
|
||||
|
||||
if isinstance(job, StreamJob):
|
||||
return False
|
||||
|
||||
async def execute_tool(**kwargs):
|
||||
conflicts = sorted(injected_job_kwargs.keys() & kwargs.keys())
|
||||
if conflicts:
|
||||
names = ", ".join(conflicts)
|
||||
raise ToolError(
|
||||
f"{names} injected by the MCP server and cannot be provided by the caller",
|
||||
)
|
||||
kwargs.update(injected_job_kwargs)
|
||||
response = await job(**kwargs)
|
||||
if tool_error_on_failure and not response.success:
|
||||
raise ToolError(str(response.answer))
|
||||
return response.answer
|
||||
|
||||
parameters = dict(job.parameters or {})
|
||||
injected_names = injected_job_kwargs.keys()
|
||||
if "properties" in parameters:
|
||||
parameters["properties"] = {
|
||||
name: schema for name, schema in parameters["properties"].items() if name not in injected_names
|
||||
}
|
||||
if "required" in parameters:
|
||||
parameters["required"] = [name for name in parameters["required"] if name not in injected_names]
|
||||
|
||||
server.add_tool(
|
||||
FunctionTool(
|
||||
name=job.name,
|
||||
description=job.description,
|
||||
fn=execute_tool,
|
||||
parameters=parameters,
|
||||
),
|
||||
)
|
||||
return True
|
||||
416
reme/config/daily_cookbook.yaml
Normal file
416
reme/config/daily_cookbook.yaml
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
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
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
service:
|
||||
backend: http
|
||||
web_enabled: true
|
||||
mcp_enabled: true
|
||||
mcp_path: /mcp
|
||||
|
||||
jobs:
|
||||
index_update_loop:
|
||||
|
|
|
|||
|
|
@ -79,13 +79,7 @@ def print_logo(app_config: "ApplicationConfig", runtime_service: "BaseService |
|
|||
host = getattr(runtime_service, "host", extra.get("host", REME_DEFAULT_HOST))
|
||||
port = getattr(runtime_service, "port", extra.get("port", REME_DEFAULT_PORT))
|
||||
info_table.add_row("🔗", "URL:", f"http://{host}:{port}")
|
||||
mcp_enabled = getattr(runtime_service, "mcp_enabled", extra.get("mcp_enabled", True))
|
||||
if mcp_enabled:
|
||||
mcp_path = getattr(runtime_service, "mcp_path", extra.get("mcp_path", "/mcp"))
|
||||
info_table.add_row("🚌", "MCP:", f"http://{host}:{port}{mcp_path}")
|
||||
info_table.add_row("📚", "FastAPI:", Text(get_version("fastapi"), style="dim"))
|
||||
if mcp_enabled:
|
||||
info_table.add_row("📚", "FastMCP:", Text(get_version("fastmcp"), style="dim"))
|
||||
case "mcp":
|
||||
transport = getattr(runtime_service, "transport", extra.get("transport", "sse"))
|
||||
info_table.add_row("🚌", "Transport:", transport)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ ReMe Studio is the local web workspace for ReMe. It lets you browse and edit use
|
|||
links, and chat with the ReMe Agent without moving durable memory into a separate application database. Search indexes,
|
||||
graphs, and other derived metadata remain rebuildable from the source files.
|
||||
|
||||

|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
ReMe Studio 是 ReMe 的本地 Web 工作区。你可以在这里浏览和编辑自己拥有的工作区文件、探索记忆之间的联系,并与 ReMe Agent
|
||||
对话,而无需将持久记忆迁移到独立的应用数据库中。搜索索引、图谱和其他派生元数据均可根据源文件重建。
|
||||
|
||||

|
||||

|
||||
|
||||
## 安装
|
||||
|
||||
|
|
|
|||
|
|
@ -9,9 +9,6 @@ const geistMono = Geist_Mono({
|
|||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const remeStudioOgImage =
|
||||
"https://github.com/user-attachments/assets/7d0db0d4-69c5-49ef-b1ca-ef8c6cab1138";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const requestHeaders = await headers();
|
||||
const host =
|
||||
|
|
@ -34,9 +31,9 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||
description: "本地优先的 Agent 记忆工作区",
|
||||
images: [
|
||||
{
|
||||
url: remeStudioOgImage,
|
||||
width: 2400,
|
||||
height: 1252,
|
||||
url: "/og.jpg",
|
||||
width: 1200,
|
||||
height: 626,
|
||||
alt: "ReMe Studio memory workspace",
|
||||
},
|
||||
],
|
||||
|
|
@ -45,7 +42,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||
card: "summary_large_image",
|
||||
title: "ReMe Studio",
|
||||
description: "本地优先的 Agent 记忆工作区",
|
||||
images: [remeStudioOgImage],
|
||||
images: ["/og.jpg"],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
BIN
reme_studio/public/og.jpg
Normal file
BIN
reme_studio/public/og.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
|
|
@ -19,8 +19,6 @@ def test_load_builtin_config_by_filename_with_suffix():
|
|||
cfg = _load_config("default.yaml")
|
||||
|
||||
assert cfg["service"]["backend"] == "http"
|
||||
assert cfg["service"]["mcp_enabled"] is True
|
||||
assert cfg["service"]["mcp_path"] == "/mcp"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider_count", [1, 2])
|
||||
|
|
@ -123,6 +121,14 @@ 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,8 +9,10 @@ from unittest.mock import MagicMock
|
|||
|
||||
import pytest
|
||||
|
||||
from reme.components import ApplicationContext
|
||||
from reme.components import ApplicationContext, R
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -201,6 +203,54 @@ 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()
|
||||
|
|
|
|||
|
|
@ -1,16 +1,13 @@
|
|||
"""HTTP service coverage for MCP and the optional bundled web workspace."""
|
||||
"""HTTP service coverage for the optional bundled web workspace."""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import ModuleType, SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
from reme.components.service.http_service import HttpService
|
||||
from reme.components.job import BaseJob, StreamJob
|
||||
from reme.utils import REME_WEB_STATIC_DIR, resolve_web_static_dir
|
||||
|
||||
|
||||
|
|
@ -33,10 +30,7 @@ def _static_build(tmp_path: Path) -> Path:
|
|||
static_dir = tmp_path / "web"
|
||||
assets_dir = static_dir / "assets"
|
||||
assets_dir.mkdir(parents=True)
|
||||
(static_dir / "index.html").write_text(
|
||||
"<main>ReMe workspace</main>",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(static_dir / "index.html").write_text("<main>ReMe workspace</main>", encoding="utf-8")
|
||||
(static_dir / "favicon.svg").write_text("<svg></svg>", encoding="utf-8")
|
||||
(assets_dir / "app.js").write_text("console.log('reme')", encoding="utf-8")
|
||||
return static_dir
|
||||
|
|
@ -72,10 +66,7 @@ def test_http_service_serves_workspace_without_shadowing_jobs(tmp_path: Path) ->
|
|||
def test_http_service_can_disable_workspace(tmp_path: Path) -> None:
|
||||
"""Leave the root route unregistered when workspace serving is disabled."""
|
||||
app = _FakeApplication()
|
||||
service = HttpService(
|
||||
web_enabled=False,
|
||||
web_static_dir=str(_static_build(tmp_path)),
|
||||
)
|
||||
service = HttpService(web_enabled=False, web_static_dir=str(_static_build(tmp_path)))
|
||||
service.build_service(app) # type: ignore[arg-type]
|
||||
service.finalize_service(app) # type: ignore[arg-type]
|
||||
|
||||
|
|
@ -83,148 +74,7 @@ def test_http_service_can_disable_workspace(tmp_path: Path) -> None:
|
|||
assert client.get("/").status_code == 404
|
||||
|
||||
|
||||
def test_http_service_exposes_non_stream_jobs_as_mcp_tools() -> None:
|
||||
"""The HTTP backend exposes the same non-stream Job instance through MCP."""
|
||||
|
||||
async def run() -> None:
|
||||
service = HttpService(web_enabled=False)
|
||||
service.build_service(_FakeApplication()) # type: ignore[arg-type]
|
||||
job = BaseJob(name="search", description="Search memories")
|
||||
|
||||
assert service.add_job(job) is True
|
||||
assert service.mcp_server is not None
|
||||
assert await service.mcp_server.get_tool("search") is not None
|
||||
assert any(route.path == "/search" for route in service.service.routes)
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_http_service_skips_stream_jobs_for_mcp() -> None:
|
||||
"""Stream jobs remain available over HTTP SSE without becoming MCP tools."""
|
||||
|
||||
async def run() -> None:
|
||||
service = HttpService(web_enabled=False)
|
||||
service.build_service(_FakeApplication()) # type: ignore[arg-type]
|
||||
|
||||
assert service.add_job(StreamJob(name="stream")) is True
|
||||
assert service.mcp_server is not None
|
||||
assert await service.mcp_server.get_tool("stream") is None
|
||||
assert any(route.path == "/stream" for route in service.service.routes)
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_http_service_uses_exact_mcp_path_and_runs_one_application_lifespan() -> None:
|
||||
"""Serve MCP at /mcp while starting and closing the shared Application once."""
|
||||
|
||||
class CountingApplication(_FakeApplication):
|
||||
"""Record how often the shared Application lifecycle is entered."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.start_count = 0
|
||||
self.close_count = 0
|
||||
|
||||
async def start(self) -> None:
|
||||
self.start_count += 1
|
||||
|
||||
async def close(self) -> None:
|
||||
self.close_count += 1
|
||||
|
||||
app = CountingApplication()
|
||||
service = HttpService(web_enabled=False)
|
||||
service.build_service(app) # type: ignore[arg-type]
|
||||
mcp_server = service.mcp_server
|
||||
|
||||
class VerifyFastMCPAppMiddleware(BaseHTTPMiddleware):
|
||||
"""Prove requests retain FastMCP middleware and application state."""
|
||||
|
||||
async def dispatch(self, request, call_next):
|
||||
"""Verify the child app context and mark its response."""
|
||||
assert request.app.state.fastmcp_server is mcp_server
|
||||
response = await call_next(request)
|
||||
response.headers["X-FastMCP-Middleware"] = "preserved"
|
||||
return response
|
||||
|
||||
service.mcp_app.add_middleware(VerifyFastMCPAppMiddleware)
|
||||
|
||||
with TestClient(service.service, follow_redirects=False) as client:
|
||||
response = client.post(
|
||||
"/mcp",
|
||||
headers={
|
||||
"Accept": "application/json, text/event-stream",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"protocolVersion": "2025-06-18",
|
||||
"capabilities": {},
|
||||
"clientInfo": {"name": "test", "version": "1"},
|
||||
},
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert '"serverInfo"' in response.text
|
||||
assert response.headers["X-FastMCP-Middleware"] == "preserved"
|
||||
assert client.post("/mcp/mcp", json={}).status_code == 404
|
||||
|
||||
assert app.start_count == 1
|
||||
assert app.close_count == 1
|
||||
|
||||
|
||||
def test_http_service_can_disable_mcp() -> None:
|
||||
"""Allow deployments to retain the legacy HTTP-only surface explicitly."""
|
||||
service = HttpService(web_enabled=False, mcp_enabled=False)
|
||||
service.build_service(_FakeApplication()) # type: ignore[arg-type]
|
||||
|
||||
with TestClient(service.service) as client:
|
||||
assert client.post("/mcp", json={}).status_code == 404
|
||||
|
||||
|
||||
def test_http_service_rejects_invalid_or_conflicting_mcp_paths() -> None:
|
||||
"""Reject paths that shadow built-ins and jobs that shadow the MCP endpoint."""
|
||||
for path in (
|
||||
"mcp",
|
||||
"/",
|
||||
"/mcp/",
|
||||
"/docs",
|
||||
"/{rest:path}",
|
||||
"/mcp?mode=test",
|
||||
"/mcp#fragment",
|
||||
"/mcp%2Fv2",
|
||||
"/mcp%20v2",
|
||||
"/mcp%252Fv2",
|
||||
"/mcp path",
|
||||
"/mcp//nested",
|
||||
"/mcp/../nested",
|
||||
):
|
||||
with pytest.raises(ValueError, match="mcp_path"):
|
||||
HttpService(mcp_path=path)
|
||||
|
||||
service = HttpService(web_enabled=False)
|
||||
service.build_service(_FakeApplication()) # type: ignore[arg-type]
|
||||
with pytest.raises(ValueError, match="conflicts with the MCP endpoint"):
|
||||
service.add_job(BaseJob(name="mcp"))
|
||||
|
||||
|
||||
def test_http_service_fails_startup_preflight_for_mcp_job_conflict() -> None:
|
||||
"""Do not let BaseService's tolerant registration hide reserved-route conflicts."""
|
||||
service = HttpService(web_enabled=False)
|
||||
service.build_service(_FakeApplication()) # type: ignore[arg-type]
|
||||
app = SimpleNamespace(
|
||||
context=SimpleNamespace(jobs={"mcp": BaseJob(name="mcp")}),
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="conflicts with the MCP endpoint"):
|
||||
service.add_jobs(app)
|
||||
|
||||
|
||||
def test_http_service_does_not_serve_symlinks_outside_static_dir(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
def test_http_service_does_not_serve_symlinks_outside_static_dir(tmp_path: Path) -> None:
|
||||
"""Do not expose files reached through symlinks outside the static build."""
|
||||
static_dir = _static_build(tmp_path)
|
||||
secret_file = tmp_path / "secret.txt"
|
||||
|
|
@ -243,10 +93,7 @@ def test_http_service_does_not_serve_symlinks_outside_static_dir(
|
|||
assert client.get("/escape.txt").text == "<main>ReMe workspace</main>"
|
||||
|
||||
|
||||
def test_static_dir_configuration_precedes_environment(
|
||||
monkeypatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
def test_static_dir_configuration_precedes_environment(monkeypatch, tmp_path: Path) -> None:
|
||||
"""Prefer an explicit static directory over the environment setting."""
|
||||
configured = _static_build(tmp_path / "configured")
|
||||
environment = _static_build(tmp_path / "environment")
|
||||
|
|
|
|||
|
|
@ -65,31 +65,6 @@ 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 -----------------------------------------------------------
|
||||
|
||||
|
||||
|
|
@ -164,7 +139,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"}
|
||||
|
||||
|
|
|
|||
|
|
@ -509,95 +509,24 @@ def test_cache_space_is_rechecked_after_async_load(monkeypatch, tmp_path):
|
|||
run(go())
|
||||
|
||||
|
||||
def test_whole_request_retries_after_vector_space_changes_between_batches():
|
||||
"""Completed batches must be discarded when a later batch changes vector space."""
|
||||
def test_completed_request_only_writes_to_its_active_cache_space():
|
||||
"""A v3 request must not populate v4 after the provider switches back to v3."""
|
||||
|
||||
async def go():
|
||||
embedding = FakeAsEmbedding()
|
||||
embedding.vector_space_id = "v3"
|
||||
store = LocalEmbeddingStore(name="t_local_write_race", max_batch_size=1, enable_cache=False)
|
||||
embedding = OpenAIAsEmbedding(name="t_space_write_race", backend="openai", model="v3", dimensions=2)
|
||||
store = LocalEmbeddingStore(name="t_local_write_race")
|
||||
store.as_embedding = embedding
|
||||
store._cache_space = embedding.vector_space_id
|
||||
calls = 0
|
||||
|
||||
async def switch_during_second_batch(batch, **_kwargs):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
if calls == 2:
|
||||
embedding.vector_space_id = "v4"
|
||||
idx, _text, key = batch[0]
|
||||
version = 3.0 if calls < 3 else 4.0
|
||||
return [(idx, key, np.array([version, 0.0], dtype=np.float16))]
|
||||
async def compute_after_round_trip(_batch, **_kwargs):
|
||||
embedding.model = FakeProviderModel("v4")
|
||||
store._cache_space = embedding.vector_space_id
|
||||
embedding.model = FakeProviderModel("v3")
|
||||
return [(0, "key", np.array([3.0, 0.0], dtype=np.float16))]
|
||||
|
||||
store._compute_batch = switch_during_second_batch
|
||||
results = await store.get_embeddings(["first", "second"])
|
||||
store._compute_batch = compute_after_round_trip
|
||||
await store._fill_misses([(0, "text", "key")], [None])
|
||||
|
||||
assert calls == 4
|
||||
assert store._cache_space == embedding.vector_space_id
|
||||
for result in results:
|
||||
np.testing.assert_array_equal(result, np.array([4.0, 0.0], dtype=np.float16))
|
||||
|
||||
run(go())
|
||||
|
||||
|
||||
def test_whole_request_rereads_cache_after_vector_space_changes(monkeypatch, tmp_path):
|
||||
"""A cache hit from the old space must not survive a later provider switch."""
|
||||
|
||||
async def go():
|
||||
monkeypatch.setattr(
|
||||
LocalEmbeddingStore,
|
||||
"component_metadata_path",
|
||||
property(lambda _self: tmp_path),
|
||||
)
|
||||
embedding = FakeAsEmbedding()
|
||||
embedding.vector_space_id = "v3"
|
||||
store = LocalEmbeddingStore(name="t_local_cache_race", enable_cache=True)
|
||||
store.as_embedding = embedding
|
||||
store._cache_space = embedding.vector_space_id
|
||||
first_key = store._cache_key("first")
|
||||
store._cache[first_key] = np.array([3.0, 0.0], dtype=np.float16)
|
||||
calls = 0
|
||||
|
||||
async def switch_on_miss(batch, **_kwargs):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
if calls == 1:
|
||||
embedding.vector_space_id = "v4"
|
||||
version = 3.0 if calls == 1 else 4.0
|
||||
return [(idx, key, np.array([version, 0.0], dtype=np.float16)) for idx, _text, key in batch]
|
||||
|
||||
store._compute_batch = switch_on_miss
|
||||
results = await store.get_embeddings(["first", "second"])
|
||||
|
||||
assert calls == 2
|
||||
for result in results:
|
||||
np.testing.assert_array_equal(result, np.array([4.0, 0.0], dtype=np.float16))
|
||||
|
||||
run(go())
|
||||
|
||||
|
||||
def test_whole_request_stops_retrying_when_vector_space_keeps_changing():
|
||||
"""Continuous configuration churn must discard the whole request instead of blocking forever."""
|
||||
|
||||
async def go():
|
||||
embedding = FakeAsEmbedding()
|
||||
embedding.vector_space_id = "v3"
|
||||
store = LocalEmbeddingStore(name="t_local_write_churn")
|
||||
store.as_embedding = embedding
|
||||
store._cache_space = embedding.vector_space_id
|
||||
calls = 0
|
||||
|
||||
async def change_space_every_time(_batch, **_kwargs):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
embedding.vector_space_id = f"v{calls + 3}"
|
||||
return [(0, "key", np.array([float(calls), 0.0], dtype=np.float16))]
|
||||
|
||||
store._compute_batch = change_space_every_time
|
||||
results = await store.get_embeddings(["text"])
|
||||
|
||||
assert calls == 3
|
||||
assert results == [None]
|
||||
assert "key" not in store._cache
|
||||
|
||||
run(go())
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ def test_logo_uses_runtime_http_address(monkeypatch) -> None:
|
|||
output = _render_logo(monkeypatch, config, runtime_service)
|
||||
|
||||
assert "http://0.0.0.0:8123" in output
|
||||
assert "http://0.0.0.0:8123/mcp" in output
|
||||
|
||||
|
||||
def test_logo_fallback_matches_service_defaults(monkeypatch) -> None:
|
||||
|
|
@ -40,18 +39,6 @@ def test_logo_fallback_matches_service_defaults(monkeypatch) -> None:
|
|||
output = _render_logo(monkeypatch, config)
|
||||
|
||||
assert f"http://{REME_DEFAULT_HOST}:{REME_DEFAULT_PORT}" in output
|
||||
assert f"http://{REME_DEFAULT_HOST}:{REME_DEFAULT_PORT}/mcp" in output
|
||||
|
||||
|
||||
def test_logo_hides_disabled_http_mcp_endpoint(monkeypatch) -> None:
|
||||
"""Do not advertise MCP when it is explicitly disabled on the HTTP service."""
|
||||
config = ApplicationConfig(
|
||||
service=ComponentConfig(backend="http", mcp_enabled=False),
|
||||
)
|
||||
|
||||
output = _render_logo(monkeypatch, config)
|
||||
|
||||
assert f"http://{REME_DEFAULT_HOST}:{REME_DEFAULT_PORT}/mcp" not in output
|
||||
|
||||
|
||||
def test_logo_uses_runtime_mcp_transport_and_address(monkeypatch) -> None:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import tomllib
|
|||
from types import ModuleType
|
||||
|
||||
from packaging.requirements import Requirement
|
||||
from packaging.version import Version
|
||||
import pytest
|
||||
|
||||
REPOSITORY = Path(__file__).resolve().parents[2]
|
||||
|
|
@ -42,13 +41,17 @@ def test_studio_packages_have_independent_identity() -> None:
|
|||
assert studio_config["project"]["name"] == "reme_studio"
|
||||
assert npm_config["name"] == "@agentscope-ai/reme_studio"
|
||||
assert studio_config["project"]["version"] == npm_config["version"]
|
||||
assert main_config["project"]["optional-dependencies"]["as"] == ["agentscope[model-ollama]==2.0.7"]
|
||||
assert main_config["project"]["optional-dependencies"]["as"] == ["agentscope[model-ollama]==2.0.6"]
|
||||
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 "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["project"]["optional-dependencies"]["qwenpaw"] == [
|
||||
"reme-ai[core]",
|
||||
"reme-auto-fin>=0.1.1",
|
||||
"reme-daily-paper>=0.1.1",
|
||||
]
|
||||
assert auto_fin_config["project"]["version"] == "0.1.1"
|
||||
assert daily_paper_config["project"]["version"] == "0.1.1"
|
||||
assert main_config["tool"]["setuptools"]["packages"]["find"]["include"] == ["reme", "reme.*"]
|
||||
assert "reme_studio*" in main_config["tool"]["setuptools"]["packages"]["find"]["exclude"]
|
||||
|
||||
|
|
@ -150,16 +153,14 @@ def test_auto_fin_license_matches_repository() -> None:
|
|||
).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_auto_fin_requires_reme_base() -> None:
|
||||
"""Keep the plugin dependency limited to ReMe's public base package."""
|
||||
def test_auto_fin_requires_reme_core() -> None:
|
||||
"""Install the optional runtime packages needed while loading Auto Fin's entry points."""
|
||||
config = tomllib.loads((REPOSITORY / "plugins" / "auto-fin" / "pyproject.toml").read_text(encoding="utf-8"))
|
||||
requirements = [Requirement(value) for value in config["project"]["dependencies"]]
|
||||
reme_requirements = [requirement for requirement in requirements if requirement.name == "reme-ai"]
|
||||
|
||||
assert len(reme_requirements) == 1
|
||||
assert not reme_requirements[0].extras
|
||||
assert Version("0.4.1.8") not in reme_requirements[0].specifier
|
||||
assert Version("0.4.1.9") in reme_requirements[0].specifier
|
||||
assert set(reme_requirements[0].extras) == {"core"}
|
||||
|
||||
|
||||
def test_daily_paper_license_matches_repository() -> None:
|
||||
|
|
@ -170,14 +171,12 @@ def test_daily_paper_license_matches_repository() -> None:
|
|||
|
||||
|
||||
def test_daily_paper_declares_runtime_dependencies() -> None:
|
||||
"""Keep Daily Paper's minimal ReMe and PDF dependencies explicit."""
|
||||
"""Keep Daily Paper's ReMe feature set and PDF parser explicit in its own distribution."""
|
||||
config = tomllib.loads((REPOSITORY / "plugins" / "daily_paper" / "pyproject.toml").read_text(encoding="utf-8"))
|
||||
requirements = [Requirement(value) for value in config["project"]["dependencies"]]
|
||||
by_name = {requirement.name: requirement for requirement in requirements}
|
||||
|
||||
assert not by_name["reme-ai"].extras
|
||||
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 set(by_name["reme-ai"].extras) == {"core"}
|
||||
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", "default"]) == 0
|
||||
assert plugin_cli_module.plugin_cli(["list", "--config", "daily_cookbook"]) == 0
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "ENABLED" in output
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue