From fd2894f9399206645b3adc634982f88c32f36dd9 Mon Sep 17 00:00:00 2001 From: jinliyl <6469360+jinliyl@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:22:00 +0800 Subject: [PATCH] fix: harden the 0.4.1.7 release configuration (#456) * fix(packaging): harden the Studio release workflow * chore(daily-paper): tune scheduled discovery defaults * fix(docs): link the ReMe blog to GitHub Pages * fix(docs): increase Chinese hero title spacing * refactor(docs): share hero title line spacing * fix(docs): keep desktop hero copy on two lines * fix(docs): widen the home hero description * style(docs): loosen hero title line height * fix(docs): hide Markdown frontmatter in rendered pages * docs(readme): simplify installation and remove standalone ReMe Studio instructions - Remove references to separate ReMe Studio package and static build steps - Clarify that `core` extra includes common integrations including Studio - Update installation instructions to use `pip install -e ".[core]"` - Remove detailed Studio usage and frontend development instructions - Note that Studio is included with `core` and optional via `web` extra - Simplify Quick Start guide by removing Studio usage step - Remove mentions of serving Studio with HTTP service when using extras - Update both English and Chinese README files accordingly * docs(readme): streamline and clarify memory design and operations - Remove redundant explanations about core extra installation - Simplify memory processing flow description for clarity - Clarify memory workspace directory default and customization - Condense automatic memory flow to emphasize rebuildable metadata - Refine search functionality explanation with RRF fusion details - Shorten and clarify agent integration description, removing redundancy - Update and simplify the operations command list, removing less common commands - Revise community and support section for conciseness and clarity - Maintain parallel updates in both English and Chinese README files * test(bump_version): add tests for version bumping and consistency checks - Add dynamic loading of bump_version and package_studio scripts for testing - Test that studio package and dependencies have matching versions - Implement fixtures to write temporary version files for testing - Add test ensuring bump_version updates all relevant files and dependencies - Add test to reject inconsistent version sources before writing - Refactor tests to use common REPOSITORY path variable - Include imports and setup for pytest in test file feat(bump_version): create script to update ReMe and Studio versions - Implement version reading from __init__.py and pyproject.toml files - Validate current versions are consistent across files before updating - Update version strings atomically to avoid partial writes - Ensure exact pinning of studio dependency in main package extras - Validate new version format against a safe pattern - Provide CLI interface to bump versions from command line - Raise errors if expected version declarations or pins are missing or duplicated * fix(release): validate split package publishing * fix(release): improve validation diagnostics * fix(release): sync docs and workflow inputs * fix(release): split PyPI publish jobs --- .github/workflows/github-pages-check.yml | 61 +++++++ .github/workflows/package-check.yml | 97 ++++++++++ .github/workflows/pages.yml | 4 + .github/workflows/python-publish.yml | 160 +++++++++++------ .github/workflows/windows-smoke.yml | 20 +++ AGENTS.md | 2 +- README.md | 98 +++-------- README_ZH.md | 82 +++------ cookbook/daily_paper/README.md | 13 +- cookbook/daily_paper/README_ZH.md | 13 +- docs/en/contributing.md | 4 + docs/en/quick_start.md | 6 + docs/zh/contributing.md | 4 + docs/zh/quick_start.md | 6 + github-pages/package.json | 3 +- github-pages/scripts/generate-content.mjs | 2 +- github-pages/src/main.js | 3 +- github-pages/src/markdown.js | 6 + github-pages/src/styles.css | 4 +- github-pages/tests/markdown.test.mjs | 15 ++ packages/reme_ai_studio/.gitignore | 1 + packages/reme_ai_studio/pyproject.toml | 5 +- pyproject.toml | 5 +- reme/__init__.py | 2 +- reme/config/daily_cookbook.yaml | 3 +- scripts/bump_version.py | 205 ++++++++++++++++++++++ scripts/package_studio.py | 13 +- tests/unit/test_daily_paper.py | 15 +- tests/unit/test_package_versions.py | 196 +++++++++++++++++++-- 29 files changed, 822 insertions(+), 226 deletions(-) create mode 100644 .github/workflows/github-pages-check.yml create mode 100644 .github/workflows/package-check.yml create mode 100644 github-pages/src/markdown.js create mode 100644 github-pages/tests/markdown.test.mjs create mode 100644 packages/reme_ai_studio/.gitignore create mode 100644 scripts/bump_version.py diff --git a/.github/workflows/github-pages-check.yml b/.github/workflows/github-pages-check.yml new file mode 100644 index 00000000..1b7ca454 --- /dev/null +++ b/.github/workflows/github-pages-check.yml @@ -0,0 +1,61 @@ +name: GitHub Pages Check + +on: + push: + branches: [main, master, dev, develop] + paths: + - '.github/workflows/github-pages-check.yml' + - 'AGENTS.md' + - 'README.md' + - 'README_ZH.md' + - 'docs/**' + - 'github-pages/**' + - 'website/README*.md' + - 'website/public/og.jpg' + - 'cookbook/*/README*.md' + - 'benchmark/*/README*.md' + - 'skills/reme_memory/SKILL.md' + pull_request: + branches: [main, master, dev, develop] + paths: + - '.github/workflows/github-pages-check.yml' + - 'AGENTS.md' + - 'README.md' + - 'README_ZH.md' + - 'docs/**' + - 'github-pages/**' + - 'website/README*.md' + - 'website/public/og.jpg' + - 'cookbook/*/README*.md' + - 'benchmark/*/README*.md' + - 'skills/reme_memory/SKILL.md' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + test-and-build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: github-pages + steps: + - uses: actions/checkout@v6 + + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: '22.13' + cache: npm + cache-dependency-path: github-pages/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Build documentation + run: npm run build diff --git a/.github/workflows/package-check.yml b/.github/workflows/package-check.yml new file mode 100644 index 00000000..6705e79e --- /dev/null +++ b/.github/workflows/package-check.yml @@ -0,0 +1,97 @@ +name: Package Check + +on: + push: + branches: [main, master, dev, develop] + paths: + - '.github/workflows/package-check.yml' + - '.github/workflows/python-publish.yml' + - 'packages/reme_ai_studio/**' + - 'pyproject.toml' + - 'reme/__init__.py' + - 'reme/utils/web_static.py' + - 'scripts/bump_version.py' + - 'scripts/package_studio.py' + - 'tests/unit/test_package_versions.py' + - 'website/**' + - 'LICENSE' + pull_request: + branches: [main, master, dev, develop] + paths: + - '.github/workflows/package-check.yml' + - '.github/workflows/python-publish.yml' + - 'packages/reme_ai_studio/**' + - 'pyproject.toml' + - 'reme/__init__.py' + - 'reme/utils/web_static.py' + - 'scripts/bump_version.py' + - 'scripts/package_studio.py' + - 'tests/unit/test_package_versions.py' + - 'website/**' + - 'LICENSE' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + distributions: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '22.13' + cache: npm + cache-dependency-path: website/package-lock.json + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.11' + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + python -m pip install build packaging pytest twine + + - name: Validate release versions + run: python scripts/bump_version.py --check + + - name: Run package tests + run: PYTHONPATH=. python -m pytest tests/unit/test_package_versions.py -q + + - name: Build Studio static workspace + working-directory: website + run: | + npm ci + npm run build:static + + - name: Build and check distributions + run: | + python scripts/package_studio.py + mkdir -p dist/reme dist/studio + python -m build --outdir dist/reme + python -m build packages/reme_ai_studio --outdir dist/studio + python -m twine check dist/reme/* dist/studio/* + + - name: Verify distributions and isolated installation + run: | + REME_WHEEL="$(pwd)/$(ls dist/reme/reme_ai-[0-9]*.whl)" + STUDIO_WHEEL="$(pwd)/$(ls dist/studio/reme_ai_studio-*.whl)" + STUDIO_SDIST="$(pwd)/$(ls dist/studio/reme_ai_studio-*.tar.gz)" + python -m zipfile -l "${REME_WHEEL}" | (! grep 'reme/web/') + python -m zipfile -l "${STUDIO_WHEEL}" | grep 'reme_ai_studio/static/index.html' + python -m zipfile -l "${STUDIO_WHEEL}" | grep 'dist-info/licenses/LICENSE' + python -m tarfile -l "${STUDIO_SDIST}" | grep '/LICENSE' + python -m venv "${RUNNER_TEMP}/reme-package-smoke" + "${RUNNER_TEMP}/reme-package-smoke/bin/python" -m pip install \ + --find-links "$(pwd)/dist/studio" "${REME_WHEEL}[core]" + cd "${RUNNER_TEMP}" + "${RUNNER_TEMP}/reme-package-smoke/bin/python" -c \ + "import reme; from reme_ai_studio import static_dir; assert (static_dir() / 'index.html').is_file()" + "${RUNNER_TEMP}/reme-package-smoke/bin/python" -c \ + "from reme.utils import resolve_web_static_dir; assert (resolve_web_static_dir() / 'index.html').is_file()" diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index b2cfbb4a..5337b01f 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -8,6 +8,10 @@ on: - "docs/**" - "README.md" - "README_ZH.md" + - "website/README*.md" + - "website/public/og.jpg" + - "cookbook/*/README*.md" + - "benchmark/*/README*.md" - "skills/reme_memory/SKILL.md" - "AGENTS.md" - ".github/workflows/pages.yml" diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 8bb12ca7..223e4d81 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,15 +1,12 @@ -# This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Publish Python Package to Pypi +name: Publish Python packages to PyPI on: workflow_dispatch: + inputs: + version: + description: Release version + required: true + type: string release: types: [published] @@ -17,47 +14,110 @@ permissions: contents: read jobs: - deploy: - + build: runs-on: ubuntu-latest + env: + RELEASE_VERSION: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.version }} steps: - - uses: actions/checkout@v6 - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version: '22' - cache: npm - cache-dependency-path: website/package-lock.json - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: '3.11' - - name: Build web workspace - working-directory: website - run: | - npm ci - npm run build:static - - name: Prepare Studio package - run: python scripts/package_studio.py - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel build - - name: Build package - run: | - python -m build --outdir dist - python -m build packages/reme_ai_studio --outdir dist - - name: Test installation - run: | - REME_WHEEL="$(pwd)/$(ls dist/reme_ai-[0-9]*.whl)" - python -m zipfile -l "${REME_WHEEL}" | (! grep 'reme/web/') - pip install --find-links "$(pwd)/dist" "${REME_WHEEL}[core]" - python -c "import reme; print(reme.__version__)" - python -c "from reme_ai_studio import static_dir; assert (static_dir() / 'index.html').is_file()" - python -c "from reme.utils import resolve_web_static_dir; assert (resolve_web_static_dir() / 'index.html').is_file()" - - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} + - uses: actions/checkout@v6 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '22.13' + cache: npm + cache-dependency-path: website/package-lock.json + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.11' + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + python -m pip install build packaging twine + + - name: Validate release version + run: python scripts/bump_version.py --check --expected-version "${RELEASE_VERSION}" + + - name: Build Studio static workspace + working-directory: website + run: | + npm ci + npm run build:static + + - name: Prepare and build distributions + run: | + python scripts/package_studio.py + mkdir -p dist/reme dist/studio + python -m build --outdir dist/reme + python -m build packages/reme_ai_studio --outdir dist/studio + python -m twine check dist/reme/* dist/studio/* + + - name: Verify distributions and isolated installation + run: | + REME_WHEEL="$(pwd)/$(ls dist/reme/reme_ai-[0-9]*.whl)" + STUDIO_WHEEL="$(pwd)/$(ls dist/studio/reme_ai_studio-*.whl)" + STUDIO_SDIST="$(pwd)/$(ls dist/studio/reme_ai_studio-*.tar.gz)" + python -m zipfile -l "${REME_WHEEL}" | (! grep 'reme/web/') + python -m zipfile -l "${STUDIO_WHEEL}" | grep 'reme_ai_studio/static/index.html' + python -m zipfile -l "${STUDIO_WHEEL}" | grep 'dist-info/licenses/LICENSE' + python -m tarfile -l "${STUDIO_SDIST}" | grep '/LICENSE' + python -m venv "${RUNNER_TEMP}/reme-release-smoke" + "${RUNNER_TEMP}/reme-release-smoke/bin/python" -m pip install \ + --find-links "$(pwd)/dist/studio" "${REME_WHEEL}[core]" + cd "${RUNNER_TEMP}" + "${RUNNER_TEMP}/reme-release-smoke/bin/python" -c \ + "import reme; from reme_ai_studio import static_dir; assert (static_dir() / 'index.html').is_file()" + "${RUNNER_TEMP}/reme-release-smoke/bin/python" -c \ + "from reme.utils import resolve_web_static_dir; assert (resolve_web_static_dir() / 'index.html').is_file()" + + - name: Upload ReMe Studio distributions + uses: actions/upload-artifact@v4 + with: + name: reme-studio-distributions + path: dist/studio/ + + - name: Upload ReMe distributions + uses: actions/upload-artifact@v4 + with: + name: reme-distributions + path: dist/reme/ + + publish-studio: + needs: build + runs-on: ubuntu-latest + steps: + - name: Download ReMe Studio distributions + uses: actions/download-artifact@v4 + with: + name: reme-studio-distributions + path: dist/studio + + - name: Publish ReMe Studio + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + packages-dir: dist/studio + skip-existing: true + + publish-reme: + needs: publish-studio + runs-on: ubuntu-latest + steps: + - name: Download ReMe distributions + uses: actions/download-artifact@v4 + with: + name: reme-distributions + path: dist/reme + + - name: Publish ReMe + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + packages-dir: dist/reme + skip-existing: true diff --git a/.github/workflows/windows-smoke.yml b/.github/workflows/windows-smoke.yml index e72080a1..54760137 100644 --- a/.github/workflows/windows-smoke.yml +++ b/.github/workflows/windows-smoke.yml @@ -23,6 +23,13 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + cache-dependency-path: website/package-lock.json + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: @@ -34,6 +41,19 @@ jobs: python -m pip install --upgrade pip setuptools wheel pip install -e packages/reme_ai_studio -e ".[dev,core]" + - name: Build Studio static workspace + working-directory: website + run: | + npm ci + npm run build:static + + - name: Verify editable source installation serves Studio + shell: pwsh + run: | + Push-Location $env:RUNNER_TEMP + python -c "from reme.utils import resolve_web_static_dir; assert (resolve_web_static_dir() / 'index.html').is_file()" + Pop-Location + - name: Run version job run: reme start service.backend=cli job=version diff --git a/AGENTS.md b/AGENTS.md index e4065fba..931204fc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,7 +64,7 @@ and concise documentation together. ReMe requires Python 3.11 or newer. Install the editable development environment with: ```bash -pip install -e ".[dev,core]" +pip install -e packages/reme_ai_studio -e ".[dev,core]" ``` Before changing behavior, inspect the adjacent implementation, schema, built-in config, and focused tests. Follow diff --git a/README.md b/README.md index 26e26741..94861322 100644 --- a/README.md +++ b/README.md @@ -61,10 +61,8 @@ keeping the files under the user's control. ## 📰 News -- [2026.08] - Published the [ReMe blog](docs/en/reme-blog.md), an end-to-end introduction to its local-first memory +- [2026.08] - Published the [ReMe blog](https://agentscope-ai.github.io/ReMe/?doc=en-reme-blog), an end-to-end introduction to its local-first memory architecture, self-evolving workflows, hybrid search, proactive discovery, and benchmark results. -- [2026.08] - Introduced [ReMe Studio](https://reme.agentscope.io/?doc=studio-en), a local web workspace for browsing, editing, and searching - memory files, chatting with the read-only ReMe Agent, inspecting the digest wikilink graph, and managing the local service. - [2026.08] - [Experience-driven enhancement method](https://reme.agentscope.io/?doc=toolmemory-en) of agent tool-use execution built on ReMe is available on [arXiv:2608.03403](https://arxiv.org/abs/2608.03403). - [2026.07] - Introduced optional Cookbooks: [Daily Paper](https://reme.agentscope.io/?doc=daily-paper-en) for paper discovery and @@ -86,18 +84,20 @@ Install from pip: pip install "reme-ai[core]" ``` -The base `reme-ai` package contains only the Python service and library. ReMe Studio is distributed separately and is -installed by the `web` and `core` extras. Use `pip install reme-ai` for embedded or headless integrations that do not -need the frontend, or `pip install "reme-ai[web]"` when Studio is needed without the other `core` integrations. - Install from source: ```bash git clone https://github.com/agentscope-ai/ReMe.git cd ReMe pip install -e packages/reme_ai_studio -e ".[core]" +cd website +npm ci +npm run build:static +cd .. ``` +The static build requires Node.js 22.13 or newer and makes Studio available from the source tree. + ### Environment Variables Configure environment variables when you want LLM-powered memory evolution or embedding retrieval. Embeddings are @@ -136,14 +136,6 @@ reme start service.port=8181 # reme start workspace_dir=/tmp/reme-demo service.port=8181 ``` -After startup, check the service status. If you use a custom port, replace `2333` in the URL below with that port. - -When the `web` or `core` extra is installed, the HTTP service also serves **ReMe Studio** at -. Studio can -browse, edit, and search the workspace, chat with the read-only workspace agent, and inspect the digest wikilink graph. -Set `service.web_enabled=false` to disable it, or use `service.web_static_dir` / `REME_WEB_STATIC_DIR` to provide a -custom static build. The Job API remains available when no web build is found. - ```bash reme version reme health_check @@ -151,32 +143,11 @@ reme help curl -s http://127.0.0.1:2333/version -H 'Content-Type: application/json' -d '{}' ``` -### Use ReMe Studio +### ReMe Studio (Optional) -Open after starting the default HTTP service. Studio provides: - -- **Files, Daily, and Knowledge views** for navigating the whole workspace or focusing on `daily/` and `digest/`. -- **Markdown tabs** with preview, split editing, optimistic save checks, and local download. -- **Memory Graph** for exploring indexed `personal`, `procedure`, and `wiki` nodes and opening their Markdown sources. -- **Read-only Agent chat** with streamed tool activity and usage; drag a workspace file into the composer to reference it. -- **Settings** for service/component status, redacted effective configuration, version information, and safe index rebuilding. -- English/Chinese language switching and light, dark, or system appearance. - -For frontend development, run ReMe and Studio in separate terminals: - -```bash -# Terminal 1, repository root -reme start - -# Terminal 2 -cd website -npm install -npm run dev -``` - -Then open . The development server uses `http://127.0.0.1:2333` by default; set -`NEXT_PUBLIC_REME_API_URL` to connect to another ReMe HTTP service. Static-build and frontend configuration instructions are -in the [ReMe Studio guide](https://reme.agentscope.io/?doc=studio-en). +The `core` installation above includes Studio. After starting ReMe, open to browse, edit, and +search the workspace. To add Studio to a base installation, use `pip install "reme-ai[web]"`. See the +[ReMe Studio guide](https://reme.agentscope.io/?doc=studio-en) for source builds, configuration, and development. ### 5-Minute Memory Demo @@ -218,7 +189,7 @@ These Markdown guides cover the main user workflows and the runtime contracts im | Guide | What you will learn | |-------|---------------------| -| [Quick Start](docs/en/quick_start.md) | Install ReMe, start the service, use Studio, and run the first file and memory operations. | +| [Quick Start](docs/en/quick_start.md) | Install ReMe, start the service, and run the first file and memory operations. | | [Memory as File](docs/en/memory_as_file.md) | Understand workspace layers, frontmatter, wikilinks, chunks, and the file-as-source-of-truth model. | | [Auto Memory](docs/en/auto_memory.md) | Preserve source conversations and distill reusable daily memory cards. | | [Auto Resource](docs/en/auto_resource.md) | Import supported text resources and turn them into source-linked daily cards. | @@ -227,8 +198,7 @@ These Markdown guides cover the main user workflows and the runtime contracts im | [Proactive](docs/en/proactive.md) | Read interest topics safely and integrate them into a host agent's decision flow. | | [Agent Integration Scenarios](docs/en/reme_scene.md) | Choose among CLI/SKILL.md, HTTP, MCP, and embedded Python integration. | | [Framework](docs/en/framework.md) | Understand Application, Job, Step, Component, service, configuration, and lifecycle boundaries. | -| [ReMe Studio](https://reme.agentscope.io/?doc=studio-en) | Use, configure, develop, test, and build the web frontend. | -| [ReMe Blog](docs/en/reme-blog.md) | Read the product story, design rationale, examples, and benchmark summary. | +| [ReMe Blog](https://agentscope-ai.github.io/ReMe/?doc=en-reme-blog) | Read the product story, design rationale, examples, and benchmark summary. | ## 🧑‍🍳 Cookbooks @@ -246,9 +216,8 @@ another row in this table. > Memory as File, File as Memory. ReMe treats **memory as files**, progressively processing filtered conversation source records and external resources -from `session/` and `resource/` into `daily/`, then consolidating them into reusable long-term memory nodes under -`digest/`. The default workspace is `.reme/` under the current directory; `workspace_dir=...` selects a different -user-owned location. +from `session/` and `resource/` into `daily/`, then `digest/`. The default workspace is `.reme/` under the current +directory; `workspace_dir=...` selects a different user-owned location. ### Directory Structure @@ -288,16 +257,8 @@ user-owned location. ## 🧭 Memory Design Philosophy -> Capture conversation source records and resources, refine them into long-term preferences, reusable experience, and -> valuable knowledge, -> while keeping the result editable by humans and agents. - -### Automatic Memory Flow - -ReMe follows a capture → index → consolidate → recall loop. Conversations and resources first become daily memory cards; -background jobs keep files searchable; `auto_dream` distills stable knowledge into `digest/`; agents recall memory -through search, wikilinks, or proactive topics. The files are the durable source of truth—indexes, graphs, catalogs, and -caches under `metadata/` can be rebuilt from them. +ReMe follows a capture → index → consolidate → recall loop. Workspace files remain the durable source of truth; +everything under `metadata/` is rebuildable. | Capability | Entry point | What it does | Output | |---------------------------------------------|-------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------| @@ -326,10 +287,8 @@ caches under `metadata/` can be rebuilt from them. -Search returns the best matching chunks with file paths and line ranges, then lists bounded incoming and outgoing -wikilink neighbors by metadata. An agent can read a promising source or traverse the graph only when needed. With -embeddings enabled, BM25 and vector rankings are fused with reciprocal rank fusion (RRF); otherwise the default remains -BM25 plus wikilink expansion. +Search returns matching chunks with line ranges and bounded wikilink neighbors. Optional vector results are fused with +BM25 through reciprocal rank fusion (RRF). > [!IMPORTANT] > `proactive` only reads and exposes interest topics produced by Auto Dream. It does not independently browse the web, @@ -355,8 +314,7 @@ dependencies, and underspecified requests. ## 🤝 Agent-friendly Integration ReMe can run as a local memory service accessed through the CLI, HTTP API, or MCP server, or it can be embedded in the -host process through its Python API. The default HTTP service can serve ReMe Studio at the same address. Agents can -choose the path that fits their runtime and share a local memory workspace when appropriate. +host process through its Python API. | Agents | Recommended path | Available after integration | |-----------------------------------------------|---------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------| @@ -395,28 +353,21 @@ choose the path that fits their runtime and share a local memory workspace when ## 🛠️ ReMe Operations -ReMe operates the workspace through a unified job interface exposed by the CLI. Agents usually only need retrieval, -reading, writing, editing, and automatic memory commands. Lower-level indexing, frontmatter, and file operation commands -are mainly for maintenance, debugging, or advanced integration. Run `reme help` for the full job list. +Run `reme help` for the full job list. Common workspace and maintenance commands are: | Command | Purpose | |-------------------------------------------|----------------------------------------------------------------------------------------| -| `reme start` | Start the local ReMe service. | -| `reme version` / `reme health_check` | Check package and component status. | | `reme status` | Show stateful data-component memory estimates and process RSS. | | [`reme search`](docs/en/memory_search.md) | Retrieve memory with BM25 and wikilinks by default, plus vectors when enabled. | | `reme read` / `reme write` / `reme edit` | Inspect and maintain Markdown memory files. | | `reme traverse` / `reme graph_snapshot` | Explore wikilink neighborhoods or the category-rooted digest graph. | | `reme chat` | Stream a read-only, workspace-aware agent conversation. Requires LLM credentials. | -| `reme auto_memory` | Turn conversation messages into daily memory cards. Requires LLM credentials. | -| `reme auto_resource` | Interpret files under `resource/` into daily resource cards. Requires LLM credentials. | -| `reme auto_dream` / `reme proactive` | Consolidate daily memory into long-term digest and surface topics worth attention. | | `reme reindex` | Rebuild search and wikilink indexes from existing files. | ## 🤝 Community and Support -- **Issues and requests**: Check [Open Issues](https://github.com/agentscope-ai/ReMe/issues) first. If there is no - related discussion, open a new issue with background, expected behavior, and impact scope. +- **Issues, requests, and help**: Check [Open Issues](https://github.com/agentscope-ai/ReMe/issues) first. If there is no + related discussion, open one with the background, expected behavior, and impact scope. - **Code contributions**: Before making changes, read the [contribution guide](https://docs.agentscope.io/reme/latest/en/contribution). Source, schemas, and tests are the authoritative architecture and extension guide. @@ -426,8 +377,7 @@ are mainly for maintenance, debugging, or advanced integration. Run `reme help` `docs(zh): update quick start`. - **Pre-submit checks**: Before submitting a PR, try to run `pre-commit run --all-files` and `pytest`. If tests that depend on LLMs, embeddings, or external services cannot run, explain that in the PR. -- **Get help**: Use [GitHub Issues](https://github.com/agentscope-ai/ReMe/issues) for bugs and feature requests. Project - documentation is available at [https://reme.agentscope.io](https://reme.agentscope.io). +- **Documentation**: Visit [reme.agentscope.io](https://reme.agentscope.io). ### Contributors diff --git a/README_ZH.md b/README_ZH.md index a1c6a7a7..e23837b5 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -53,10 +53,8 @@ Code 等 Agent 协作,在持续整理知识的同时,始终把文件控制 ## 📰 新闻 -- [2026.08] - 发布 [ReMe 博客](docs/zh/reme-blog.md),系统介绍本地优先的记忆架构、自进化工作流、混合检索、 +- [2026.08] - 发布 [ReMe 博客](https://agentscope-ai.github.io/ReMe/?doc=zh-reme-blog),系统介绍本地优先的记忆架构、自进化工作流、混合检索、 主动发现与评测结果。 -- [2026.08] - 新增 [ReMe Studio](https://reme.agentscope.io/?doc=studio-zh):用于浏览、编辑和搜索记忆文件,与只读 ReMe Agent - 对话,查看 digest wikilink 图,并管理本地服务。 - [2026.08] - 基于 ReMe 的智能体工具使用 [经验驱动增强方法](https://reme.agentscope.io/?doc=toolmemory-zh)已发布,见 [arXiv:2608.03403](https://arxiv.org/abs/2608.03403)。 @@ -84,9 +82,15 @@ pip install "reme-ai[core]" ```bash git clone https://github.com/agentscope-ai/ReMe.git cd ReMe -pip install -e ".[core]" +pip install -e packages/reme_ai_studio -e ".[core]" +cd website +npm ci +npm run build:static +cd .. ``` +静态构建要求 Node.js 22.13 或更高版本,并让源码安装可以直接使用 Studio。 + ### 环境变量 如果需要 LLM 驱动的记忆演化或 embedding 检索,可以配置环境变量。embedding 默认关闭,因此默认配置不会启动 embedding 模型,也不需要 @@ -125,13 +129,6 @@ reme start service.port=8181 # reme start workspace_dir=/tmp/reme-demo service.port=8181 ``` -启动后可以检查服务状态;如果使用了自定义端口,请将下面 URL 中的 `2333` 替换为对应端口。 - -如果安装包中包含 Web 构建产物,HTTP 服务还会在 提供 **ReMe Studio**,用于浏览、编辑和搜索 -workspace,与只读 workspace agent 对话,以及查看 digest wikilink 图。可以设置 -`service.web_enabled=false` 关闭,或通过 `service.web_static_dir` / `REME_WEB_STATIC_DIR` 指定自定义静态目录。找不到 Web -构建产物时,Job API 仍可正常使用。 - ```bash reme version reme health_check @@ -139,31 +136,11 @@ reme help curl -s http://127.0.0.1:2333/version -H 'Content-Type: application/json' -d '{}' ``` -### 使用 ReMe Studio +### ReMe Studio(可选) -启动默认 HTTP 服务后,在浏览器打开 。Studio 提供: - -- **文件、日记和知识库视图**:浏览整个 workspace,或聚焦 `daily/` 和 `digest/`。 -- **Markdown 多标签页**:支持预览、分栏编辑、基于修改时间的冲突检查、保存和本地下载。 -- **记忆图谱**:浏览已索引的 `personal`、`procedure` 和 `wiki` 节点,并打开对应 Markdown 源文件。 -- **只读 Agent 对话**:流式查看工具调用和模型用量;可将 workspace 文件拖入输入框作为引用。 -- **设置与服务管理**:查看服务/组件状态、脱敏后的生效配置和版本,并安全重建派生索引。 -- 支持中英文切换,以及浅色、深色和跟随系统外观。 - -如需开发前端,在两个终端中分别启动 ReMe 和 Studio: - -```bash -# 终端 1:仓库根目录 -reme start - -# 终端 2 -cd website -npm install -npm run dev -``` - -然后打开 。开发服务默认连接 `http://127.0.0.1:2333`;如需连接其他 ReMe HTTP -服务,请设置 `NEXT_PUBLIC_REME_API_URL`。静态构建和前端配置说明见 [ReMe Studio 指南](https://reme.agentscope.io/?doc=studio-zh)。 +上面的 `core` 安装已包含 Studio。启动 ReMe 后,打开 即可浏览、编辑和搜索 workspace。 +如需为基础安装单独添加 Studio,可使用 `pip install "reme-ai[web]"`。源码构建、配置和开发说明见 +[ReMe Studio 指南](https://reme.agentscope.io/?doc=studio-zh)。 ### 5 分钟记忆 Demo @@ -205,7 +182,7 @@ ReMe 会把 Agent 记忆保存为可读的 Markdown。 | 文档 | 主要内容 | |------|----------| -| [快速开始](docs/zh/quick_start.md) | 安装 ReMe、启动服务、使用 Studio,并执行首次文件和记忆操作。 | +| [快速开始](docs/zh/quick_start.md) | 安装 ReMe、启动服务,并执行首次文件和记忆操作。 | | [Memory as File](docs/zh/memory_as_file.md) | 理解 workspace 分层、frontmatter、wikilink、chunk 和文件事实来源模型。 | | [Auto Memory](docs/zh/auto_memory.md) | 保留过滤后的对话来源记录,并提炼可复用的 daily 记忆卡片。 | | [Auto Resource](docs/zh/auto_resource.md) | 导入支持的文本资料,转换为可追溯来源的 daily 卡片。 | @@ -214,8 +191,7 @@ ReMe 会把 Agent 记忆保存为可读的 Markdown。 | [Proactive](docs/zh/proactive.md) | 安全读取兴趣主题,并将其接入宿主 Agent 的决策流程。 | | [Agent 接入场景](docs/zh/reme_scene.md) | 在 CLI/SKILL.md、HTTP、MCP 和嵌入式 Python 集成之间选择。 | | [框架说明](docs/zh/framework.md) | 理解 Application、Job、Step、Component、service、配置和生命周期边界。 | -| [ReMe Studio](https://reme.agentscope.io/?doc=studio-zh) | 使用、配置、开发、测试和构建 Web 前端。 | -| [ReMe 博客](docs/zh/reme-blog.md) | 了解完整产品故事、设计动机、使用示例和评测摘要。 | +| [ReMe 博客](https://agentscope-ai.github.io/ReMe/?doc=zh-reme-blog) | 了解完整产品故事、设计动机、使用示例和评测摘要。 | ## 🧑‍🍳 Cookbooks @@ -231,8 +207,8 @@ cookbook 会继续在表格中按行追加。 > Memory as File, File as Memory. -ReMe 将 **记忆视为文件**,让过滤后的对话来源记录和外部资料从 `session/`、`resource/` 渐进加工到 `daily/`,再沉淀为 `digest/` -中可长期复用的知识节点。默认 workspace 是当前目录下的 `.reme/`;可通过 `workspace_dir=...` 选择其他由用户控制的位置。 +ReMe 将 **记忆视为文件**,让过滤后的对话来源记录和外部资料从 `session/`、`resource/` 渐进加工到 `daily/`,再沉淀为 +`digest/`。默认 workspace 是当前目录下的 `.reme/`;可通过 `workspace_dir=...` 选择其他由用户控制的位置。 ### 目录结构 @@ -272,13 +248,7 @@ ReMe 将 **记忆视为文件**,让过滤后的对话来源记录和外部资 ## 🧭 记忆设计理念 -> 捕获过滤后的对话来源记录和资料,将其整理为长期偏好、可复用经验和有价值的知识,并让结果始终能被用户和 Agent 直接编辑。 - -### 自动记忆流程 - -ReMe 遵循 capture → index → consolidate → recall 的循环。对话和资料先变成 daily 记忆卡片;后台任务保持文件可检索; -`auto_dream` 将稳定知识沉淀到 `digest/`;Agent 再通过搜索、wikilink 或 proactive topics 召回记忆。文件是持久化的事实来源, -`metadata/` 中的索引、图谱、catalog 和缓存都可以由它们重建。 +ReMe 遵循 capture → index → consolidate → recall 的循环。workspace 文件是持久化的事实来源,`metadata/` 中的内容均可重建。 | 能力 | 入口 | 作用 | 输出 | |---------------------------------------------|-------------------------------------------|----------------------------------------------------------------------------------------------|--------------------------------------------------------------| @@ -307,8 +277,7 @@ ReMe 遵循 capture → index → consolidate → recall 的循环。对话和 -搜索会先返回最相关的 chunks、文件路径和行号范围,再以元数据形式列出数量受限的入链与出链邻居。Agent 只需在判断确实相关后再读取原文或继续遍历图谱。 -启用 embedding 时,BM25 和向量排名通过 RRF 融合;默认未启用 embedding 时,则使用 BM25 + wikilink 扩展。 +搜索返回带行号范围的相关 chunks 和数量受限的 wikilink 邻居;可选向量结果通过 RRF 与 BM25 融合。 > [!IMPORTANT] > `proactive` 只读取并暴露 Auto Dream 生成的兴趣主题,不会自行联网、发送通知或改写知识库;是否以及如何使用主题,由宿主 Agent @@ -330,7 +299,7 @@ ReMe 通过 Agent 多轮搜索与读取的方式,评测多会话和超长上 ## 🤝 Agent-friendly Integration ReMe 既可以作为本地记忆服务,通过 CLI、HTTP API 或 MCP server 接入,也可以通过 Python API 嵌入宿主进程。不同 Agent 可以选择适合自身 -runtime 的路径,并按需共享同一个本地 memory workspace。默认 HTTP 服务还可以在同一地址提供 ReMe Studio。 +runtime 的路径。 | Agent | 推荐接入方式 | 接入后能力 | |-----------------------------------------------|-------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------| @@ -369,26 +338,20 @@ runtime 的路径,并按需共享同一个本地 memory workspace。默认 HTT ## 🛠️ ReMe Operations -ReMe 通过 CLI 暴露的统一 job interface 操作 workspace。Agent 通常只需要使用检索、读取、写入、编辑和自动记忆相关命令;更底层的索引、 -frontmatter 和文件操作接口主要用于维护、调试或高级集成。完整 job 列表可以运行 `reme help` 查看。 +运行 `reme help` 可查看完整 job 列表。常用 workspace 与维护命令如下: | 命令 | 作用 | |-------------------------------------------|---------------------------------------------------------------| -| `reme start` | 启动本地 ReMe 服务。 | -| `reme version` / `reme health_check` | 检查包版本和组件状态。 | | `reme status` | 查看有状态数据组件的内存估算及进程 RSS。 | | [`reme search`](docs/zh/memory_search.md) | 默认使用 BM25 和 wikilink 检索,启用后增加向量检索。 | | `reme read` / `reme write` / `reme edit` | 检查和维护 Markdown 记忆文件。 | | `reme traverse` / `reme graph_snapshot` | 浏览 wikilink 邻域或按类别组织的 digest 图。 | | `reme chat` | 与可感知 workspace 的只读 Agent 进行流式对话;需要 LLM 凭证。 | -| `reme auto_memory` | 将对话 messages 转为 daily 记忆卡片;需要 LLM 凭证。 | -| `reme auto_resource` | 将 `resource/` 下的文件解读为 daily 资料卡片;需要 LLM 凭证。 | -| `reme auto_dream` / `reme proactive` | 将 daily 记忆整理为长期 digest,并暴露值得关注的主题。 | | `reme reindex` | 基于已有文件重建检索和 wikilink 索引。 | ## 🤝 社区与支持 -- **问题反馈与需求**:请先查看 [Open Issues](https://github.com/agentscope-ai/ReMe/issues);如无相关讨论,可新建 Issue +- **问题反馈、需求与帮助**:请先查看 [Open Issues](https://github.com/agentscope-ai/ReMe/issues);如无相关讨论,可新建 Issue 说明背景、目标行为和影响范围。 - **代码贡献**:改动前建议阅读 [贡献指南](https://docs.agentscope.io/reme/latest/zh/contribution)。架构与扩展方式以源码、schema 和测试为准。 @@ -397,8 +360,7 @@ frontmatter 和文件操作接口主要用于维护、调试或高级集成。 `docs(zh): update quick start`。 - **提交前检查**:提交 PR 前请尽量运行 `pre-commit run --all-files` 和 `pytest`;如有依赖 LLM、embedding 或外部服务的测试无法运行,请在 PR 中说明。 -- **获取帮助**:如需反馈 Bug 或功能请求,请使用 [GitHub Issues](https://github.com/agentscope-ai/ReMe/issues);项目文档见 - [https://reme.agentscope.io](https://reme.agentscope.io)。 +- **项目文档**:访问 [reme.agentscope.io](https://reme.agentscope.io)。 ### 贡献者 diff --git a/cookbook/daily_paper/README.md b/cookbook/daily_paper/README.md index 25444f29..1652cc8f 100644 --- a/cookbook/daily_paper/README.md +++ b/cookbook/daily_paper/README.md @@ -182,8 +182,8 @@ when present. The two data sources reach a mirror differently: Hugging Face is g parameter, while arXiv is driven by its environment variable alone. ```dotenv -# Enable the mirror for the built-in daily_paper_cron job -DAILY_PAPER_USE_HF_MIRROR=true +# The built-in daily_paper_cron job enables the mirror by default; set false to use the official service +DAILY_PAPER_USE_HF_MIRROR=false # Read only when the manual or scheduled job enables the mirror; defaults to https://hf-mirror.com when unset HF_MIRROR_URL=https://hf-mirror.com @@ -202,8 +202,8 @@ trailing slash is optional. There is no fallback chain: whichever base URL a cli > **Behavior change:** `HF_MIRROR_URL` used to redirect Hugging Face traffic on its own. It is now read only when the > job runs with `use_hf_mirror=true`; otherwise the official service is used and the client logs a warning that the -> variable was ignored. Pass `use_hf_mirror=true` for manual requests, or set -> `DAILY_PAPER_USE_HF_MIRROR=true` for `daily_paper_cron`, to keep an existing mirror-only setup working. +> variable was ignored. Pass `use_hf_mirror=true` for manual requests. The built-in `daily_paper_cron` job enables the +> mirror by default; set `DAILY_PAPER_USE_HF_MIRROR=false` to make that scheduled job use the official service. ## Running the workflow @@ -231,8 +231,9 @@ reme start config=daily_cookbook ``` The built-in service listens on `127.0.0.1:8001`. `daily_paper_cron` runs every day at 08:00 in the -`Asia/Shanghai` timezone. Set `DAILY_PAPER_USE_HF_MIRROR=true` to make that scheduled job use the Hugging Face mirror. -Override the bind address with `DAILY_PAPER_HOST`, `DAILY_PAPER_PORT`, or startup arguments. +`Asia/Shanghai` timezone, prioritizes the topic `大模型长期记忆`, and uses the Hugging Face mirror by default. Set +`DAILY_PAPER_USE_HF_MIRROR=false` to use the official service. Override the bind address with `DAILY_PAPER_HOST`, +`DAILY_PAPER_PORT`, or startup arguments. ```bash curl -s http://127.0.0.1:8001/daily_paper \ diff --git a/cookbook/daily_paper/README_ZH.md b/cookbook/daily_paper/README_ZH.md index d6b9e0d6..c5af96e8 100644 --- a/cookbook/daily_paper/README_ZH.md +++ b/cookbook/daily_paper/README_ZH.md @@ -172,8 +172,8 @@ reme_workspace/ Face 由 `use_hf_mirror` 任务参数控制,arXiv 仅由环境变量驱动。 ```dotenv -# 为内置 daily_paper_cron 定时任务启用镜像站 -DAILY_PAPER_USE_HF_MIRROR=true +# 内置 daily_paper_cron 定时任务默认启用镜像站;设为 false 可改用官方服务 +DAILY_PAPER_USE_HF_MIRROR=false # 仅在手动任务或定时任务启用镜像时读取;未配置时使用 https://hf-mirror.com HF_MIRROR_URL=https://hf-mirror.com @@ -192,7 +192,8 @@ URL,就只访问该地址。 > **行为变更:** 以往只要设置 `HF_MIRROR_URL` 就会改变 Hugging Face > 的访问地址;现在该变量仅在任务启用镜像时才会读取,否则直接访问官方站点,并输出一条“已忽略该变量”的告警日志。手动调用需传入 -> `use_hf_mirror=true`,`daily_paper_cron` 定时任务需设置 `DAILY_PAPER_USE_HF_MIRROR=true`,才能继续走镜像。 +> `use_hf_mirror=true`。内置 `daily_paper_cron` 定时任务默认启用镜像;设置 `DAILY_PAPER_USE_HF_MIRROR=false` +> 可让该定时任务改用官方服务。 ## 运行方式 @@ -219,9 +220,9 @@ reme start config=daily_cookbook job=daily_paper date=2026-08-06 force=true reme start config=daily_cookbook ``` -内置服务监听 `127.0.0.1:8001`,`daily_paper_cron` 按 `Asia/Shanghai` 时区每天 08:00 运行。设置 -`DAILY_PAPER_USE_HF_MIRROR=true` 可让该定时任务使用 Hugging Face 镜像站。可通过 `DAILY_PAPER_HOST`、`DAILY_PAPER_PORT` -或启动参数覆盖监听地址和端口。 +内置服务监听 `127.0.0.1:8001`,`daily_paper_cron` 按 `Asia/Shanghai` 时区每天 08:00 运行,默认优先关注 +`大模型长期记忆`,并使用 Hugging Face 镜像站。设置 `DAILY_PAPER_USE_HF_MIRROR=false` 可改用官方服务。可通过 +`DAILY_PAPER_HOST`、`DAILY_PAPER_PORT` 或启动参数覆盖监听地址和端口。 ```bash curl -s http://127.0.0.1:8001/daily_paper \ diff --git a/docs/en/contributing.md b/docs/en/contributing.md index 44c8be25..e9fe9c3e 100644 --- a/docs/en/contributing.md +++ b/docs/en/contributing.md @@ -40,6 +40,10 @@ The project requires Python 3.11 or later. A virtual environment is recommended: python -m venv .venv source .venv/bin/activate pip install -e packages/reme_ai_studio -e ".[dev,full]" +cd website +npm ci +npm run build:static +cd .. pre-commit install ``` diff --git a/docs/en/quick_start.md b/docs/en/quick_start.md index d2791d89..bd73f91a 100644 --- a/docs/en/quick_start.md +++ b/docs/en/quick_start.md @@ -16,8 +16,14 @@ Install from source: git clone https://github.com/agentscope-ai/ReMe.git cd ReMe pip install -e packages/reme_ai_studio -e ".[core]" +cd website +npm ci +npm run build:static +cd .. ``` +The static build step requires Node.js 22.13 or newer and makes Studio available when running ReMe from the source tree. + Installing the `core` extra is recommended. The current code imports the AgentScope wrapper, and self-evolving memory also depends on it. diff --git a/docs/zh/contributing.md b/docs/zh/contributing.md index 0e134e66..14271abf 100644 --- a/docs/zh/contributing.md +++ b/docs/zh/contributing.md @@ -38,6 +38,10 @@ ReMe 的核心代码位于: python -m venv .venv source .venv/bin/activate pip install -e packages/reme_ai_studio -e ".[dev,full]" +cd website +npm ci +npm run build:static +cd .. pre-commit install ``` diff --git a/docs/zh/quick_start.md b/docs/zh/quick_start.md index e00688e1..b3059866 100644 --- a/docs/zh/quick_start.md +++ b/docs/zh/quick_start.md @@ -16,8 +16,14 @@ pip install "reme-ai[core]" git clone https://github.com/agentscope-ai/ReMe.git cd ReMe pip install -e packages/reme_ai_studio -e ".[core]" +cd website +npm ci +npm run build:static +cd .. ``` +静态构建步骤需要 Node.js 22.13 或更高版本,用于在从源码运行 ReMe 时提供 Studio。 + `core` extra 建议安装:当前代码会导入 AgentScope wrapper,自进化记忆也依赖它。 如果要使用 `auto_memory`、`auto_resource`、`auto_dream` 这类 Agent 流程,再配置 LLM: diff --git a/github-pages/package.json b/github-pages/package.json index e3f19c24..90487446 100644 --- a/github-pages/package.json +++ b/github-pages/package.json @@ -9,7 +9,8 @@ "scripts": { "dev": "node scripts/generate-content.mjs && vite", "build": "node scripts/generate-content.mjs && vite build", - "preview": "vite preview" + "preview": "vite preview", + "test": "node --test tests/*.test.mjs" }, "dependencies": { "dompurify": "^3.2.6", diff --git a/github-pages/scripts/generate-content.mjs b/github-pages/scripts/generate-content.mjs index e6957ada..4a3130d6 100644 --- a/github-pages/scripts/generate-content.mjs +++ b/github-pages/scripts/generate-content.mjs @@ -228,7 +228,7 @@ for (const product of productDocuments) { } } await mkdir(path.join(outputDir, "website", "public"), { recursive: true }); -await cp(path.join(repoDir, "website", "public", "og.png"), path.join(outputDir, "website", "public", "og.png")); +await cp(path.join(repoDir, "website", "public", "og.jpg"), path.join(outputDir, "website", "public", "og.jpg")); await mkdir(path.join(outputDir, "skills", "reme_memory"), { recursive: true }); await cp( path.join(repoDir, "skills", "reme_memory", "SKILL.md"), diff --git a/github-pages/src/main.js b/github-pages/src/main.js index 992b0f83..f3438cdc 100644 --- a/github-pages/src/main.js +++ b/github-pages/src/main.js @@ -1,5 +1,6 @@ import DOMPurify from "dompurify"; import { marked } from "marked"; +import { stripMarkdownFrontmatter } from "./markdown.js"; import "./styles.css"; const baseUrl = import.meta.env.BASE_URL; @@ -370,7 +371,7 @@ async function openDocument(id, pushHistory = true) { const response = await fetch(`${baseUrl}content/${document.path}`); if (!response.ok) throw new Error(`Unable to load ${document.path}`); configureMarkdown(document); - const markdown = await response.text(); + const markdown = stripMarkdownFrontmatter(await response.text()); const body = DOMPurify.sanitize(await marked.parse(markdown), { ADD_ATTR: ["target"], }); diff --git a/github-pages/src/markdown.js b/github-pages/src/markdown.js new file mode 100644 index 00000000..ef81a773 --- /dev/null +++ b/github-pages/src/markdown.js @@ -0,0 +1,6 @@ +const FRONTMATTER_PATTERN = /^\uFEFF?---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/; + +/** Remove a leading YAML frontmatter block before rendering Markdown. */ +export function stripMarkdownFrontmatter(markdown) { + return markdown.replace(FRONTMATTER_PATTERN, ""); +} diff --git a/github-pages/src/styles.css b/github-pages/src/styles.css index 803b463f..7b3ee4ad 100644 --- a/github-pages/src/styles.css +++ b/github-pages/src/styles.css @@ -91,8 +91,8 @@ a { color: inherit; } .article[data-group="home"] { max-width: 980px; } .home-hero { padding: 34px 0 76px; } .home-eyebrow, .section-kicker { margin: 0 0 17px; color: #12806d; font: 750 11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.13em; } -.home-hero h1 { max-width: 800px; margin: 0; color: #102019; font-size: clamp(46px, 6.3vw, 76px); line-height: 0.99; letter-spacing: -0.055em; } -.home-lead { max-width: 720px; margin: 27px 0 0; color: #526159; font-size: 18px; line-height: 1.72; } +.home-hero h1 { margin: 0; color: #102019; font-size: clamp(46px, 6.3vw, 76px); line-height: 1.15; letter-spacing: -0.055em; } +.home-lead { margin: 27px 0 0; color: #526159; font-size: 18px; line-height: 1.72; } .home-actions { display: flex; flex-wrap: wrap; gap: 11px; margin-top: 31px; } .home-actions a { padding: 11px 17px; border-radius: 10px; text-decoration: none; font-size: 14px; font-weight: 720; } .primary-action { color: white; background: #087f6a; box-shadow: 0 8px 22px rgba(8, 127, 106, 0.2); } diff --git a/github-pages/tests/markdown.test.mjs b/github-pages/tests/markdown.test.mjs new file mode 100644 index 00000000..129b6190 --- /dev/null +++ b/github-pages/tests/markdown.test.mjs @@ -0,0 +1,15 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { stripMarkdownFrontmatter } from "../src/markdown.js"; + +test("strips leading YAML frontmatter before rendering", () => { + assert.equal( + stripMarkdownFrontmatter("---\nname: reme_memory\ndescription: Memory skill\n---\n\n# ReMe Memory\n"), + "\n# ReMe Memory\n", + ); +}); + +test("preserves Markdown without frontmatter", () => { + const markdown = "# ReMe Memory\n\nContent\n"; + assert.equal(stripMarkdownFrontmatter(markdown), markdown); +}); diff --git a/packages/reme_ai_studio/.gitignore b/packages/reme_ai_studio/.gitignore new file mode 100644 index 00000000..9dc7e3d4 --- /dev/null +++ b/packages/reme_ai_studio/.gitignore @@ -0,0 +1 @@ +/LICENSE diff --git a/packages/reme_ai_studio/pyproject.toml b/packages/reme_ai_studio/pyproject.toml index 8fe88424..eabd6d38 100644 --- a/packages/reme_ai_studio/pyproject.toml +++ b/packages/reme_ai_studio/pyproject.toml @@ -1,9 +1,10 @@ [project] name = "reme-ai-studio" -version = "0.4.1.6" +version = "0.4.1.7" description = "Optional ReMe Studio static frontend." readme = "README.md" license = "Apache-2.0" +license-files = ["LICENSE"] requires-python = ">=3.11" [project.urls] @@ -20,5 +21,5 @@ include-package-data = false "reme_ai_studio" = ["static/**"] [build-system] -requires = ["setuptools>=45", "wheel"] +requires = ["setuptools>=77", "wheel"] build-backend = "setuptools.build_meta" diff --git a/pyproject.toml b/pyproject.toml index f2c24e0e..99f0419b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dependencies = [ [project.optional-dependencies] web = [ - "reme-ai-studio==0.4.1.6", + "reme-ai-studio==0.4.1.7", ] core = [ "agentscope==2.0.4.post1", @@ -58,9 +58,10 @@ core = [ "neo4j>=6.2.0", "networkx>=3.4.2", "polars>=1.43.0", - "reme-ai-studio==0.4.1.6", + "reme-ai-studio==0.4.1.7", ] dev = [ + "packaging>=24.2", "pre-commit>=4.6.1", "pytest>=9.1.1", "pytest-asyncio>=0.23", diff --git a/reme/__init__.py b/reme/__init__.py index 4937bd91..dea7ece7 100644 --- a/reme/__init__.py +++ b/reme/__init__.py @@ -1,6 +1,6 @@ """ReMe CLI package.""" -__version__ = "0.4.1.6" +__version__ = "0.4.1.7" from . import config from . import constants diff --git a/reme/config/daily_cookbook.yaml b/reme/config/daily_cookbook.yaml index d2f7d6f4..953024ca 100644 --- a/reme/config/daily_cookbook.yaml +++ b/reme/config/daily_cookbook.yaml @@ -441,7 +441,8 @@ jobs: daily_paper_cron: backend: cron cron: "0 8 * * *" - use_hf_mirror: ${DAILY_PAPER_USE_HF_MIRROR:-false} + topics: "大模型长期记忆" + use_hf_mirror: ${DAILY_PAPER_USE_HF_MIRROR:-true} candidate_limit: *candidate_limit rrf_k: *rrf_k weekly_weight: *weekly_weight diff --git a/scripts/bump_version.py b/scripts/bump_version.py new file mode 100644 index 00000000..587ca90b --- /dev/null +++ b/scripts/bump_version.py @@ -0,0 +1,205 @@ +"""Validate or update the ReMe and ReMe Studio release version.""" + +from __future__ import annotations + +import argparse +import os +import re +import tempfile +import tomllib +from pathlib import Path + +from packaging.version import InvalidVersion, Version + +REPOSITORY_DIR = Path(__file__).resolve().parents[1] + +_VERSION_PATTERN = re.compile(r'(?m)^__version__ = "(?P[^"]+)"$') +_STUDIO_VERSION_PATTERN = re.compile(r'(?m)^version = "(?P[^"]+)"$') + + +def _paths(repository: Path) -> tuple[Path, Path, Path]: + return ( + repository / "reme" / "__init__.py", + repository / "pyproject.toml", + repository / "packages" / "reme_ai_studio" / "pyproject.toml", + ) + + +def _canonical_version(value: str) -> str: + """Return one canonical PEP 440 version or explain how to correct it.""" + try: + canonical = str(Version(value)) + except InvalidVersion as error: + raise ValueError( + f"Invalid PEP 440 version {value!r}. Use a version such as 0.4.1.8, then rerun this command.", + ) from error + if canonical != value: + raise ValueError(f"Version {value!r} is not canonical PEP 440; use {canonical!r} instead.") + return canonical + + +def _read_toml(source: Path) -> dict: + """Read one TOML file or identify the file that needs correction.""" + try: + return tomllib.loads(source.read_text(encoding="utf-8")) + except (OSError, UnicodeError, tomllib.TOMLDecodeError) as error: + raise ValueError(f"{source}: cannot read valid TOML ({error}). Fix this file and retry.") from error + + +def _required_table(config: dict, keys: tuple[str, ...], source: Path) -> dict: + """Return a required TOML table or explain where to add it.""" + value: object = config + for key in keys: + if not isinstance(value, dict) or key not in value: + table = ".".join(keys) + raise ValueError(f"{source}: missing or invalid [{table}] table. Add or fix this table and retry.") + value = value[key] + if not isinstance(value, dict): + table = ".".join(keys) + raise ValueError(f"{source}: [{table}] must be a TOML table. Fix this table and retry.") + return value + + +def _match_version(text: str, pattern: re.Pattern[str], source: Path) -> str: + matches = list(pattern.finditer(text)) + if len(matches) != 1: + raise ValueError(f"Expected exactly one version declaration in {source}, found {len(matches)}.") + return matches[0].group("version") + + +def _replace_version(text: str, pattern: re.Pattern[str], version: str, source: Path) -> str: + _match_version(text, pattern, source) + return pattern.sub(lambda match: match.group(0).replace(match.group("version"), version), text) + + +def _write_atomic(path: Path, content: str) -> None: + """Replace one text file without exposing a partially written file.""" + temporary_name: str | None = None + try: + with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as temporary: + temporary.write(content) + temporary_name = temporary.name + os.chmod(temporary_name, path.stat().st_mode) + os.replace(temporary_name, path) + finally: + if temporary_name is not None and os.path.exists(temporary_name): + os.unlink(temporary_name) + + +def read_version(repository: Path = REPOSITORY_DIR) -> str: + """Read and validate the main distribution version.""" + version_file, _, _ = _paths(repository) + version = _match_version(version_file.read_text(encoding="utf-8"), _VERSION_PATTERN, version_file) + try: + return _canonical_version(version) + except ValueError as error: + raise ValueError(f"{version_file}: {error} Fix the __version__ declaration and retry.") from error + + +def check_versions(repository: Path = REPOSITORY_DIR, expected_version: str | None = None) -> str: + """Validate both distributions and their exact optional-dependency pins.""" + version_file, main_config_file, studio_config_file = _paths(repository) + version = read_version(repository) + main_config = _read_toml(main_config_file) + studio_config = _read_toml(studio_config_file) + dependency = f"reme-ai-studio=={version}" + optional_dependencies = _required_table(main_config, ("project", "optional-dependencies"), main_config_file) + studio_project = _required_table(studio_config, ("project",), studio_config_file) + problems: list[str] = [] + + studio_version = studio_project.get("version") + if studio_version != version: + problems.append(f"{studio_config_file}: project.version is {studio_version!r}; expected {version!r}") + if optional_dependencies.get("web") != [dependency]: + problems.append(f"{main_config_file}: web must be exactly [{dependency!r}]") + core_dependencies = optional_dependencies.get("core") + if not isinstance(core_dependencies, list) or core_dependencies.count(dependency) != 1: + problems.append(f"{main_config_file}: core must contain {dependency!r} exactly once") + + expected = None + if expected_version is not None: + expected = _canonical_version(expected_version.removeprefix("v")) + if version != expected: + problems.append(f"{version_file}: package version is {version!r}; release expects {expected!r}") + + if problems: + details = "\n".join(f"- {problem}" for problem in problems) + raise ValueError( + "Version metadata is inconsistent:\n" + f"{details}\n" + f"Fix the listed values, or run `python scripts/bump_version.py {expected or version}` " + "after restoring a consistent current version.", + ) + return version + + +def bump_version(version: str, repository: Path = REPOSITORY_DIR) -> str: + """Update every release version, rolling back if any write or validation fails.""" + version = _canonical_version(version) + current_version = check_versions(repository) + version_file, main_config_file, studio_config_file = _paths(repository) + paths = (version_file, main_config_file, studio_config_file) + original = {path: path.read_text(encoding="utf-8") for path in paths} + current_dependency = f"reme-ai-studio=={current_version}" + + main_text = original[main_config_file] + if main_text.count(current_dependency) != 2: + raise ValueError(f"Expected exactly two {current_dependency!r} pins in {main_config_file}.") + updated = { + version_file: _replace_version(original[version_file], _VERSION_PATTERN, version, version_file), + main_config_file: main_text.replace(current_dependency, f"reme-ai-studio=={version}"), + studio_config_file: _replace_version( + original[studio_config_file], + _STUDIO_VERSION_PATTERN, + version, + studio_config_file, + ), + } + + written: list[Path] = [] + try: + for path in paths: + _write_atomic(path, updated[path]) + written.append(path) + check_versions(repository, version) + except BaseException as error: + rollback_errors: list[str] = [] + for path in reversed(written): + try: + _write_atomic(path, original[path]) + except OSError as rollback_error: + rollback_errors.append(f"{path}: {rollback_error}") + if rollback_errors: + raise RuntimeError( + "Version update failed and rollback was incomplete. Restore these files from Git:\n- " + + "\n- ".join(rollback_errors), + ) from error + raise RuntimeError( + "Version update failed; all changed files were restored. Fix the error and retry.", + ) from error + return current_version + + +def main() -> None: + """Run the version validation or update command.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("version", nargs="?", help="New version for both distributions, for example 0.4.1.8") + parser.add_argument("--check", action="store_true", help="Validate versions without changing files") + parser.add_argument("--expected-version", help="Also require this release/tag version; a leading v is accepted") + args = parser.parse_args() + if args.check: + if args.version: + parser.error("version cannot be used with --check; use --expected-version") + version = check_versions(expected_version=args.expected_version) + print(f"Release versions are consistent: {version}") + return + if args.expected_version: + parser.error("--expected-version requires --check") + if not args.version: + parser.error("provide a version to update, or use --check") + previous_version = bump_version(args.version) + print(f"Updated ReMe and ReMe Studio from {previous_version} to {args.version}") + + +if __name__ == "__main__": + main() diff --git a/scripts/package_studio.py b/scripts/package_studio.py index 66ed4ca5..96396bc9 100644 --- a/scripts/package_studio.py +++ b/scripts/package_studio.py @@ -10,6 +10,8 @@ REPOSITORY_DIR = Path(__file__).resolve().parents[1] WEBSITE_DIR = REPOSITORY_DIR / "website" PACKAGE_DIR = REPOSITORY_DIR / "packages" / "reme_ai_studio" STATIC_DIR = PACKAGE_DIR / "src" / "reme_ai_studio" / "static" +LICENSE_FILE = REPOSITORY_DIR / "LICENSE" +STATIC_GITIGNORE = "*\n!.gitignore\n" _RAW_WEBSITE_URL = "https://raw.githubusercontent.com/agentscope-ai/ReMe/main/website" _REPOSITORY_URL = "https://github.com/agentscope-ai/ReMe" @@ -35,15 +37,20 @@ def build_readme() -> str: def prepare_package(*, copy_static: bool = True) -> None: - """Generate the PyPI README and optionally stage the static build.""" + """Generate package metadata files and optionally stage the static build.""" (PACKAGE_DIR / "README.md").write_text(build_readme(), encoding="utf-8") + shutil.copyfile(LICENSE_FILE, PACKAGE_DIR / "LICENSE") if not copy_static: return source = WEBSITE_DIR / "dist-static" if not (source / "index.html").is_file(): raise FileNotFoundError(f"Studio static build is unavailable: {source}") - shutil.rmtree(STATIC_DIR, ignore_errors=True) - shutil.copytree(source, STATIC_DIR) + try: + shutil.rmtree(STATIC_DIR, ignore_errors=True) + shutil.copytree(source, STATIC_DIR) + finally: + STATIC_DIR.mkdir(parents=True, exist_ok=True) + (STATIC_DIR / ".gitignore").write_text(STATIC_GITIGNORE, encoding="utf-8") def main() -> None: diff --git a/tests/unit/test_daily_paper.py b/tests/unit/test_daily_paper.py index 182a02d7..e0a027a3 100644 --- a/tests/unit/test_daily_paper.py +++ b/tests/unit/test_daily_paper.py @@ -503,6 +503,11 @@ def test_daily_paper_topics_parameter_defaults_to_empty(): } +def test_daily_paper_cron_prioritizes_long_term_llm_memory(): + """The scheduled workflow prioritizes papers about long-term LLM memory.""" + assert _load_config("daily_cookbook")["jobs"]["daily_paper_cron"]["topics"] == "大模型长期记忆" + + def test_daily_paper_hf_mirror_parameter_defaults_to_disabled(): """The public job schema exposes an explicit Hugging Face mirror switch.""" use_hf_mirror = _load_config("daily_cookbook")["jobs"]["daily_paper"]["parameters"]["properties"]["use_hf_mirror"] @@ -514,14 +519,14 @@ def test_daily_paper_hf_mirror_parameter_defaults_to_disabled(): } -def test_daily_paper_cron_hf_mirror_uses_explicit_environment_switch(monkeypatch): - """The scheduled workflow can opt in to the Hugging Face mirror.""" +def test_daily_paper_cron_hf_mirror_defaults_enabled_with_environment_override(monkeypatch): + """The scheduled workflow uses the mirror by default and supports an explicit override.""" monkeypatch.delenv("DAILY_PAPER_USE_HF_MIRROR", raising=False) - assert _load_config("daily_cookbook")["jobs"]["daily_paper_cron"]["use_hf_mirror"] is False - - monkeypatch.setenv("DAILY_PAPER_USE_HF_MIRROR", "true") assert _load_config("daily_cookbook")["jobs"]["daily_paper_cron"]["use_hf_mirror"] is True + monkeypatch.setenv("DAILY_PAPER_USE_HF_MIRROR", "false") + assert _load_config("daily_cookbook")["jobs"]["daily_paper_cron"]["use_hf_mirror"] is False + def test_paper_pick_list_uses_an_object_root_for_tool_output(): """AgentScope function arguments require an object-root JSON schema.""" diff --git a/tests/unit/test_package_versions.py b/tests/unit/test_package_versions.py index fb5b9b94..5b69038d 100644 --- a/tests/unit/test_package_versions.py +++ b/tests/unit/test_package_versions.py @@ -1,28 +1,204 @@ """Keep the separately published distributions version-compatible.""" +import importlib.util from pathlib import Path import tomllib +from types import ModuleType -import reme -from scripts.package_studio import build_readme +import pytest + +REPOSITORY = Path(__file__).resolve().parents[2] + + +def _load_script(name: str) -> ModuleType: + script = REPOSITORY / "scripts" / f"{name}.py" + spec = importlib.util.spec_from_file_location(name, script) + if spec is None or spec.loader is None: + raise ImportError(f"Unable to load {script}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +bump_version = _load_script("bump_version") +package_studio = _load_script("package_studio") def test_studio_package_and_extra_versions_match_reme() -> None: """Require one version bump to update both wheels and their exact pin.""" - repository = Path(__file__).resolve().parents[2] - main_config = tomllib.loads((repository / "pyproject.toml").read_text(encoding="utf-8")) + main_config = tomllib.loads((REPOSITORY / "pyproject.toml").read_text(encoding="utf-8")) studio_config = tomllib.loads( - (repository / "packages" / "reme_ai_studio" / "pyproject.toml").read_text(encoding="utf-8"), + (REPOSITORY / "packages" / "reme_ai_studio" / "pyproject.toml").read_text(encoding="utf-8"), ) - expected_dependency = f"reme-ai-studio=={reme.__version__}" + version = bump_version.read_version(REPOSITORY) + expected_dependency = f"reme-ai-studio=={version}" - assert studio_config["project"]["version"] == reme.__version__ + assert studio_config["project"]["version"] == version assert main_config["project"]["optional-dependencies"]["web"] == [expected_dependency] assert expected_dependency in main_config["project"]["optional-dependencies"]["core"] +def _write_version_fixture(repository: Path, *, studio_version: str = "1.2.3") -> None: + (repository / "reme").mkdir() + (repository / "packages" / "reme_ai_studio").mkdir(parents=True) + (repository / "reme" / "__init__.py").write_text('__version__ = "1.2.3"\n', encoding="utf-8") + (repository / "pyproject.toml").write_text( + """[project] +name = "reme-ai" + +[project.optional-dependencies] +web = ["reme-ai-studio==1.2.3"] +core = ["example", "reme-ai-studio==1.2.3"] +""", + encoding="utf-8", + ) + (repository / "packages" / "reme_ai_studio" / "pyproject.toml").write_text( + f'[project]\nname = "reme-ai-studio"\nversion = "{studio_version}"\n', + encoding="utf-8", + ) + + +def test_bump_version_updates_both_packages_and_exact_pins(tmp_path: Path) -> None: + """Update the two package versions and both dependency declarations together.""" + _write_version_fixture(tmp_path) + + previous_version = bump_version.bump_version("1.2.4", tmp_path) + + assert previous_version == "1.2.3" + assert (tmp_path / "reme" / "__init__.py").read_text(encoding="utf-8") == '__version__ = "1.2.4"\n' + assert (tmp_path / "pyproject.toml").read_text(encoding="utf-8").count("reme-ai-studio==1.2.4") == 2 + studio_config = tomllib.loads( + (tmp_path / "packages" / "reme_ai_studio" / "pyproject.toml").read_text(encoding="utf-8"), + ) + assert studio_config["project"]["version"] == "1.2.4" + + +def test_bump_version_rejects_inconsistent_sources_before_writing( + tmp_path: Path, +) -> None: + """Refuse to update any file if the current release metadata has drifted.""" + _write_version_fixture(tmp_path, studio_version="1.2.2") + version_file = tmp_path / "reme" / "__init__.py" + original_version_text = version_file.read_text(encoding="utf-8") + + with pytest.raises(ValueError, match=r"project.version is '1.2.2'; expected '1.2.3'"): + bump_version.bump_version("1.2.4", tmp_path) + + assert version_file.read_text(encoding="utf-8") == original_version_text + + +@pytest.mark.parametrize("version", ["release", "1..2", "1.2.3_"]) +def test_bump_version_rejects_invalid_pep440_versions(tmp_path: Path, version: str) -> None: + """Reject invalid release versions before changing any source file.""" + _write_version_fixture(tmp_path) + version_file = tmp_path / "reme" / "__init__.py" + original_version_text = version_file.read_text(encoding="utf-8") + + with pytest.raises(ValueError, match="Invalid PEP 440 version"): + bump_version.bump_version(version, tmp_path) + + assert version_file.read_text(encoding="utf-8") == original_version_text + + +def test_check_versions_reports_release_tag_mismatch(tmp_path: Path) -> None: + """Explain which source must change when a release tag does not match.""" + _write_version_fixture(tmp_path) + + with pytest.raises(ValueError, match=r"package version is '1.2.3'; release expects '1.2.4'"): + bump_version.check_versions(tmp_path, "v1.2.4") + + +def test_read_version_identifies_invalid_source_file(tmp_path: Path) -> None: + """Point maintainers to the invalid Python version declaration.""" + _write_version_fixture(tmp_path) + version_file = tmp_path / "reme" / "__init__.py" + version_file.write_text('__version__ = "1..2"\n', encoding="utf-8") + + with pytest.raises(ValueError, match=rf"{version_file}.*Fix the __version__ declaration"): + bump_version.check_versions(tmp_path) + + +def test_check_versions_identifies_invalid_toml_file(tmp_path: Path) -> None: + """Point maintainers to malformed package metadata.""" + _write_version_fixture(tmp_path) + config_file = tmp_path / "pyproject.toml" + config_file.write_text("[project\n", encoding="utf-8") + + with pytest.raises(ValueError, match=rf"{config_file}.*cannot read valid TOML.*Fix this file"): + bump_version.check_versions(tmp_path) + + +def test_check_versions_identifies_missing_table(tmp_path: Path) -> None: + """Explain which required TOML table must be restored.""" + _write_version_fixture(tmp_path) + config_file = tmp_path / "pyproject.toml" + config_file.write_text('[project]\nname = "reme-ai"\n', encoding="utf-8") + + with pytest.raises(ValueError, match=rf"{config_file}.*\[project.optional-dependencies\].*Add or fix"): + bump_version.check_versions(tmp_path) + + +def test_bump_version_rolls_back_when_a_write_fails(monkeypatch, tmp_path: Path) -> None: + """Restore earlier files when a later atomic replacement fails.""" + # pylint: disable=protected-access + _write_version_fixture(tmp_path) + paths = bump_version._paths(tmp_path) + original = {path: path.read_text(encoding="utf-8") for path in paths} + write_atomic = bump_version._write_atomic + attempts = 0 + + def fail_second_write(path: Path, content: str) -> None: + nonlocal attempts + attempts += 1 + if attempts == 2: + raise OSError("simulated write failure") + write_atomic(path, content) + + monkeypatch.setattr(bump_version, "_write_atomic", fail_second_write) + + with pytest.raises(RuntimeError, match="all changed files were restored"): + bump_version.bump_version("1.2.4", tmp_path) + + assert {path: path.read_text(encoding="utf-8") for path in paths} == original + + def test_studio_readme_is_generated_from_website_docs() -> None: """Keep the packaged PyPI description synchronized with the source docs.""" - repository = Path(__file__).resolve().parents[2] - packaged_readme = repository / "packages" / "reme_ai_studio" / "README.md" - assert packaged_readme.read_text(encoding="utf-8") == build_readme() + packaged_readme = REPOSITORY / "packages" / "reme_ai_studio" / "README.md" + assert packaged_readme.read_text(encoding="utf-8") == package_studio.build_readme() + + +def test_studio_package_preparation_copies_license(monkeypatch, tmp_path: Path) -> None: + """Include the repository license in the independently distributed Studio package.""" + package_dir = tmp_path / "reme_ai_studio" + package_dir.mkdir() + monkeypatch.setattr(package_studio, "PACKAGE_DIR", package_dir) + + package_studio.prepare_package(copy_static=False) + + assert (package_dir / "LICENSE").read_text(encoding="utf-8") == (REPOSITORY / "LICENSE").read_text( + encoding="utf-8", + ) + + +def test_studio_package_preparation_preserves_static_gitignore(monkeypatch, tmp_path: Path) -> None: + """Keep generated static assets ignored after staging the Studio build.""" + package_dir = tmp_path / "reme_ai_studio" + package_dir.mkdir() + website_dir = tmp_path / "website" + source_dir = website_dir / "dist-static" + source_dir.mkdir(parents=True) + (source_dir / "index.html").write_text("", encoding="utf-8") + static_dir = package_dir / "src" / "reme_ai_studio" / "static" + static_dir.mkdir(parents=True) + (static_dir / ".gitignore").write_text(package_studio.STATIC_GITIGNORE, encoding="utf-8") + + monkeypatch.setattr(package_studio, "PACKAGE_DIR", package_dir) + monkeypatch.setattr(package_studio, "WEBSITE_DIR", website_dir) + monkeypatch.setattr(package_studio, "STATIC_DIR", static_dir) + monkeypatch.setattr(package_studio, "build_readme", lambda: "# ReMe Studio\n") + + package_studio.prepare_package() + + assert (static_dir / "index.html").is_file() + assert (static_dir / ".gitignore").read_text(encoding="utf-8") == package_studio.STATIC_GITIGNORE