[
  {
    "path": ".gemini/styleguide.md",
    "content": "# AgentScope Code Review Guide\n\nYou should conduct a strict code review. Each requirement is labeled with priority:\n- **[MUST]** must be satisfied or PR will be rejected\n- **[SHOULD]** strongly recommended\n- **[MAY]** optional suggestion\n\n## 1. Code Quality\n\n### [MUST] Lazy Loading\n- Third-party library dependencies should be imported at the point of use, avoid centralized imports at file top\n  - The `Third-party library` refers to libraries not included in the `dependencies` variable in `pyproject.toml`.\n- For base class imports, use factory pattern:\n```python\ndef get_xxx_cls() -> \"MyClass\":\n    from xxx import BaseClass\n    class MyClass(BaseClass): ...\n    return MyClass\n```\n\n### [SHOULD] Code Conciseness\nAfter understanding the code intent, check if it can be optimized:\n- Avoid unnecessary temporary variables\n- Merge duplicate code blocks\n- Prioritize reusing existing utility functions\n\n### [MUST] Encapsulation Standards\n- All Python files under `src/agentscope` should be named with `_` prefix, and exposure controlled through `__init__.py`\n- Classes and functions used internally by the framework that don't need to be exposed to users must be named with `_` prefix\n\n## 2. [MUST] Code Security\n- Prohibit hardcoding API keys/tokens/passwords\n- Use environment variables or configuration files for management\n- Check for debug information and temporary credentials\n- Check for injection attack risks (SQL/command/code injection, etc.)\n\n## 3. [MUST] Testing & Dependencies\n- New features must include unit tests\n- New dependencies need to be added to the corresponding section in `pyproject.toml`\n- Dependencies for non-core scenarios should not be added to the minimal dependency list\n\n## 4. Code Standards\n\n### [MUST] Comment Standards\n- **Use English**\n- All classes/methods must have complete docstrings, strictly following the template:\n```python\ndef func(a: str, b: int | None = None) -> str:\n    \"\"\"{description}\n\n    Args:\n        a (`str`):\n            The argument a\n        b (`int | None`, optional):\n            The argument b\n\n    Returns:\n        `str`:\n            The return str\n    \"\"\"\n```\n- Use reStructuredText syntax for special content:\n```python\nclass MyClass:\n    \"\"\"xxx\n\n    `Example link <https://xxx>`_\n\n    .. note:: Example note\n\n    .. tip:: Example tip\n\n    .. important:: Example important info\n\n    .. code-block:: python\n\n        def hello_world():\n            print(\"Hello world!\")\n\n    \"\"\"\n```\n\n### [MUST] Pre-commit Checks\n- **Strict review**: In most cases, code should be modified rather than skipping checks\n- **File-level check skipping is prohibited**\n- Only allowed skip: agent class system prompt parameters (to avoid `\\n` formatting issues)\n\n---\n\n## 5. Git Standards\n\n### [MUST] PR Title\n- Follow Conventional Commits\n- Must use prefixes: `feat/fix/docs/ci/refactor/test`, etc.\n- Format: `feat(scope): description`\n- Example: `feat(memory): add redis cache support`"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug Report\nabout: Create a report to help us improve\ntitle: '[Bug]:'\nlabels: 'bug'\nassignees: ''\n\n---\n\n**<u>AgentScope is an open-source project. To involve a broader community, we recommend asking your questions in English.</u>**\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n\n1. You code\n2. How to execute\n3. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Error messages**\nDetailed error messages.\n\n**Environment (please complete the following information):**\n\n- AgentScope Version: [e.g. 1.0.0 via `print(agentscope.__version__)`]\n- Python Version: [e.g. 3.10]\n- OS: [e.g. macos, windows]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/custom.md",
    "content": "---\nname: Custom issue template\nabout: Describe this issue template's purpose here.\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**<u>AgentScope is an open-source project. To involve a broader community, we recommend asking your questions in English.</u>**\n\n\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature Request\nabout: Suggest an idea for this project\ntitle: '[Feature]: '\nlabels: 'enhancement'\nassignees: ''\n\n---\n\n**<u>AgentScope is an open-source project. To involve a broader community, we recommend asking your questions in English.</u>**\n\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here."
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "## PR Title Format\n\nPlease ensure your PR title follows the Conventional Commits format:\n- Format: `<type>(<scope>): <description>`\n- Example: `feat(memory): add redis cache support`\n- Allowed types: `feat`, `fix`, `docs`, `ci`, `refactor`, `test`, `chore`, `perf`, `style`, `build`, `revert`\n- Description should start with a lowercase letter\n\n## AgentScope Version\n\n[The version of AgentScope you are working on, e.g. `import agentscope; print(agentscope.__version__)`]\n\n## Description\n\n[Please describe the background, purpose, changes made, and how to test this PR]\n\n## Checklist\n\nPlease check the following items before code is ready to be reviewed.\n\n- [ ]  Code has been formatted with `pre-commit run --all-files` command\n- [ ]  All tests are passing\n- [ ]  Docstrings are in Google style\n- [ ]  Related documentation has been updated (e.g. links, examples, etc.)\n- [ ]  Code is ready for review"
  },
  {
    "path": ".github/copilot-instructions.md",
    "content": "# AgentScope Code Review Guide\n\nYou should conduct a strict code review. Each requirement is labeled with priority:\n- **[MUST]** must be satisfied or PR will be rejected\n- **[SHOULD]** strongly recommended\n- **[MAY]** optional suggestion\n\n## 1. Code Quality\n\n### [MUST] Lazy Loading\n- Third-party library dependencies should be imported at the point of use, avoid centralized imports at file top\n  - The `Third-party library` refers to libraries not included in the `dependencies` variable in `pyproject.toml`.\n- For base class imports, use factory pattern:\n```python\ndef get_xxx_cls() -> \"MyClass\":\n    from xxx import BaseClass\n    class MyClass(BaseClass): ...\n    return MyClass\n```\n\n### [SHOULD] Code Conciseness\nAfter understanding the code intent, check if it can be optimized:\n- Avoid unnecessary temporary variables\n- Merge duplicate code blocks\n- Prioritize reusing existing utility functions\n\n### [MUST] Encapsulation Standards\n- All Python files under `src/agentscope` should be named with `_` prefix, and exposure controlled through `__init__.py`\n- Classes and functions used internally by the framework that don't need to be exposed to users must be named with `_` prefix\n\n## 2. [MUST] Code Security\n- Prohibit hardcoding API keys/tokens/passwords\n- Use environment variables or configuration files for management\n- Check for debug information and temporary credentials\n- Check for injection attack risks (SQL/command/code injection, etc.)\n\n## 3. [MUST] Testing & Dependencies\n- New features must include unit tests\n- New dependencies need to be added to the corresponding section in `pyproject.toml`\n- Dependencies for non-core scenarios should not be added to the minimal dependency list\n\n## 4. Code Standards\n\n### [MUST] Comment Standards\n- **Use English**\n- All classes/methods must have complete docstrings, strictly following the template:\n```python\ndef func(a: str, b: int | None = None) -> str:\n    \"\"\"{description}\n\n    Args:\n        a (`str`):\n            The argument a\n        b (`int | None`, optional):\n            The argument b\n\n    Returns:\n        `str`:\n            The return str\n    \"\"\"\n```\n- Use reStructuredText syntax for special content:\n```python\nclass MyClass:\n    \"\"\"xxx\n\n    `Example link <https://xxx>`_\n\n    .. note:: Example note\n\n    .. tip:: Example tip\n\n    .. important:: Example important info\n\n    .. code-block:: python\n\n        def hello_world():\n            print(\"Hello world!\")\n\n    \"\"\"\n```\n\n### [MUST] Pre-commit Checks\n- **Strict review**: In most cases, code should be modified rather than skipping checks\n- **File-level check skipping is prohibited**\n- Only allowed skip: agent class system prompt parameters (to avoid `\\n` formatting issues)\n\n---\n\n## 5. Git Standards\n\n### [MUST] PR Title\n- Follow Conventional Commits\n- Must use prefixes: `feat/fix/docs/ci/refactor/test`, etc.\n- Format: `feat(scope): description`\n- Example: `feat(memory): add redis cache support`"
  },
  {
    "path": ".github/scripts/update_news.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\nScript to automatically update NEWS section in README files.\nReads the first 10 news items from docs/NEWS.md and updates README.md and\nREADME_zh.md.\n\"\"\"\n\nfrom pathlib import Path\n\n\ndef read_news_items(news_file: Path, max_items: int = 10) -> list[str]:\n    \"\"\"\n    Read news items from NEWS.md file.\n\n    Args:\n        news_file (`Path`):\n            Path to the NEWS.md file\n        max_items (`int`, optional):\n            Maximum number of items to read\n\n    Returns:\n        `list[str]`:\n            List of news items\n    \"\"\"\n    with open(news_file, \"r\", encoding=\"utf-8\") as f:\n        content = f.read()\n\n    # Split by lines that start with \"- **[\"\n    lines = content.strip().split(\"\\n\")\n    news_items = []\n\n    for line in lines:\n        if line.strip().startswith(\"- **[\"):\n            news_items.append(line)\n            if len(news_items) >= max_items:\n                break\n\n    return news_items\n\n\ndef update_readme(\n    readme_file: Path,\n    news_items: list[str],\n) -> None:\n    \"\"\"\n    Update the NEWS section in README file using HTML comment markers.\n\n    Args:\n        readme_file (`Path`):\n            Path to the README file\n        news_items (`list[str]`):\n            List of news items to insert\n    \"\"\"\n    with open(readme_file, \"r\", encoding=\"utf-8\") as f:\n        content = f.read()\n\n    # Use HTML comment markers to identify the NEWS section\n    begin_marker = \"<!-- BEGIN NEWS -->\"\n    end_marker = \"<!-- END NEWS -->\"\n\n    if begin_marker not in content or end_marker not in content:\n        print(f\"⚠️  NEWS markers not found in {readme_file.name}\")\n        print(\n            f\"    Please add '{begin_marker}' and '{end_marker}' to mark the \"\n            f\"NEWS section\",\n        )\n        return\n\n    # Find positions of markers\n    begin_pos = content.find(begin_marker)\n    end_pos = content.find(end_marker)\n\n    if begin_pos == -1 or end_pos == -1 or begin_pos >= end_pos:\n        print(f\"❌ Invalid NEWS markers in {readme_file.name}\")\n        return\n\n    # Create new NEWS content\n    news_content = \"\\n\".join(news_items)\n\n    # Replace content between markers\n    new_content = (\n        content[: begin_pos + len(begin_marker)]\n        + \"\\n\"\n        + news_content\n        + \"\\n\"\n        + content[end_pos:]\n    )\n\n    with open(readme_file, \"w\", encoding=\"utf-8\") as f:\n        f.write(new_content)\n\n    print(f\"✅ Updated {readme_file.name}\")\n\n\ndef main() -> None:\n    \"\"\"Main function to update NEWS in README files.\"\"\"\n    # Define paths\n    repo_root = Path(__file__).parent.parent.parent\n    news_file_en = repo_root / \"docs\" / \"NEWS.md\"\n    news_file_zh = repo_root / \"docs\" / \"NEWS_zh.md\"\n    readme_en = repo_root / \"README.md\"\n    readme_zh = repo_root / \"README_zh.md\"\n\n    # Update English README from NEWS.md\n    if news_file_en.exists():\n        print(f\"📖 Reading news items from {news_file_en}\")\n        news_items_en = read_news_items(news_file_en, max_items=10)\n        print(f\"📰 Found {len(news_items_en)} English news items\")\n\n        if news_items_en and readme_en.exists():\n            print(f\"📝 Updating {readme_en.name}...\")\n            update_readme(readme_en, news_items_en)\n        elif not news_items_en:\n            print(\"⚠️  No English news items found\")\n        else:\n            print(f\"⚠️  {readme_en} not found\")\n    else:\n        print(f\"❌ NEWS.md not found at {news_file_en}\")\n\n    # Update Chinese README from NEWS_zh.md\n    if news_file_zh.exists() and news_file_zh.stat().st_size > 0:\n        print(f\"📖 Reading news items from {news_file_zh}\")\n        news_items_zh = read_news_items(news_file_zh, max_items=10)\n        print(f\"📰 Found {len(news_items_zh)} Chinese news items\")\n\n        if news_items_zh and readme_zh.exists():\n            print(f\"📝 Updating {readme_zh.name}...\")\n            update_readme(readme_zh, news_items_zh)\n        elif not news_items_zh:\n            print(\"⚠️  No Chinese news items found\")\n        else:\n            print(f\"⚠️  {readme_zh} not found\")\n    else:\n        print(\n            f\"⚠️  NEWS_zh.md not found or empty at {news_file_zh}, \"\n            f\"using English news for Chinese README\",\n        )\n        # Fallback: use English news for Chinese README if NEWS_zh.md\n        # doesn't exist\n        if news_file_en.exists() and readme_zh.exists():\n            print(f\"📖 Reading news items from {news_file_en} (fallback)\")\n            news_items = read_news_items(news_file_en, max_items=10)\n            if news_items:\n                print(f\"📝 Updating {readme_zh.name} with English news...\")\n                update_readme(readme_zh, news_items)\n\n    print(\"✨ All done!\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": ".github/workflows/pr-title-check.yml",
    "content": "name: PR Title Check\n\non:\n  pull_request:\n    branches:\n      - main\n    types: [opened, edited, synchronize, reopened]\n\njobs:\n  check-pr-title:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check PR Title Format\n        uses: amannn/action-semantic-pull-request@v6.1.1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          # Configure allowed types based on your requirements\n          types: |\n            feat\n            fix\n            docs\n            ci\n            refactor\n            test\n            chore\n            perf\n            style\n            build\n            revert\n          # Require a scope (the part in parentheses)\n          requireScope: false\n          # Scope pattern: only lowercase letters, numbers, hyphens, and underscores allowed\n          scopePattern: ^[a-z0-9_-]+$\n          scopePatternError: |\n            The scope (text in parentheses) must contain only lowercase letters, numbers, hyphens, and underscores.\n            Example: \"feat(memory): add redis cache support\"\n            Invalid: \"feat(Memory): ...\" or \"feat(MEMORY): ...\"\n          # Subject (description) must not be empty and must be lowercase\n          subjectPattern: ^(?![A-Z]).+$\n          subjectPatternError: |\n            The subject (description after colon) must start with a lowercase letter.\n            Example: \"feat(memory): add redis cache support\"\n          # Validate the entire PR title against the Conventional Commits spec\n          validateSingleCommit: false\n          # Ignore merge commits\n          ignoreLabels: |\n            ignore-semantic-pull-request\n\n"
  },
  {
    "path": ".github/workflows/pre-commit.yml",
    "content": "name: Pre-commit\n\non: [push, pull_request]\n\njobs:\n  run:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: True\n      matrix:\n        os: [ubuntu-latest]\n    env:\n      OS: ${{ matrix.os }}\n      PYTHON: '3.10'\n    steps:\n    - uses: actions/checkout@master\n    - name: Setup Python\n      uses: actions/setup-python@master\n      with:\n        python-version: '3.10'\n    - name: Update setuptools\n      run: |\n        pip install setuptools==68.2.2 wheel==0.41.2\n    - name: Install AgentScope\n      run: |\n        pip install -q -e .[dev]\n    - name: Install pre-commit\n      run: |\n        pre-commit install\n    - name: Pre-commit starts\n      run: |\n        pre-commit run --all-files > pre-commit.log 2>&1 || true\n        cat pre-commit.log\n        if grep -q Failed pre-commit.log; then\n          echo -e \"\\e[41m  [**FAIL**] Please install pre-commit and format your code first. \\e[0m\"\n          exit 1\n        fi\n        echo -e \"\\e[46m  ********************************Passed******************************** \\e[0m\"\n"
  },
  {
    "path": ".github/workflows/publish-pypi.yml",
    "content": "# This workflow will upload a Python Package using Twine when a release is created\n# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries\n\n# This workflow uses actions that are not certified by GitHub.\n# They are provided by a third-party and are governed by\n# separate terms of service, privacy policy, and support\n# documentation.\n\nname: Publish PyPi Package\n\non:\n  workflow_dispatch:\n  release:\n    types: [published]\n\npermissions:\n  contents: read\n\njobs:\n  deploy:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v6\n    - name: Set up Python\n      uses: actions/setup-python@v6\n      with:\n        python-version: '3.10'\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install setuptools wheel build\n    - name: Build package\n      run: python -m build\n    - name: Test installation\n      run: |\n        pip install dist/*.whl\n        python -c \"import agentscope; print(agentscope.__version__)\"\n    - name: Publish package\n      uses: pypa/gh-action-pypi-publish@release/v1\n      with:\n        user: __token__\n        password: ${{ secrets.PYPI_API_TOKEN }}"
  },
  {
    "path": ".github/workflows/sphinx_docs.yml",
    "content": "name: Deploy Sphinx documentation to Pages\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  build_en:\n    timeout-minutes: 20\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest]\n        python-version: ['3.10']\n    env:\n      OS: ${{ matrix.os }}\n      PYTHON: '3.10'\n    steps:\n      - uses: actions/checkout@v6\n      - name: Setup Python\n        uses: actions/setup-python@v6\n        with:\n          python-version: '3.10'\n      - name: Update setuptools\n        run: |\n          pip install setuptools==78.1.1 wheel==0.45.1\n      - name: Install Dependencies\n        run: |\n          pip install -q -e .[dev]\n      - name: Add execute permission to build.sh\n        run: |\n          chmod +x docs/tutorial/en/build.sh\n      - name: Build English Documentation\n        env:\n          DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}\n          GAODE_API_KEY: ${{ secrets.GAODE_API_KEY }}\n        run: |\n          cd docs/tutorial/en/\n          ./build.sh\n      - name: Deploy English Documentation\n        uses: peaceiris/actions-gh-pages@v4\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n          publish_dir: docs/tutorial/en/build/html\n          cname: doc.agentscope.io\n          keep_files: true\n\n  build_zh:\n    needs: build_en\n    timeout-minutes: 20\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ ubuntu-latest ]\n        python-version: [ '3.10' ]\n    env:\n      OS: ${{ matrix.os }}\n      PYTHON: '3.10'\n    steps:\n      - uses: actions/checkout@v6\n      - name: Setup Python\n        uses: actions/setup-python@v6\n        with:\n          python-version: '3.10'\n      - name: Setup Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: '16'\n      - name: Verify npm installation\n        run: npm --version\n      - name: Update setuptools\n        run: |\n          pip install setuptools==78.1.1 wheel==0.45.1\n      - name: Install Dependencies\n        run: |\n          pip install -q -e .[dev]\n      - name: Add execute permission to build.sh\n        run: |\n          chmod +x docs/tutorial/zh_CN/build.sh\n      - name: Build Chinese Documentation\n        env:\n          DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}\n          GAODE_API_KEY: ${{ secrets.GAODE_API_KEY }}\n        run: |\n          cd docs/tutorial/zh_CN/\n          ./build.sh\n      - name: Deploy Chinese Documentation\n        uses: peaceiris/actions-gh-pages@v4\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n          publish_dir: docs/tutorial/zh_CN/build/html\n          destination_dir: zh_CN\n          cname: doc.agentscope.io\n          keep_files: true"
  },
  {
    "path": ".github/workflows/stale.yml",
    "content": "# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.\n#\n# You can adjust the behavior by modifying this file.\n# For more information, see:\n# https://github.com/actions/stale\nname: Mark stale issues and pull requests\n\non:\n  schedule:\n  - cron: '30 9 * * *'\n\njobs:\n  stale:\n\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n      pull-requests: write\n\n    steps:\n    - uses: actions/stale@v10\n      with:\n        repo-token: ${{ secrets.GITHUB_TOKEN }}\n        stale-issue-message: 'This issue is marked as stale because there has been no activity for 60 days. Remove stale label or add new comments or this issue will be closed in 90 day.'\n        close-issue-message: 'Close this stale issue.'\n        stale-issue-label: 'stale-issue'\n        exempt-issue-labels: 'RoadMap,Roadmap'\n        days-before-stale: 60\n        days-before-close: 30\n\n        stale-pr-message: 'This PR is marked as stale because there has been no activity for 60 days. Remove stale label or add new comments or this PR will be closed in 30 days.'\n        close-pr-message: 'Close this stale PR.'\n        stale-pr-label: 'stale-pr'\n        days-before-pr-stale: 60\n        days-before-pr-close: 30\n"
  },
  {
    "path": ".github/workflows/toc.yml",
    "content": "name: Generate TOC\non:\n  push:\n    paths:\n      - 'README.md'\n      - 'README_ZH.md'\n    branches-ignore:\n      - 'main'\n\n# Prevent concurrent runs that modify README files\nconcurrency:\n  group: readme-updates-${{ github.ref }}\n  cancel-in-progress: false\n\njobs:\n  generateTOC:\n    name: TOC Generator\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: technote-space/toc-generator@v4\n        with:\n          TOC_TITLE: \"## 📑 Table of Contents\"\n          CREATE_PR: false\n          TARGET_PATHS: \"README.md,README_ZH.md\"\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TOC_TITLE_MAP: |\n            README.md: ## 📑 Table of Contents\n            README_ZH.md: ## 📑 目录"
  },
  {
    "path": ".github/workflows/unittest.yml",
    "content": "name: Python Unittest Coverage\n\non: [push, pull_request]\n\njobs:\n  test:\n    if: false == contains(github.event.pull_request.title, 'WIP')\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-15]\n        python-version: ['3.10', '3.11', '3.12']\n    env:\n      OS: ${{ matrix.os }}\n    steps:\n    - uses: actions/checkout@master\n    - name: Setup Python ${{ matrix.python-version }}\n      uses: actions/setup-python@master\n      with:\n        python-version: ${{ matrix.python-version }}\n    - name: Update setuptools\n      run: |\n        pip install setuptools==78.1.1 wheel==0.45.1\n    - name: Install Dev Dependencies\n      run: |\n        pip install -q -e .[dev]\n        pip install coverage pytest\n    - name: Run tests with coverage\n      run: |\n        coverage run -m pytest tests\n    - name: Generate coverage report\n      run: |\n        coverage report -m"
  },
  {
    "path": ".github/workflows/update_news.yml",
    "content": "name: Update NEWS in README\non:\n  push:\n    paths:\n      - 'docs/NEWS.md'\n      - 'docs/NEWS_zh.md'\n    branches-ignore:\n      - 'main'\n\n# Prevent concurrent runs that modify README files\nconcurrency:\n  group: readme-updates-${{ github.ref }}\n  cancel-in-progress: false\n\njobs:\n  updateNews:\n    name: NEWS Updater\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Set up Python\n        uses: actions/setup-python@v6\n        with:\n          python-version: '3.10'\n\n      - name: Update NEWS in README files\n        run: python .github/scripts/update_news.py\n\n      - name: Commit changes\n        run: |\n          git config --local user.email \"github-actions[bot]@users.noreply.github.com\"\n          git config --local user.name \"github-actions[bot]\"\n          git add README.md README_zh.md\n          git diff --staged --quiet || git commit -m \"docs: auto-sync NEWS section to README files\"\n          git push\n\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n.idea/\n\n# macOS\n.DS_Store\n\n# docs\ndocs/tutorial/en/build/\ndocs/tutorial/zh_CN/build/\n\n# Sphinx build artifacts\ndocs/tutorial/**/doctrees/\ndocs/tutorial/**/.doctrees/\n*.buildinfo\n*.pickle\n\nnode_modules/\npackage-lock.json\n*.tsbuildinfo\n.wireit/\n.angular/\nuv.lock"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v4.3.0\n    hooks:\n      - id: check-ast\n      - id: sort-simple-yaml\n      - id: check-yaml\n        exclude: |\n          (?x)^(\n              meta.yaml\n          )$\n      - id: check-xml\n      - id: check-toml\n      - id: check-docstring-first\n      - id: check-json\n      - id: fix-encoding-pragma\n      - id: detect-private-key\n      - id: trailing-whitespace\n  - repo: https://github.com/asottile/add-trailing-comma\n    rev: v3.1.0\n    hooks:\n      - id: add-trailing-comma\n  - repo: https://github.com/pre-commit/mirrors-mypy\n    rev: v1.7.0\n    hooks:\n      - id: mypy\n        exclude:\n            (?x)(\n                pb2\\.py$\n                | grpc\\.py$\n                | ^docs\n                | \\.html$\n            )\n        args: [ --disallow-untyped-defs,\n                --disallow-incomplete-defs,\n                --ignore-missing-imports,\n                --disable-error-code=var-annotated,\n                --disable-error-code=union-attr,\n                --disable-error-code=assignment,\n                --disable-error-code=attr-defined,\n                --disable-error-code=import-untyped,\n                --disable-error-code=truthy-function,\n                --disable-error-code=typeddict-item,\n                --follow-imports=skip,\n                --explicit-package-bases,\n                ]\n  # - repo: https://github.com/numpy/numpydoc\n  #   rev: v1.6.0\n  #   hooks:\n  #     - id: numpydoc-validation\n  - repo: https://github.com/psf/black\n    rev: 23.3.0\n    hooks:\n    - id: black\n      args: [--line-length=79]\n  - repo: https://github.com/PyCQA/flake8\n    rev: 6.1.0\n    hooks:\n      - id: flake8\n        args: [\"--extend-ignore=E203\"]\n        exclude: ^docs\n  - repo: https://github.com/pylint-dev/pylint\n    rev: v3.0.2\n    hooks:\n      - id: pylint\n        exclude:\n            (?x)(\n                ^docs\n                | pb2\\.py$\n                | grpc\\.py$\n                | \\.demo$\n                | \\.md$\n                | \\.html$\n                | ^examples/paper_llm_based_algorithm/\n          )\n        args: [\n          --disable=W0511,\n          --disable=W0718,\n          --disable=W0122,\n          --disable=C0103,\n          --disable=R0913,\n          --disable=E0401,\n          --disable=E1101,\n          --disable=C0415,\n          --disable=W0603,\n          --disable=R1705,\n          --disable=R0914,\n          --disable=E0601,\n          --disable=W0602,\n          --disable=W0604,\n          --disable=R0801,\n          --disable=R0902,\n          --disable=R0903,\n          --disable=C0123,\n          --disable=W0231,\n          --disable=W1113,\n          --disable=W0221,\n          --disable=R0401,\n          --disable=W0632,\n          --disable=W0123,\n          --disable=C3001,\n        ]\n  - repo: https://github.com/regebro/pyroma\n    rev: \"5.0\"\n    hooks:\n      - id: pyroma\n        args: [--min=10, .]\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to AgentScope\n\n## Welcome! 🎉\n\nThank you for your interest in contributing to AgentScope! As an open-source project, we warmly welcome and encourage\ncontributions from the community. Whether you're fixing bugs, adding new features, improving documentation, or sharing\nideas, your contributions help make AgentScope better for everyone.\n\n## How to Contribute\n\nTo ensure smooth collaboration and maintain the quality of the project, please follow these guidelines when contributing:\n\n### 1. Check Existing Plans and Issues\n\nBefore starting your contribution, please review our development roadmap:\n\n- **Check the [Projects](https://github.com/orgs/agentscope-ai/projects/2) page** and **[Issues with `roadmap` label](https://github.com/agentscope-ai/agentscope/issues?q=is%3Aissue%20state%3Aopen%20label%3ARoadmap)** to see our planned development tasks.\n\n  - **If a related issue exists** and is marked as unassigned or open:\n    - Please comment on the issue to express your interest in working on it\n    - This helps avoid duplicate efforts and allows us to coordinate development\n\n  - **If no related issue exists**:\n    - Please create a new issue describing your proposed changes or feature\n    - Our team will respond promptly to provide feedback and guidance\n    - This helps us maintain the project roadmap and coordinate community efforts\n\n### 2. Commit Message Format\n\nWe follow the [Conventional Commits](https://www.conventionalcommits.org/) specification. This leads to more readable\ncommit history and enables automatic changelog generation.\n\n**Format:**\n```\n<type>(<scope>): <subject>\n```\n\n**Types:**\n- `feat:` A new feature\n- `fix:` A bug fix\n- `docs:` Documentation only changes\n- `style:` Changes that do not affect the meaning of the code (white-space, formatting, etc)\n- `refactor:` A code change that neither fixes a bug nor adds a feature\n- `perf:` A code change that improves performance\n- `ci:` Adding missing tests or correcting existing tests\n- `chore:` Changes to the build process or auxiliary tools and libraries\n\n**Examples:**\n```bash\nfeat(models): add support for Claude-3 model\nfix(agent): resolve memory leak in ReActAgent\ndocs(readme): update installation instructions\nrefactor(formatter): simplify message formatting logic\nci(models): add unit tests for OpenAI integration\n```\n\n### 3. Pull Request Title Format\n\nPull request titles must follow the same [Conventional Commits](https://www.conventionalcommits.org/) specification:\n\n**Format:**\n```\n<type>(<scope>): <description>\n```\n\n**Requirements:**\n- The title must start with one of the allowed types: `feat`, `fix`, `docs`, `ci`, `refactor`, `test`, `chore`, `perf`, `style`, `build`, `revert`\n- Scope is optional but recommended\n- **Scope must be lowercase** - only lowercase letters, numbers, hyphens (`-`), and underscores (`_`) are allowed\n- Description should start with a lowercase letter\n- Keep the title concise and descriptive\n\n**Examples:**\n```\n✅ Valid:\nfeat(memory): add redis cache support\nfix(agent): resolve memory leak in ReActAgent\ndocs(tutorial): update installation guide\nci(workflow): add PR title validation\nrefactor(my-feature): simplify logic\n\n❌ Invalid:\nfeat(Memory): add cache          # Scope must be lowercase\nfeat(MEMORY): add cache          # Scope must be lowercase\nfeat(MyFeature): add feature     # Scope must be lowercase\n```\n\n**Automated Validation:**\n- PR titles targeting the `main` branch are automatically validated by GitHub Actions\n- PRs with invalid titles will be blocked until the title is corrected\n\n### 4. Code Development Guidelines\n\n#### a. Pre-commit Checks\n\nBefore submitting code, you must run pre-commit hooks to ensure code quality and consistency:\n\n**Installation:**\n```bash\npip install pre-commit\npre-commit install\n```\n\n**Running pre-commit:**\n```bash\n# Run on all files\npre-commit run --all-files\n\n# Pre-commit will automatically run on git commit after installation\n```\n\n#### b. Import Statement Guidelines\n\nAgentScope follows a **lazy import principle** to minimize resource loading:\n\n- **DO**: Import modules only when they are actually used\n  ```python\n  def some_function():\n      import openai\n      # Use openai library here\n  ```\n\nThis approach ensures that `import agentscope` remains lightweight and doesn't load unnecessary dependencies.\n\n#### c. Unit Tests\n\n- All new features must include appropriate unit tests\n- Ensure existing tests pass before submitting your PR\n- Run tests using:\n  ```bash\n  pytest tests\n  ```\n\n#### d. Documentation\n\n- Update relevant documentation for new features\n- Include code examples where appropriate\n- Update the README.md if your changes affect user-facing functionality\n\n\n## Types of Contributions\n\n### Adding New Chat Models\n\nAgentScope currently supports the following API providers at the chat model level: **OpenAI**, **DashScope**,\n**Gemini**, **Anthropic**, and **Ollama**. These APIs are compatible with various service providers including vLLM,\nDeepSeek, SGLang, and others.\n\n**⚠️ Important Notice:**\n\nAdding a new chat model is not merely a model-level task. It involves multiple components including:\n- Message formatters\n- Token counters\n- Tools API integration\n\nThis is a substantial amount of work. To better focus our efforts on agent capability development and maintenance,\n**the official development team currently does not plan to add support for new chat model APIs**. However, when there\nis a strong need from the developer community, we will do our best to accommodate these requirements.\n\n**If you wish to contribute a new chat model**, here are the components needed to be compatible with the\nexisting `ReActAgent` in the repository:\n\n#### Required Components:\n\n1. **Chat Model Class** (under `agentscope.model`):\n   ```python\n   from agentscope.model import ChatModelBase\n\n\n   class YourChatModel(ChatModelBase):\n       \"\"\"\n       The functionalities that you need to consider include:\n       - Tools API integration\n       - Both streaming and non-streaming modes (compatible with tools API)\n       - tool_choice argument\n       - reasoning models\n       \"\"\"\n   ```\n\n2. **Formatter Class** (under `agentscope.formatter`):\n   ```python\n   from agentscope.formatter import FormatterBase\n\n   class YourModelFormatter(FormatterBase):\n       \"\"\"\n       Convert `Msg` objects into the format required by your API provider.\n       If your API doesn't support multi-agent scenarios (e.g. doesn't support the name field in messages), you need to\n       implement two separate formatter classes for chatbot and multi-agent scenarios.\n       \"\"\"\n   ```\n\n3. **Token Counter** (under `agentscope.token`, recommended):\n   ```python\n   from agentscope.token import TokenCounterBase\n\n   class YourTokenCounter(TokenCounterBase):\n       \"\"\"\n       Implement token counting logic for your model.\n       This is recommended but not strictly required.\n       \"\"\"\n   ```\n\n### Adding New Agents\n\nTo achieve true modularity, the `agentscope.agent` module currently aims to maintain only the **`ReActAgent`** class\nas the core implementation. We ensure all functionalities in this class are **modular, detachable, and composable**.\n\nIn AgentScope, we follow an examples-first development workflow: prototype new implementations in the `examples/`\ndirectory, then abstract and modularize the functionality, and finally integrate it into the core library.\n\nFor specialized or domain-specific agents, we recommend contributing them to the **`examples/agents`** directory:\n\n```\nexamples/\n└── agents/\n    ├── main.py\n    ├── README.md  # Explain the agent's purpose and usage\n    └── ... # The other scripts\n```\n\n### Adding New Examples\n\nWe highly encourage contributions of new examples that showcase the capabilities of AgentScope! Your examples help others learn and get inspired.\n\n**📝 About the Examples Directory:**\n\nTo maintain code quality and keep the repository accessible for everyone, we've designed the `examples/` directory in the main AgentScope repository to focus on **demonstrating AgentScope's functionalities**. Think of these as educational references and feature showcases that help developers quickly understand what AgentScope can do.\n\n**What makes a great example here:**\n- Clearly demonstrates specific AgentScope features or capabilities\n- Easy to understand and follow along\n- Serves as a learning material or reference implementation\n- Focused and concise\n\n**For More Complex Applications:**\n\nHave you built something amazing with AgentScope? Perhaps a more sophisticated, production-ready application? That's fantastic! 🎉\n\nWe'd love to see your work in our **[agentscope-samples](https://github.com/agentscope-ai/agentscope-samples)** repository. This dedicated space is perfect for showcasing complete, real-world applications and sharing your AgentScope-based projects with the community. It's a great way to inspire others and demonstrate the full potential of the AgentScope ecosystem!\n\n**Example Organization:**\n\nExamples in the main repository are organized into subdirectories based on their type:\n\n- `examples/agent/` for specialized agents\n- `examples/functionality/` for showcasing specific functionalities of AgentScope\n- `examples/game/` for game-related examples\n- `examples/evaluation/` for evaluation scripts\n- `examples/workflows/` for workflow demonstrations\n- `examples/tuner/` for tuning-related examples\n\nAn example structure could be:\n\n```\nexamples/\n└── {example_type}/\n    └── {example_name}/\n        ├── main.py\n        ├── README.md  # Explain the example's purpose and usage\n        └── ... # The other scripts\n```\n\n### Adding New Memory Databases\n\nThe memory module in AgentScope currently supports:\n\n- **In-memory storage**: For lightweight, temporary memory needs\n- **Relational databases via SQLAlchemy**: For persistent, structured data storage\n- **NoSQL databases**: For flexible schema requirements (e.g., Redis)\n\n**⚠️ Important Notice:**\n\nFor **relational databases**, we use **SQLAlchemy** as a unified abstraction layer. SQLAlchemy already supports a wide\nrange of SQL databases including PostgreSQL, MySQL, SQLite, Oracle, Microsoft SQL Server, and many others.\n\n**Therefore, we do not accept separate implementations for relational databases that are already supported by SQLAlchemy.**\nIf you need support for a specific relational database, please ensure it works through the existing SQLAlchemy integration.\n\n**If you wish to contribute a new memory database implementation**, please consider:\n\n1. **For relational databases**: Use the existing SQLAlchemy integration.\n\n2. **For NoSQL databases**: If you're adding support for a new NoSQL database (e.g., MongoDB, Cassandra), please:\n   - Implement a new memory class that extends the appropriate base class\n   - Add comprehensive unit tests\n   - Update documentation accordingly\n\n\n## Do's and Don'ts\n\n### ✅ DO:\n\n- **Start small**: Begin with small, manageable contributions\n- **Communicate early**: Discuss major changes before implementing them\n- **Write tests**: Ensure your code is well-tested\n- **Document your code**: Help others understand your contributions\n- **Follow commit conventions**: Use conventional commit messages\n- **Be respectful**: Follow our Code of Conduct\n- **Ask questions**: If you're unsure about something, just ask!\n\n### ❌ DON'T:\n\n- **Don't surprise us with big pull requests**: Large, unexpected PRs are difficult to review and may not align with project goals. Always open an issue first to discuss major changes\n- **Don't ignore CI failures**: Fix any issues flagged by continuous integration\n- **Don't mix concerns**: Keep PRs focused on a single feature or fix\n- **Don't forget to update tests**: Changes in functionality should be reflected in tests\n- **Don't break existing APIs**: Maintain backward compatibility when possible, or clearly document breaking changes\n- **Don't add unnecessary dependencies**: Keep the core library lightweight\n- **Don't bypass the lazy import principle**: This keeps AgentScope fast to import\n\n## Getting Help\n\nIf you need assistance or have questions:\n\n- 💬 Open a [Discussion](https://github.com/agentscope-ai/agentscope/discussions)\n- 🐛 Report bugs via [Issues](https://github.com/agentscope-ai/agentscope/issues)\n- 📧 Contact the maintainers at DingTalk or Discord (links in the README.md)\n\n\n---\n\nThank you for contributing to AgentScope! Your efforts help build a better tool for the entire community. 🚀\n"
  },
  {
    "path": "CONTRIBUTING_zh.md",
    "content": "# 贡献到 AgentScope\n\n## 欢迎！🎉\n\n感谢开源社区对 AgentScope 项目的关注和支持，作为一个开源项目，我们热烈欢迎并鼓励来自社区的贡献。无论是修复错误、添加新功能、改进文档还是\n分享想法，这些贡献都能帮助 AgentScope 变得更好。\n\n## 如何贡献\n\n为了确保顺利协作并保持项目质量，请在贡献时遵循以下指南：\n\n### 1. 检查现有计划和问题\n\n在开始贡献之前，请查看我们的开发路线图：\n\n- **查看 [Projects](https://github.com/orgs/agentscope-ai/projects/2) 页面** 和 **[带有 `roadmap` 标签的 Issues](https://github.com/agentscope-ai/agentscope/issues?q=is%3Aissue%20state%3Aopen%20label%3ARoadmap)** 以了解我们计划的开发任务。\n\n  - **如果存在相关问题** 并且标记为未分配或开放状态：\n    - 请在该问题下评论，表达您有兴趣参与该任务\n    - 这有助于协调开发工作，避免重复工作\n\n  - **如果不存在相关问题**：\n    - 请创建一个新 issue 用以描述对应的更改或功能\n    - 我们的团队将及时进行回复并提供反馈\n    - 这有助于我们维护项目路线图并协调社区工作\n\n### 2. 提交信息格式\n\nAgentScope 遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范。这使得提交历史更易读，并能够自动生成更新日志。\n\n**格式：**\n```\n<type>(<scope>): <subject>\n```\n\n**类型：**\n- `feat:` 新功能\n- `fix:` 错误修复\n- `docs:` 仅文档更改\n- `style:` 不影响代码含义的更改（空格、格式等）\n- `refactor:` 既不修复错误也不添加功能的代码更改\n- `perf:` 提高性能的代码更改\n- `ci:` 添加缺失的测试或更正现有测试\n- `chore:` 对构建过程或辅助工具和库的更改\n\n**示例：**\n```bash\nfeat(models): add support for Claude-3 model\nfix(agent): resolve memory leak in ReActAgent\ndocs(readme): update installation instructions\nrefactor(formatter): simplify message formatting logic\nci(models): add unit tests for OpenAI integration\n```\n\n### 3. Pull Request 标题格式\n\nPull Request 标题必须遵循相同的 [Conventional Commits](https://www.conventionalcommits.org/) 规范：\n\n**格式：**\n```\n<type>(<scope>): <description>\n```\n\n**要求：**\n- 标题必须以允许的类型之一开头：`feat`、`fix`、`docs`、`ci`、`refactor`、`test`、`chore`、`perf`、`style`、`build`、`revert`\n- Scope 是可选的但建议添加\n- **Scope 必须是小写** - 只允许小写字母、数字、连字符（`-`）和下划线（`_`）\n- 描述应以小写字母开头\n- 保持标题简洁且具有描述性\n\n**示例：**\n```\n✅ 有效：\nfeat(memory): add redis cache support\nfix(agent): resolve memory leak in ReActAgent\ndocs(tutorial): update installation guide\nci(workflow): add PR title validation\nrefactor(my-feature): simplify logic\n\n❌ 无效：\nfeat(Memory): add cache          # Scope 必须是小写\nfeat(MEMORY): add cache          # Scope 必须是小写\nfeat(MyFeature): add feature     # Scope 必须是小写\n```\n\n**自动化验证：**\n- 针对 `main` 分支的 PR 标题会通过 GitHub Actions 自动验证\n- 标题无效的 PR 将被阻止，直到标题被修正\n\n### 4. 代码开发指南\n\n#### a. 提交前检查\n\n在提交代码之前，请运行 pre-commit 钩子以确保代码质量和一致性：\n\n\n```bash\npip install pre-commit\npre-commit install\n```\n\n**运行 pre-commit：**\n```bash\n# 在所有文件上运行\npre-commit run --all-files\n\n# 安装后，pre-commit 将在 git commit 时自动运行\n```\n\n#### b. 关于代码中的 Import\n\nAgentScope 遵循**懒加载导入原则**以最小化资源加载：\n\n- **推荐做法**：仅在实际使用时导入模块\n  ```python\n  def some_function():\n      import openai\n      # 在此处使用 openai 库\n  ```\n\n这种方法确保 `import agentscope` 是一个轻量操作，不会加载不必要的依赖项。\n\n#### c. 单元测试\n\n- 所有新功能都必须包含适当的单元测试\n- 在提交 PR 之前确保现有测试通过\n- 使用以下命令运行测试：\n  ```bash\n  pytest tests\n  ```\n\n#### d. 文档\n\n- 为新功能更新相关文档\n- 在适当的地方包含代码示例\n- 如果更改影响面向用户的功能，请更新 README.md\n\n\n## 贡献类型\n\n### 添加新的 ChatModel\n\nAgentScope 目前内置支持以下 API 提供商：**OpenAI**、**DashScope**、**Gemini**、**Anthropic** 和 **Ollama**。\n其中 `OpenAIChatModel` 的实现还兼容不同的服务提供商，如 vLLM，DeepSeek、SGLang 等。\n\n**⚠️ 重要：**\n\n添加新的 ChatModel 不仅涉及模型层面的实现，还涉及到其它组件的配合，具体包括：\n- 消息格式化器（formatter）\n- Token 计数器（token counter）\n- Tools API 集成\n\n这意味着添加一个 ChatModel 需要大量的工作来确保其与 AgentScope 生态系统的其他部分无缝集成。\n为了更好地专注于智能体能力开发和维护，**官方开发团队目前不计划添加对新 API 的支持**。\n但是当开发者社区有强烈需求时，我们将尽力满足这些需求。\n\n**对于一个 ChatModel 类的实现**，为了与仓库中 `ReActAgent` 兼容，所需要实现的组件如下：\n\n#### 必需组件：\n\n1. **ChatModel**（位于 `agentscope.model` 下）：\n   ```python\n   from agentscope.model import ChatModelBase\n\n\n   class YourChatModel(ChatModelBase):\n       \"\"\"\n       需要考虑的功能包括：\n       - 集成 tools API\n       - 支持流式和非流式模式，并与 tools API 兼容\n       - 支持 tool_choice 参数\n       - 考虑支持推理模型\n       \"\"\"\n   ```\n\n2. **格式化器类**（位于 `agentscope.formatter` 下）：\n   ```python\n   from agentscope.formatter import FormatterBase\n\n   class YourModelFormatter(FormatterBase):\n       \"\"\"\n       将 `Msg` 对象转换为对应 API 提供商所需的格式。\n       如果模型 API 不支持多智能体场景（例如不支持消息中的 name 字段），需要\n       为 chatbot 和多智能体场景分别实现两个格式化器类。\n       \"\"\"\n   ```\n\n3. **Token 计数器**（位于 `agentscope.token` 下，推荐）：\n   ```python\n   from agentscope.token import TokenCounterBase\n\n   class YourTokenCounter(TokenCounterBase):\n       \"\"\"\n       为对应模型实现 token 计数逻辑（推荐实现，非严格要求）。\n       \"\"\"\n   ```\n\n### 添加新的智能体\n\n为了确保 AgentScope 中所有的功能实现都是**模块化的、可拆卸的和可组合的**，`agentscope.agent` 模块目前仅维护 **`ReActAgent`** 类作为核心实现。\n\n在 AgentScope 中，我们遵循示例优先的开发工作流程：\n\n- 在 `examples/` 目录中初步实现新的功能\n- 然后将重要功能抽象和模块化，集成到核心库中\n- 修改 `examples/` 目录中的示例以使用新的核心功能\n\n对于专门的或特定领域的智能体，我们建议按照以下组织形式将它们贡献到 **`examples/agent`** 目录：\n\n```\nexamples/\n└── agent/\n    ├── main.py\n    ├── README.md  # 解释智能体的目的和用法\n    └── ... # 其他脚本\n```\n\n### 添加新的示例\n\n欢迎开源社区贡献新的示例来展示 AgentScope 的各种功能！\n\n**📝 关于示例目录：**\n\n为了避免仓库变得过于臃肿，我们将 AgentScope 仓库中的 `examples/` 目录设计为专注于**展示 AgentScope 的功能性**。可以把这些示例看作是指导性的参考和功能展示，帮助开发者快速理解 AgentScope 能做什么。\n\n**什么样的示例适合放在这里：**\n- 清晰地展示 AgentScope 的特定功能或能力\n- 易于理解和跟随学习\n- 作为学习材料或参考实现\n- 专注且简洁\n\n**对于更复杂的应用：**\n\n对于更加复杂，生产就绪的应用，我们非常期待在 **[agentscope-samples](https://github.com/agentscope-ai/agentscope-samples)** 仓库中看到您的作品。这个仓库专门用于展示、分享基于 AgentScope 生态搭建的完整的、真实世界的应用。\n\n**示例组织方式：**\n\n主仓库中的示例根据类型组织到子目录中：\n\n- `examples/agent/` 用于专门的智能体\n- `examples/functionality/` 用于展示 AgentScope 的特定功能\n- `examples/game/` 用于游戏相关示例\n- `examples/evaluation/` 用于评估脚本\n- `examples/workflows/` 用于工作流演示\n- `examples/tuner/` 用于微调相关示例\n\n示例结构如下：\n\n```\nexamples/\n└── {example_type}/\n    └── {example_name}/\n        ├── main.py\n        ├── README.md  # 解释示例的目的和用法\n        └── ... # 其他脚本\n```\n\n### 添加新的记忆数据库\n\nAgentScope 的记忆模块目前支持：\n\n- **内存存储**：用于轻量级的临时记忆需求\n- **通过 SQLAlchemy 支持关系型数据库**：用于持久化的结构化数据存储\n- **NoSQL 数据库**：用于灵活的模式需求（例如 Redis）\n\n**⚠️ 请注意：**\n\n对于**关系型数据库**，我们使用 **SQLAlchemy** 作为统一的抽象层。SQLAlchemy 已经支持多种 SQL 数据库，包括 PostgreSQL、MySQL、SQLite、Oracle、Microsoft SQL Server 等。\n\n**因此，为了保持 AgentScope 代码的整洁，目前不接受为 SQLAlchemy 已经支持的关系型数据库单独实现新的支持。**\n如果您需要支持特定的关系型数据库，请确保通过现有的 SQLAlchemy 集成来实现。\n\n**如果您希望贡献新的记忆数据库实现**，请考虑以下几点：\n\n1. **对于关系型数据库**：使用现有的 SQLAlchemy 集成。\n\n2. **对于 NoSQL 数据库**：如果您要添加对新 NoSQL 数据库的支持（例如 MongoDB、Cassandra），请：\n   - 实现一个扩展适当基类的新记忆类\n   - 添加全面的单元测试\n   - 相应地更新文档\n\n\n## Do's and Don'ts\n\n### ✅ DO\n\n- **从小处着手**：从小的、可管理的贡献开始\n- **及早沟通**：在实现主要功能之前进行讨论\n- **编写测试**：确保代码经过充分测试\n- **添加代码注释**：帮助他人理解贡献内容\n- **遵循提交约定**：使用约定式提交消息\n- **保持尊重**：遵守我们的行为准则\n- **提出问题**：如果不确定某事，请提问！\n\n### ❌ DON'T\n\n- **不要用大型 PR 让我们措手不及**：大型的、意外的 PR 难以审查，并且可能与项目目标不一致。在进行重大更改之前，请务必先开启一个问题进行讨论\n- **不要忽略 CI 失败**：修复持续集成标记的任何问题\n- **不要混合关注点**：保持 PR 专注于单一功能的实现或修复\n- **不要忘记更新测试**：功能的更改应反映在测试中\n- **不要破坏现有 API**：在可能的情况下保持向后兼容性，或清楚地记录破坏性更改\n- **不要添加不必要的依赖项**：保持核心库轻量级\n- **不要绕过懒加载导入原则**：确保 AgentScope 在导入阶段不至于臃肿\n\n## 获取帮助\n\n如果需要帮助或有疑问：\n\n- 💬 开启一个 [Discussion](https://github.com/agentscope-ai/agentscope/discussions)\n- 🐛 通过 [Issues](https://github.com/agentscope-ai/agentscope/issues) 报告错误\n- 📧 通过钉钉交流群或 Discord 联系开发团队（链接在 README.md 中）\n\n\n---\n\n感谢为 AgentScope 做出贡献！🚀\n\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2024 Alibaba\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n\n\n--------------------------------------------------------------------------------\n\n\nSome codes of tests/run.py is modified from\nhttps://github.com/alibaba/FederatedScope/blob/master/tests/run.py, which is\nalso licensed under the terms of the Apache 2.0.\n\n\n--------------------------------------------------------------------------------\n\nCode in src/agentscope/web/static/js/socket.io.js is adapted from\nhttps://cdnjs.cloudflare.com/ajax/libs/socket.io/3.1.3/socket.io.js (MIT License)\n\nCopyright (c) 2014-2021 Guillermo Rauch\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n--------------------------------------------------------------------------------\n\nCode in src/agentscope/web/static/js/jquery-3.3.1.min.js is adapted from\nhttps://code.jquery.com/jquery-3.3.1.min.js (MIT License)\n\nCopyright (c) JS Foundation and other contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n--------------------------------------------------------------------------------\n\nCode in src/agentscope/web/static/js/bootstrap.bundle.min.js is adapted from\nhttps://cdn.jsdelivr.net/npm/bootstrap@4.3.1/dist/js/bootstrap.bundle.min.js\n (MIT License)\n\nCopyright (c) 2011-2019 The Bootstrap Authors (https://github\n.com/twbs/bootstrap/graphs/contributors)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n--------------------------------------------------------------------------------\n\nCode in src/agentscope/web/static/js/bootstrap-table.min.js is adapted from\nhttps://unpkg.com/bootstrap-table@1.18.0/dist/bootstrap-table.min.js (MIT\nLicense)\n\nCopyright (c) wenzhixin <wenzhixin2010@gmail.com> (http://wenzhixin.net.cn/)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n--------------------------------------------------------------------------------\n\nCode in src/agentscope/web/static/css/bootstrap.min.css is adapted from\nhttps://cdn.jsdelivr.net/npm/bootstrap@4.3.1/dist/css/bootstrap.min.css (MIT\nLicense)\n\nCopyright 2011-2019 The Bootstrap Authors\nCopyright 2011-2019 Twitter, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n\n--------------------------------------------------------------------------------\n\nFonts in src/agentscope/web/static/fonts/KRYPTON.ttf is adapted from\nhttps://github.com/githubnext/monaspace (SIL Open Font License 1.1). These\nfonts are distributed with their original license. See https://github\n.com/githubnext/monaspace/blob/main/LICENSE for the full text of the license.\nThe following font families are included:\n\n- Monaspace (with subfamilies: Krypton)\n\nCopyright (c) 2023, GitHub https://github.com/githubnext/monaspace\nwith Reserved Font Name \"Monaspace\", including subfamilies: \"Argon\", \"Neon\",\n\"Xenon\", \"Radon\", and \"Krypton\"\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n\n--------------------------------------------------------------------------------\n\nFonts in src/agentscope/web/static/fonts/OSWALD.ttf is adapted from\nhttps://fonts.google.com/specimen/Oswald (SIL Open Font License 1.1). These\nfonts are distributed with their original license. See https://github\n.com/googlefonts/OswaldFont/blob/main/OFL.txt for the full text of the license.\n\nCopyright 2016 The Oswald Project Authors (https://github\n.com/googlefonts/OswaldFont)\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n\n--------------------------------------------------------------------------------"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <img\n    src=\"https://img.alicdn.com/imgextra/i1/O1CN01nTg6w21NqT5qFKH1u_!!6000000001621-55-tps-550-550.svg\"\n    alt=\"AgentScope Logo\"\n    width=\"200\"\n  />\n</p>\n\n<span align=\"center\">\n\n[**中文主页**](https://github.com/agentscope-ai/agentscope/blob/main/README_zh.md) | [**Tutorial**](https://doc.agentscope.io/) | [**Roadmap (Jan 2026 -)**](https://github.com/agentscope-ai/agentscope/blob/main/docs/roadmap.md) | [**FAQ**](https://doc.agentscope.io/tutorial/faq.html)\n\n</span>\n\n<p align=\"center\">\n    <a href=\"https://arxiv.org/abs/2402.14034\">\n        <img\n            src=\"https://img.shields.io/badge/cs.MA-2402.14034-B31C1C?logo=arxiv&logoColor=B31C1C\"\n            alt=\"arxiv\"\n        />\n    </a>\n    <a href=\"https://pypi.org/project/agentscope/\">\n        <img\n            src=\"https://img.shields.io/badge/python-3.10+-blue?logo=python\"\n            alt=\"pypi\"\n        />\n    </a>\n    <a href=\"https://pypi.org/project/agentscope/\">\n        <img\n            src=\"https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fpypi.org%2Fpypi%2Fagentscope%2Fjson&query=%24.info.version&prefix=v&logo=pypi&label=version\"\n            alt=\"pypi\"\n        />\n    </a>\n    <a href=\"https://discord.gg/eYMpfnkG8h\">\n        <img\n            src=\"https://img.shields.io/discord/1194846673529213039?label=Discord&logo=discord\"\n            alt=\"discord\"\n        />\n    </a>\n    <a href=\"https://doc.agentscope.io/\">\n        <img\n            src=\"https://img.shields.io/badge/Docs-English%7C%E4%B8%AD%E6%96%87-blue?logo=markdown\"\n            alt=\"docs\"\n        />\n    </a>\n    <a href=\"./LICENSE\">\n        <img\n            src=\"https://img.shields.io/badge/license-Apache--2.0-black\"\n            alt=\"license\"\n        />\n    </a>\n</p>\n\n<p align=\"center\">\n<img src=\"https://trendshift.io/api/badge/repositories/10079\" alt=\"modelscope%2Fagentscope | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/>\n</p>\n\n## What is AgentScope?\n\nAgentScope is a production-ready, easy-to-use agent framework with essential abstractions that work with rising model capability and built-in support for finetuning.\n\nWe design for increasingly agentic LLMs.\nOur approach leverages the models' reasoning and tool use abilities\nrather than constraining them with strict prompts and opinionated orchestrations.\n\n## Why use AgentScope?\n\n- **Simple**: start building your agents in 5 minutes with built-in ReAct agent, tools, skills, human-in-the-loop steering, memory, planning, realtime voice, evaluation and model finetuning\n- **Extensible**: large number of ecosystem integrations for tools, memory and observability; built-in support for MCP and A2A; message hub for flexible multi-agent orchestration and workflows\n- **Production-ready**: deploy and serve your agents locally, as serverless in the cloud, or on your K8s cluster with built-in OTel support\n\n\n<p align=\"center\">\n<img src=\"./assets/images/agentscope.png\" width=\"90%\" />\n<br/>\nThe AgentScope Ecosystem\n</p>\n\n\n## News\n<!-- BEGIN NEWS -->\n- **[2026-02] `FEAT`:** Realtime Voice Agent support. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/realtime_voice_agent) | [Multi-Agent Realtime Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/workflows/multiagent_realtime) | [Tutorial](https://doc.agentscope.io/tutorial/task_realtime.html)\n- **[2026-01] `COMM`:** Biweekly Meetings launched to share ecosystem updates and development plans - join us! [Details & Schedule](https://github.com/agentscope-ai/agentscope/discussions/1126)\n- **[2026-01] `FEAT`:** Database support & memory compression in memory module. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/short_term_memory/memory_compression) | [Tutorial](https://doc.agentscope.io/tutorial/task_memory.html)\n- **[2025-12] `INTG`:** A2A (Agent-to-Agent) protocol support. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/a2a_agent) | [Tutorial](https://doc.agentscope.io/tutorial/task_a2a.html)\n- **[2025-12] `FEAT`:** TTS (Text-to-Speech) support. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/tts) | [Tutorial](https://doc.agentscope.io/tutorial/task_tts.html)\n- **[2025-11] `INTG`:** Anthropic Agent Skill support. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/agent_skill) | [Tutorial](https://doc.agentscope.io/tutorial/task_agent_skill.html)\n- **[2025-11] `RELS`:** Alias-Agent for diverse real-world tasks and Data-Juicer Agent for data processing open-sourced. [Alias-Agent](https://github.com/agentscope-ai/agentscope-samples/tree/main/alias) | [Data-Juicer Agent](https://github.com/agentscope-ai/agentscope-samples/tree/main/data_juicer_agent)\n- **[2025-11] `INTG`:** Agentic RL via Trinity-RFT library. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/tuner/react_agent) | [Trinity-RFT](https://github.com/agentscope-ai/Trinity-RFT)\n- **[2025-11] `INTG`:** ReMe for enhanced long-term memory. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/long_term_memory/reme)\n- **[2025-11] `RELS`:** agentscope-samples repository launched and agentscope-runtime upgraded with Docker/K8s deployment and VNC-powered GUI sandboxes. [Samples](https://github.com/agentscope-ai/agentscope-samples) | [Runtime](https://github.com/agentscope-ai/agentscope-runtime)\n<!-- END NEWS -->\n\n[More news →](./docs/NEWS.md)\n\n## Community\n\nWelcome to join our community on\n\n| [Discord](https://discord.gg/eYMpfnkG8h)                                                                                         | DingTalk                                                                  |\n|----------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|\n| <img src=\"https://gw.alicdn.com/imgextra/i1/O1CN01hhD1mu1Dd3BWVUvxN_!!6000000000238-2-tps-400-400.png\" width=\"100\" height=\"100\"> | <img src=\"./assets/images/dingtalk_qr_code.png\" width=\"100\" height=\"100\"> |\n\n<!-- START doctoc generated TOC please keep comment here to allow auto update -->\n<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->\n## 📑 Table of Contents\n\n- [Quickstart](#quickstart)\n  - [Installation](#installation)\n    - [From PyPI](#from-pypi)\n    - [From source](#from-source)\n- [Example](#example)\n  - [Hello AgentScope!](#hello-agentscope)\n  - [Voice Agent](#voice-agent)\n  - [Realtime Voice Agent](#realtime-voice-agent)\n  - [Human-in-the-loop](#human-in-the-loop)\n  - [Flexible MCP Usage](#flexible-mcp-usage)\n  - [Agentic RL](#agentic-rl)\n  - [Multi-Agent Workflows](#multi-agent-workflows)\n- [Documentation](#documentation)\n- [More Examples & Samples](#more-examples--samples)\n  - [Functionality](#functionality)\n  - [Agent](#agent)\n  - [Game](#game)\n  - [Workflow](#workflow)\n  - [Evaluation](#evaluation)\n  - [Tuner](#tuner)\n- [Contributing](#contributing)\n- [License](#license)\n- [Publications](#publications)\n- [Contributors](#contributors)\n\n<!-- END doctoc generated TOC please keep comment here to allow auto update -->\n\n## Quickstart\n\n### Installation\n\n> AgentScope requires **Python 3.10** or higher.\n\n#### From PyPI\n\n```bash\npip install agentscope\n```\n\nOr with uv:\n\n```bash\nuv pip install agentscope\n```\n\n#### From source\n\n```bash\n# Pull the source code from GitHub\ngit clone -b main https://github.com/agentscope-ai/agentscope.git\n\n# Install the package in editable mode\ncd agentscope\n\npip install -e .\n# or with uv:\n# uv pip install -e .\n```\n\n\n## Example\n\n### Hello AgentScope!\n\nStart with a conversation between user and a ReAct agent 🤖 named \"Friday\"!\n\n```python\nfrom agentscope.agent import ReActAgent, UserAgent\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.tool import Toolkit, execute_python_code, execute_shell_command\nimport os, asyncio\n\n\nasync def main():\n    toolkit = Toolkit()\n    toolkit.register_tool_function(execute_python_code)\n    toolkit.register_tool_function(execute_shell_command)\n\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"You're a helpful assistant named Friday.\",\n        model=DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            stream=True,\n        ),\n        memory=InMemoryMemory(),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n    )\n\n    user = UserAgent(name=\"user\")\n\n    msg = None\n    while True:\n        msg = await agent(msg)\n        msg = await user(msg)\n        if msg.get_text_content() == \"exit\":\n            break\n\nasyncio.run(main())\n```\n\n### Voice Agent\n\nCreate a voice-enabled ReAct agent that can understand and respond with speech, even playing a multi-agent werewolf game with voice interactions.\n\n\nhttps://github.com/user-attachments/assets/c5f05254-aff6-4375-90df-85e8da95d5da\n\n\n### Realtime Voice Agent\n\nBuild a realtime voice agent with web interface that can interact with users via voice input and output.\n\n[Realtime chatbot](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/realtime_voice_agent) | [Realtime Multi-Agent Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/workflows/multiagent_realtime)\n\nhttps://github.com/user-attachments/assets/1b7b114b-e995-4586-9b3f-d3bb9fcd2558\n\n\n\n### Human-in-the-loop\n\nSupport realtime interruption in ReActAgent: conversation can be interrupted via cancellation in realtime and resumed\nseamlessly via robust memory preservation.\n\n<img src=\"./assets/images/realtime_steering_en.gif\" alt=\"Realtime Steering\" width=\"60%\"/>\n\n### Flexible MCP Usage\n\nUse individual MCP tools as **local callable functions** to compose toolkits or wrap into a more complex tool.\n\n```python\nfrom agentscope.mcp import HttpStatelessClient\nfrom agentscope.tool import Toolkit\nimport os\n\nasync def fine_grained_mcp_control():\n    # Initialize the MCP client\n    client = HttpStatelessClient(\n        name=\"gaode_mcp\",\n        transport=\"streamable_http\",\n        url=f\"https://mcp.amap.com/mcp?key={os.environ['GAODE_API_KEY']}\",\n    )\n\n    # Obtain the MCP tool as a **local callable function**, and use it anywhere\n    func = await client.get_callable_function(func_name=\"maps_geo\")\n\n    # Option 1: Call directly\n    await func(address=\"Tiananmen Square\", city=\"Beijing\")\n\n    # Option 2: Pass to agent as a tool\n    toolkit = Toolkit()\n    toolkit.register_tool_function(func)\n    # ...\n\n    # Option 3: Wrap into a more complex tool\n    # ...\n```\n\n### Agentic RL\n\nTrain your agentic application seamlessly with Reinforcement Learning integration. We also prepare multiple sample projects covering various scenarios:\n\n| Example                                                                                          | Description                                                 | Model                  | Training Result             |\n|--------------------------------------------------------------------------------------------------|-------------------------------------------------------------|------------------------|-----------------------------|\n| [Math Agent](https://github.com/agentscope-ai/agentscope-samples/tree/main/tuner/math_agent)     | Tune a math-solving agent with multi-step reasoning.        | Qwen3-0.6B             | Accuracy: 75% → 85%         |\n| [Frozen Lake](https://github.com/agentscope-ai/agentscope-samples/tree/main/tuner/frozen_lake)   | Train an agent to navigate the Frozen Lake environment.     | Qwen2.5-3B-Instruct    | Success rate: 15% → 86%     |\n| [Learn to Ask](https://github.com/agentscope-ai/agentscope-samples/tree/main/tuner/learn_to_ask) | Tune agents using LLM-as-a-judge for automated feedback.    | Qwen2.5-7B-Instruct    | Accuracy: 47% → 92%         |\n| [Email Search](https://github.com/agentscope-ai/agentscope-samples/tree/main/tuner/email_search) | Improve tool-use capabilities without labeled ground truth. | Qwen3-4B-Instruct-2507 | Accuracy: 60%               |\n| [Werewolf Game](https://github.com/agentscope-ai/agentscope-samples/tree/main/tuner/werewolves)  | Train agents for strategic multi-agent game interactions.   | Qwen2.5-7B-Instruct    | Werewolf win rate: 50% → 80% |\n| [Data Augment](https://github.com/agentscope-ai/agentscope-samples/tree/main/tuner/data_augment) | Generate synthetic training data to enhance tuning results. | Qwen3-0.6B             | AIME-24 accuracy: 20% → 60% |\n\n### Multi-Agent Workflows\n\nAgentScope provides ``MsgHub`` and pipelines to streamline multi-agent conversations, offering efficient message routing and seamless information sharing\n\n```python\nfrom agentscope.pipeline import MsgHub, sequential_pipeline\nfrom agentscope.message import Msg\nimport asyncio\n\nasync def multi_agent_conversation():\n    # Create agents\n    agent1 = ...\n    agent2 = ...\n    agent3 = ...\n    agent4 = ...\n\n    # Create a message hub to manage multi-agent conversation\n    async with MsgHub(\n        participants=[agent1, agent2, agent3],\n        announcement=Msg(\"Host\", \"Introduce yourselves.\", \"assistant\")\n    ) as hub:\n        # Speak in a sequential manner\n        await sequential_pipeline([agent1, agent2, agent3])\n        # Dynamic manage the participants\n        hub.add(agent4)\n        hub.delete(agent3)\n        await hub.broadcast(Msg(\"Host\", \"Goodbye!\", \"assistant\"))\n\nasyncio.run(multi_agent_conversation())\n```\n\n\n## Documentation\n\n- [Tutorial](https://doc.agentscope.io/tutorial/)\n- [FAQ](https://doc.agentscope.io/tutorial/faq.html)\n- [API Docs](https://doc.agentscope.io/api/agentscope.html)\n\n## More Examples & Samples\n\n### Functionality\n\n- [MCP](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/mcp)\n- [Anthropic Agent Skill](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/agent_skill)\n- [Plan](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/plan)\n- [Structured Output](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/structured_output)\n- [RAG](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/rag)\n- [Long-Term Memory](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/long_term_memory)\n- [Session with SQLite](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/session_with_sqlite)\n- [Stream Printing Messages](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/stream_printing_messages)\n- [TTS](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/tts)\n- [Code-first Deployment](https://github.com/agentscope-ai/agentscope/tree/main/examples/deployment/planning_agent)\n- [Memory Compression](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/short_term_memory/memory_compression)\n\n### Agent\n\n- [ReAct Agent](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/react_agent)\n- [Voice Agent](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/voice_agent)\n- [Deep Research Agent](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/deep_research_agent)\n- [Browser-use Agent](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/browser_agent)\n- [Meta Planner Agent](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/meta_planner_agent)\n- [A2A Agent](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/a2a_agent)\n- [Realtime Voice Agent](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/realtime_voice_agent)\n\n### Game\n\n- [Nine-player Werewolves](https://github.com/agentscope-ai/agentscope/tree/main/examples/game/werewolves)\n\n### Workflow\n\n- [Multi-agent Debate](https://github.com/agentscope-ai/agentscope/tree/main/examples/workflows/multiagent_debate)\n- [Multi-agent Conversation](https://github.com/agentscope-ai/agentscope/tree/main/examples/workflows/multiagent_conversation)\n- [Multi-agent Concurrent](https://github.com/agentscope-ai/agentscope/tree/main/examples/workflows/multiagent_concurrent)\n- [Multi-agent Realtime Conversation](https://github.com/agentscope-ai/agentscope/tree/main/examples/workflows/multiagent_realtime)\n\n### Evaluation\n\n- [ACEBench](https://github.com/agentscope-ai/agentscope/tree/main/examples/evaluation/ace_bench)\n\n### Tuner\n\n- [Tune ReAct Agent](https://github.com/agentscope-ai/agentscope/tree/main/examples/tuner/react_agent)\n\n\n## Contributing\n\nWe welcome contributions from the community! Please refer to our [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines\non how to contribute.\n\n## License\n\nAgentScope is released under Apache License 2.0.\n\n## Publications\n\nIf you find our work helpful for your research or application, please cite our papers.\n\n- [AgentScope 1.0: A Developer-Centric Framework for Building Agentic Applications](https://arxiv.org/abs/2508.16279)\n\n- [AgentScope: A Flexible yet Robust Multi-Agent Platform](https://arxiv.org/abs/2402.14034)\n\n```\n@article{agentscope_v1,\n    author  = {Dawei Gao, Zitao Li, Yuexiang Xie, Weirui Kuang, Liuyi Yao, Bingchen Qian, Zhijian Ma, Yue Cui, Haohao Luo, Shen Li, Lu Yi, Yi Yu, Shiqi He, Zhiling Luo, Wenmeng Zhou, Zhicheng Zhang, Xuguang He, Ziqian Chen, Weikai Liao, Farruh Isakulovich Kushnazarov, Yaliang Li, Bolin Ding, Jingren Zhou}\n    title   = {AgentScope 1.0: A Developer-Centric Framework for Building Agentic Applications},\n    journal = {CoRR},\n    volume  = {abs/2508.16279},\n    year    = {2025},\n}\n\n@article{agentscope,\n    author  = {Dawei Gao, Zitao Li, Xuchen Pan, Weirui Kuang, Zhijian Ma, Bingchen Qian, Fei Wei, Wenhao Zhang, Yuexiang Xie, Daoyuan Chen, Liuyi Yao, Hongyi Peng, Zeyu Zhang, Lin Zhu, Chen Cheng, Hongzhu Shi, Yaliang Li, Bolin Ding, Jingren Zhou}\n    title   = {AgentScope: A Flexible yet Robust Multi-Agent Platform},\n    journal = {CoRR},\n    volume  = {abs/2402.14034},\n    year    = {2024},\n}\n```\n\n## Contributors\n\nAll thanks to our contributors:\n\n<a href=\"https://github.com/agentscope-ai/agentscope/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=agentscope-ai/agentscope&max=999&columns=12&anon=1\" />\n</a>\n"
  },
  {
    "path": "README_zh.md",
    "content": "<p align=\"center\">\n  <img\n    src=\"https://img.alicdn.com/imgextra/i1/O1CN01nTg6w21NqT5qFKH1u_!!6000000001621-55-tps-550-550.svg\"\n    alt=\"AgentScope Logo\"\n    width=\"200\"\n  />\n</p>\n\n<span align=\"center\">\n\n[**English Homepage**](https://github.com/agentscope-ai/agentscope/blob/main/README.md) | [**Tutorial**](https://doc.agentscope.io/zh_CN/) | [**Roadmap (Jan 2026 -)**](https://github.com/agentscope-ai/agentscope/blob/main/docs/roadmap.md) | [**FAQ**](https://doc.agentscope.io/zh_CN/tutorial/faq.html)\n\n</span>\n\n<p align=\"center\">\n    <a href=\"https://arxiv.org/abs/2402.14034\">\n        <img\n            src=\"https://img.shields.io/badge/cs.MA-2402.14034-B31C1C?logo=arxiv&logoColor=B31C1C\"\n            alt=\"arxiv\"\n        />\n    </a>\n    <a href=\"https://pypi.org/project/agentscope/\">\n        <img\n            src=\"https://img.shields.io/badge/python-3.10+-blue?logo=python\"\n            alt=\"pypi\"\n        />\n    </a>\n    <a href=\"https://pypi.org/project/agentscope/\">\n        <img\n            src=\"https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fpypi.org%2Fpypi%2Fagentscope%2Fjson&query=%24.info.version&prefix=v&logo=pypi&label=version\"\n            alt=\"pypi\"\n        />\n    </a>\n    <a href=\"https://discord.gg/eYMpfnkG8h\">\n        <img\n            src=\"https://img.shields.io/discord/1194846673529213039?label=Discord&logo=discord\"\n            alt=\"discord\"\n        />\n    </a>\n    <a href=\"https://doc.agentscope.io/\">\n        <img\n            src=\"https://img.shields.io/badge/Docs-English%7C%E4%B8%AD%E6%96%87-blue?logo=markdown\"\n            alt=\"docs\"\n        />\n    </a>\n    <a href=\"./LICENSE\">\n        <img\n            src=\"https://img.shields.io/badge/license-Apache--2.0-black\"\n            alt=\"license\"\n        />\n    </a>\n</p>\n\n<p align=\"center\">\n<img src=\"https://trendshift.io/api/badge/repositories/10079\" alt=\"modelscope%2Fagentscope | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/>\n</p>\n\n## What is AgentScope？\n\nAgentScope 是一款企业级开箱即用的智能体框架，提供灵活的核心抽象以适配不断进化的模型能力，并原生支持模型微调。\n\n我们为新一代自主智能的大语言模型而生。 我们的理念是释放模型的推理与工具调用潜能，而不是用僵化的提示工程和预设流程束缚它们的手脚。\n\n## Why use AgentScope？\n\n- **简单**: 使用内置的 ReAct 智能体、工具、技能、人机协作、记忆、计划、实时语音、评估和模型微调轻松构建智能体应用\n- **可扩展**: 大量生态系统集成，包括工具、记忆和可观察性；内置 MCP 和 A2A 支持；消息中心（MsgHub）提供灵活的多智能体编排能力\n- **生产就绪**: 在本地、云端 Serverless 或 K8s 集群上轻松部署智能体应用，并内置 OTel 可观察性支持\n\n\n<p align=\"center\">\n<img src=\"./assets/images/agentscope.png\" width=\"90%\" alt=\"AgentScope 生态系统\" />\n<br/>\nAgentScope 生态\n</p>\n\n\n## 📢 新闻\n<!-- BEGIN NEWS -->\n- **[2026-02] `功能`:** 支持实时语音交互。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/realtime_voice_agent) | [多智能体实时交互](https://github.com/agentscope-ai/agentscope/tree/main/examples/workflows/multiagent_realtime) | [文档](https://doc.agentscope.io/tutorial/task_realtime.html)\n- **[2026-01] `社区`:** AgentScope 双周会议启动，分享生态更新和开发计划 - 欢迎加入！[详情与安排](https://github.com/agentscope-ai/agentscope/discussions/1126)\n- **[2026-01] `功能`:** 记忆模块新增数据库支持和记忆压缩。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/short_term_memory/memory_compression) | [教程](https://doc.agentscope.io/tutorial/task_memory.html)\n- **[2025-12] `集成`:** A2A（智能体间通信）协议支持。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/a2a_agent) | [教程](https://doc.agentscope.io/zh_CN/tutorial/task_a2a.html)\n- **[2025-12] `功能`:** TTS（文本转语音）支持。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/tts) | [教程](https://doc.agentscope.io/zh_CN/tutorial/task_tts.html)\n- **[2025-11] `集成`:** Anthropic Agent Skill 支持。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/agent_skill) | [教程](https://doc.agentscope.io/zh_CN/tutorial/task_agent_skill.html)\n- **[2025-11] `发布`:** 面向多样化真实任务的 Alias-Agent 和数据处理的 Data-Juicer Agent 开源。[Alias-Agent](https://github.com/agentscope-ai/agentscope-samples/tree/main/alias) | [Data-Juicer Agent](https://github.com/agentscope-ai/agentscope-samples/tree/main/data_juicer_agent)\n- **[2025-11] `集成`:** 通过 Trinity-RFT 库实现智能体强化学习。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/tuner/react_agent) | [Trinity-RFT](https://github.com/agentscope-ai/Trinity-RFT)\n- **[2025-11] `集成`:** ReMe 增强长期记忆。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/long_term_memory/reme)\n- **[2025-11] `发布`:** agentscope-samples 样例库上线，agentscope-runtime 升级支持 Docker/K8s 部署和 VNC 图形沙盒。[样例库](https://github.com/agentscope-ai/agentscope-samples) | [Runtime](https://github.com/agentscope-ai/agentscope-runtime)\n<!-- END NEWS -->\n\n[更多新闻 →](./docs/NEWS_zh.md)\n\n## 联系我们\n\n欢迎加入我们的社区！\n\n| [Discord](https://discord.gg/eYMpfnkG8h)                                                                                         | 钉钉                                                                        |\n|----------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|\n| <img src=\"https://gw.alicdn.com/imgextra/i1/O1CN01hhD1mu1Dd3BWVUvxN_!!6000000000238-2-tps-400-400.png\" width=\"100\" height=\"100\"> | <img src=\"./assets/images/dingtalk_qr_code.png\" width=\"100\" height=\"100\"> |\n\n<!-- START doctoc generated TOC please keep comment here to allow auto update -->\n<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->\n## 📑 Table of Contents\n\n- [快速开始](#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B)\n  - [安装](#%E5%AE%89%E8%A3%85)\n    - [从 PyPI 安装](#%E4%BB%8E-pypi-%E5%AE%89%E8%A3%85)\n    - [从源码安装](#%E4%BB%8E%E6%BA%90%E7%A0%81%E5%AE%89%E8%A3%85)\n- [样例](#%E6%A0%B7%E4%BE%8B)\n  - [Hello AgentScope！](#hello-agentscope)\n  - [语音智能体](#%E8%AF%AD%E9%9F%B3%E6%99%BA%E8%83%BD%E4%BD%93)\n  - [实时语音智能体](#%E5%AE%9E%E6%97%B6%E8%AF%AD%E9%9F%B3%E6%99%BA%E8%83%BD%E4%BD%93)\n  - [人机协作](#%E4%BA%BA%E6%9C%BA%E5%8D%8F%E4%BD%9C)\n  - [灵活的 MCP 控制](#%E7%81%B5%E6%B4%BB%E7%9A%84-mcp-%E6%8E%A7%E5%88%B6)\n  - [智能体强化学习](#%E6%99%BA%E8%83%BD%E4%BD%93%E5%BC%BA%E5%8C%96%E5%AD%A6%E4%B9%A0)\n  - [多智能体工作流](#%E5%A4%9A%E6%99%BA%E8%83%BD%E4%BD%93%E5%B7%A5%E4%BD%9C%E6%B5%81)\n- [文档](#%E6%96%87%E6%A1%A3)\n- [更多样例](#%E6%9B%B4%E5%A4%9A%E6%A0%B7%E4%BE%8B)\n  - [功能](#%E5%8A%9F%E8%83%BD)\n  - [智能体](#%E6%99%BA%E8%83%BD%E4%BD%93)\n  - [游戏](#%E6%B8%B8%E6%88%8F)\n  - [工作流](#%E5%B7%A5%E4%BD%9C%E6%B5%81)\n  - [评估](#%E8%AF%84%E4%BC%B0)\n  - [微调](#%E5%BE%AE%E8%B0%83)\n- [贡献](#%E8%B4%A1%E7%8C%AE)\n- [许可](#%E8%AE%B8%E5%8F%AF)\n- [论文](#%E8%AE%BA%E6%96%87)\n- [贡献者](#%E8%B4%A1%E7%8C%AE%E8%80%85)\n\n<!-- END doctoc generated TOC please keep comment here to allow auto update -->\n\n## 快速开始\n\n### 安装\n\n> AgentScope 需要 **Python 3.10** 或更高版本。\n\n#### 从 PyPI 安装\n\n```bash\npip install agentscope\n```\n\n或使用 uv：\n\n```bash\nuv pip install agentscope\n```\n\n#### 从源码安装\n\n```bash\n# 从 GitHub 拉取源码\ngit clone -b main https://github.com/agentscope-ai/agentscope.git\n\n# 以可编辑模式安装包\ncd agentscope\n\npip install -e .\n# 或使用 uv：\n# uv pip install -e .\n```\n\n## 样例\n\n### Hello AgentScope！\n\n开始与名为\"Friday\"的 ReAct 智能体 🤖 进行对话！\n\n```python\nfrom agentscope.agent import ReActAgent, UserAgent\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.tool import Toolkit, execute_python_code, execute_shell_command\nimport os, asyncio\n\n\nasync def main():\n    toolkit = Toolkit()\n    toolkit.register_tool_function(execute_python_code)\n    toolkit.register_tool_function(execute_shell_command)\n\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"You're a helpful assistant named Friday.\",\n        model=DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            stream=True,\n        ),\n        memory=InMemoryMemory(),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n    )\n\n    user = UserAgent(name=\"user\")\n\n    msg = None\n    while True:\n        msg = await agent(msg)\n        msg = await user(msg)\n        if msg.get_text_content() == \"exit\":\n            break\n\nasyncio.run(main())\n```\n\n### 语音智能体\n\n创建支持语音的 ReAct 智能体，能够理解语音并进行语音回复，还可以使用语音交互玩多智能体狼人杀游戏。\n\nhttps://github.com/user-attachments/assets/559af387-fd6f-4f0c-b882-cd4778214801\n\n\n### 实时语音智能体\n\n使用 AgentScope 轻松构建实时交互的智能体应用，提供统一的事件接口和工具调用支持。\n\n[实时语音智能体](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/realtime_voice_agent) | [多智能体实时交互](https://github.com/agentscope-ai/agentscope/tree/main/examples/workflows/multiagent_realtime)\n\nhttps://github.com/user-attachments/assets/d9674ad5-f71d-43d5-a341-5bada318aee0\n\n\n\n### 人机协作\n\n在 ReActAgent 中支持实时打断：可以通过取消操作实时中断对话，并通过强大的记忆保留机制无缝恢复。\n\n<img src=\"./assets/images/realtime_steering_zh.gif\" alt=\"Realtime Steering\" width=\"60%\"/>\n\n### 灵活的 MCP 控制\n\nAgentScope 支持将单个 MCP 工具作为**本地可调用函数**使用，装备给智能体或封装为更复杂的工具。\n\n```python\nfrom agentscope.mcp import HttpStatelessClient\nfrom agentscope.tool import Toolkit\nimport os\n\nasync def fine_grained_mcp_control():\n    # 以高德MCP为例，初始化MCP客户端\n    client = HttpStatelessClient(\n        name=\"gaode_mcp\",\n        transport=\"streamable_http\",\n        url=f\"https://mcp.amap.com/mcp?key={os.environ['GAODE_API_KEY']}\",\n    )\n\n    # 将 MCP 工具获取为**本地可调用函数**，并在任何地方使用\n    func = await client.get_callable_function(func_name=\"maps_geo\")\n\n    # 选项 1：直接调用\n    await func(address=\"天安门广场\", city=\"北京\")\n\n    # 选项 2：作为工具传递给智能体\n    toolkit = Toolkit()\n    toolkit.register_tool_function(func)\n    # ...\n\n    # 选项 3：包装为更复杂的工具\n    # ...\n```\n\n### 智能体强化学习\n\n通过强化学习集成无缝训练智能体应用。我们还准备了涵盖各种场景的样例项目：\n\n| 样例                                                                                               | 描述                         | 模型                     | 训练结果                        |\n|--------------------------------------------------------------------------------------------------|----------------------------|------------------------|-----------------------------|\n| [Math Agent](https://github.com/agentscope-ai/agentscope-samples/tree/main/tuner/math_agent)     | 通过多步推理调优数学求解智能体。           | Qwen3-0.6B             | Accuracy: 75% → 85%         |\n| [Frozen Lake](https://github.com/agentscope-ai/agentscope-samples/tree/main/tuner/frozen_lake)   | 训练智能体进行冰湖游戏。               | Qwen2.5-3B-Instruct    | Success rate: 15% → 86%     |\n| [Learn to Ask](https://github.com/agentscope-ai/agentscope-samples/tree/main/tuner/learn_to_ask) | 使用 LLM 作为评判获得自动反馈，从而调优智能体。 | Qwen2.5-7B-Instruct    | Accuracy: 47% → 92%         |\n| [Email Search](https://github.com/agentscope-ai/agentscope-samples/tree/main/tuner/email_search) | 在训练数据没有标注真值的情况下提升工具使用能力。   | Qwen3-4B-Instruct-2507 | Accuracy: 60%               |\n| [Werewolf Game](https://github.com/agentscope-ai/agentscope-samples/tree/main/tuner/werewolves)  | 训练智能体进行战略性多智能体游戏互动。        | Qwen2.5-7B-Instruct    | 狼人胜率：50% → 80%              |\n| [Data Augment](https://github.com/agentscope-ai/agentscope-samples/tree/main/tuner/data_augment) | 生成合成训练数据以增强调优结果。           | Qwen3-0.6B             | AIME-24 accuracy: 20% → 60% |\n\n### 多智能体工作流\n\nAgentScope 提供 ``MsgHub`` 和 pipeline 来简化多智能体对话，提供高效的消息路由和无缝信息共享\n\n```python\nfrom agentscope.pipeline import MsgHub, sequential_pipeline\nfrom agentscope.message import Msg\nimport asyncio\n\nasync def multi_agent_conversation():\n    # 创建智能体\n    agent1 = ...\n    agent2 = ...\n    agent3 = ...\n    agent4 = ...\n\n    # 创建消息中心来管理多智能体对话\n    async with MsgHub(\n        participants=[agent1, agent2, agent3],\n        announcement=Msg(\"Host\", \"请介绍一下自己。\", \"assistant\")\n    ) as hub:\n        # 按顺序发言\n        await sequential_pipeline([agent1, agent2, agent3])\n        # 动态管理参与者\n        hub.add(agent4)\n        hub.delete(agent3)\n        await hub.broadcast(Msg(\"Host\", \"再见！\", \"assistant\"))\n\nasyncio.run(multi_agent_conversation())\n```\n\n\n## 文档\n\n- [教程](https://doc.agentscope.io/zh_CN/tutorial/)\n- [常见问题](https://doc.agentscope.io/zh_CN/tutorial/faq.html)\n- [API 文档](https://doc.agentscope.io/zh_CN/api/agentscope.html)\n\n## 更多样例\n\n### 功能\n\n- [MCP](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/mcp)\n- [Anthropic 智能体技能](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/agent_skill)\n- [计划](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/plan)\n- [结构化输出](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/structured_output)\n- [RAG](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/rag)\n- [长期记忆](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/long_term_memory)\n- [基于 SQLite 的会话管理](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/session_with_sqlite)\n- [流式打印消息](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/stream_printing_messages)\n- [TTS](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/tts)\n- [高代码部署](https://github.com/agentscope-ai/agentscope/tree/main/examples/deployment/planning_agent)\n- [记忆压缩](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/short_term_memory/memory_compression)\n\n### 智能体\n\n- [ReAct 智能体](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/react_agent)\n- [语音智能体](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/voice_agent)\n- [Deep Research 智能体](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/deep_research_agent)\n- [Browser-use 智能体](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/browser_agent)\n- [Meta Planner 智能体](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/meta_planner_agent)\n- [A2A 智能体](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/a2a_agent)\n- [实时语音交互智能体](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/realtime_voice_agent)\n\n### 游戏\n\n- [九人制狼人杀](https://github.com/agentscope-ai/agentscope/tree/main/examples/game/werewolves)\n\n### 工作流\n\n- [多智能体辩论](https://github.com/agentscope-ai/agentscope/tree/main/examples/workflows/multiagent_debate)\n- [多智能体对话](https://github.com/agentscope-ai/agentscope/tree/main/examples/workflows/multiagent_conversation)\n- [多智能体并发](https://github.com/agentscope-ai/agentscope/tree/main/examples/workflows/multiagent_concurrent)\n- [多智能体实时语音交互](https://github.com/agentscope-ai/agentscope/tree/main/examples/workflows/multiagent_realtime)\n\n### 评估\n\n- [ACEBench](https://github.com/agentscope-ai/agentscope/tree/main/examples/evaluation/ace_bench)\n\n### 微调\n\n- [调优 ReAct 智能体](https://github.com/agentscope-ai/agentscope/tree/main/examples/tuner/react_agent)\n\n\n## 贡献\n\n我们欢迎社区的贡献！请参阅我们的 [贡献指南](./CONTRIBUTING_zh.md) 了解如何贡献到 AgentScope。\n\n## 许可\n\nAgentScope 基于 Apache License 2.0 发布。\n\n## 论文\n\n如果我们的工作对您的研究或应用有帮助，请引用我们的论文。\n\n- [AgentScope 1.0: A Developer-Centric Framework for Building Agentic Applications](https://arxiv.org/abs/2508.16279)\n\n- [AgentScope: A Flexible yet Robust Multi-Agent Platform](https://arxiv.org/abs/2402.14034)\n\n```\n@article{agentscope_v1,\n    author  = {Dawei Gao, Zitao Li, Yuexiang Xie, Weirui Kuang, Liuyi Yao, Bingchen Qian, Zhijian Ma, Yue Cui, Haohao Luo, Shen Li, Lu Yi, Yi Yu, Shiqi He, Zhiling Luo, Wenmeng Zhou, Zhicheng Zhang, Xuguang He, Ziqian Chen, Weikai Liao, Farruh Isakulovich Kushnazarov, Yaliang Li, Bolin Ding, Jingren Zhou},\n    title   = {AgentScope 1.0: A Developer-Centric Framework for Building Agentic Applications},\n    journal = {CoRR},\n    volume  = {abs/2508.16279},\n    year    = {2025},\n}\n\n@article{agentscope,\n    author  = {Dawei Gao, Zitao Li, Xuchen Pan, Weirui Kuang, Zhijian Ma, Bingchen Qian, Fei Wei, Wenhao Zhang, Yuexiang Xie, Daoyuan Chen, Liuyi Yao, Hongyi Peng, Zeyu Zhang, Lin Zhu, Chen Cheng, Hongzhu Shi, Yaliang Li, Bolin Ding, Jingren Zhou},\n    title   = {AgentScope: A Flexible yet Robust Multi-Agent Platform},\n    journal = {CoRR},\n    volume  = {abs/2402.14034},\n    year    = {2024},\n}\n```\n\n## 贡献者\n\n感谢所有贡献者：\n\n<a href=\"https://github.com/agentscope-ai/agentscope/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=agentscope-ai/agentscope&max=999&columns=12&anon=1\" alt=\"贡献者\" />\n</a>\n"
  },
  {
    "path": "docs/NEWS.md",
    "content": "<!-- This is the source of truth for all NEWS items. -->\n<!-- The first 10 items are automatically synced to README.md and README_zh.md via GitHub Actions. -->\n<!-- To update news in READMEs, modify this file and push to trigger the workflow. -->\n\n- **[2026-02] `FEAT`:** Realtime Voice Agent support. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/realtime_voice_agent) | [Multi-Agent Realtime Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/workflows/multiagent_realtime) | [Tutorial](https://doc.agentscope.io/tutorial/task_realtime.html)\n- **[2026-01] `COMM`:** Biweekly Meetings launched to share ecosystem updates and development plans - join us! [Details & Schedule](https://github.com/agentscope-ai/agentscope/discussions/1126)\n- **[2026-01] `FEAT`:** Database support & memory compression in memory module. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/short_term_memory/memory_compression) | [Tutorial](https://doc.agentscope.io/tutorial/task_memory.html)\n- **[2025-12] `INTG`:** A2A (Agent-to-Agent) protocol support. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/a2a_agent) | [Tutorial](https://doc.agentscope.io/tutorial/task_a2a.html)\n- **[2025-12] `FEAT`:** TTS (Text-to-Speech) support. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/tts) | [Tutorial](https://doc.agentscope.io/tutorial/task_tts.html)\n- **[2025-11] `INTG`:** Anthropic Agent Skill support. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/agent_skill) | [Tutorial](https://doc.agentscope.io/tutorial/task_agent_skill.html)\n- **[2025-11] `RELS`:** Alias-Agent for diverse real-world tasks and Data-Juicer Agent for data processing open-sourced. [Alias-Agent](https://github.com/agentscope-ai/agentscope-samples/tree/main/alias) | [Data-Juicer Agent](https://github.com/agentscope-ai/agentscope-samples/tree/main/data_juicer_agent)\n- **[2025-11] `INTG`:** Agentic RL via Trinity-RFT library. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/tuner/react_agent) | [Trinity-RFT](https://github.com/agentscope-ai/Trinity-RFT)\n- **[2025-11] `INTG`:** ReMe for enhanced long-term memory. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/long_term_memory/reme)\n- **[2025-11] `RELS`:** agentscope-samples repository launched and agentscope-runtime upgraded with Docker/K8s deployment and VNC-powered GUI sandboxes. [Samples](https://github.com/agentscope-ai/agentscope-samples) | [Runtime](https://github.com/agentscope-ai/agentscope-runtime)\n- **[2025-11] `DOCS`:** Contributing Guide is online - welcome to contribute! [Guide](./CONTRIBUTING.md)\n- **[2025-09] `FEAT`:** RAG module released. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/rag) | [Tutorial](https://doc.agentscope.io/tutorial/task_rag.html)\n- **[2025-09] `FEAT`:** Voice agent support - ReActAgent now supports Qwen-Omni and GPT-Audio natively. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/voice_agent) | [Roadmap](https://github.com/agentscope-ai/agentscope/issues/773)\n- **[2025-09] `FEAT`:** Plan module released. [Tutorial](https://doc.agentscope.io/tutorial/task_plan.html)\n- **[2025-09] `RELS`:** AgentScope Runtime open-sourced - enabling effective agent deployment with sandboxed tool execution. [GitHub](https://github.com/agentscope-ai/agentscope-runtime)\n- **[2025-09] `RELS`:** AgentScope Studio open-sourced. [GitHub](https://github.com/agentscope-ai/agentscope-studio)\n- **[2025-08] `DOCS`:** Tutorial v1 is online. [Tutorial](https://doc.agentscope.io)\n- **[2025-08] `RELS`:** AgentScope v1 released - fully embracing asynchronous execution with many new features and improvements. [Changelog](https://github.com/agentscope-ai/agentscope/blob/main/docs/changelog.md)\n\n"
  },
  {
    "path": "docs/NEWS_zh.md",
    "content": "<!-- This is the source of truth for all NEWS items. -->\n<!-- The first 10 items are automatically synced to README.md and README_zh.md via GitHub Actions. -->\n<!-- To update news in READMEs, modify this file and push to trigger the workflow. -->\n\n- **[2026-02] `功能`:** 支持实时语音交互。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/realtime_voice_agent) | [多智能体实时交互](https://github.com/agentscope-ai/agentscope/tree/main/examples/workflows/multiagent_realtime) | [文档](https://doc.agentscope.io/tutorial/task_realtime.html)\n- **[2026-01] `社区`:** AgentScope 双周会议启动，分享生态更新和开发计划 - 欢迎加入！[详情与安排](https://github.com/agentscope-ai/agentscope/discussions/1126)\n- **[2026-01] `功能`:** 记忆模块新增数据库支持和记忆压缩。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/short_term_memory/memory_compression) | [教程](https://doc.agentscope.io/tutorial/task_memory.html)\n- **[2025-12] `集成`:** A2A（智能体间通信）协议支持。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/a2a_agent) | [教程](https://doc.agentscope.io/zh_CN/tutorial/task_a2a.html)\n- **[2025-12] `功能`:** TTS（文本转语音）支持。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/tts) | [教程](https://doc.agentscope.io/zh_CN/tutorial/task_tts.html)\n- **[2025-11] `集成`:** Anthropic Agent Skill 支持。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/agent_skill) | [教程](https://doc.agentscope.io/zh_CN/tutorial/task_agent_skill.html)\n- **[2025-11] `发布`:** 面向多样化真实任务的 Alias-Agent 和数据处理的 Data-Juicer Agent 开源。[Alias-Agent](https://github.com/agentscope-ai/agentscope-samples/tree/main/alias) | [Data-Juicer Agent](https://github.com/agentscope-ai/agentscope-samples/tree/main/data_juicer_agent)\n- **[2025-11] `集成`:** 通过 Trinity-RFT 库实现智能体强化学习。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/tuner/react_agent) | [Trinity-RFT](https://github.com/agentscope-ai/Trinity-RFT)\n- **[2025-11] `集成`:** ReMe 增强长期记忆。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/long_term_memory/reme)\n- **[2025-11] `发布`:** agentscope-samples 样例库上线，agentscope-runtime 升级支持 Docker/K8s 部署和 VNC 图形沙盒。[样例库](https://github.com/agentscope-ai/agentscope-samples) | [Runtime](https://github.com/agentscope-ai/agentscope-runtime)\n- **[2025-11] `文档`:** 贡献指南上线 - 欢迎参与贡献！[指南](./CONTRIBUTING_zh.md)\n- **[2025-09] `功能`:** RAG 模块发布。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/rag) | [教程](https://doc.agentscope.io/zh_CN/tutorial/task_rag.html)\n- **[2025-09] `功能`:** 语音智能体支持 - ReActAgent 原生支持 Qwen-Omni 和 GPT-Audio。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/voice_agent) | [Roadmap](https://github.com/agentscope-ai/agentscope/issues/773)\n- **[2025-09] `功能`:** Plan 模块发布。[教程](https://doc.agentscope.io/zh_CN/tutorial/task_plan.html)\n- **[2025-09] `发布`:** AgentScope Runtime 开源 - 支持沙盒化工具执行的高效智能体部署。[GitHub](https://github.com/agentscope-ai/agentscope-runtime)\n- **[2025-09] `发布`:** AgentScope Studio 开源。[GitHub](https://github.com/agentscope-ai/agentscope-studio)\n- **[2025-08] `文档`:** v1 版本教程上线。[教程](https://doc.agentscope.io/zh_CN/)\n- **[2025-08] `发布`:** AgentScope v1 发布 - 全面拥抱异步执行，诸多新功能和改进。[变更日志](https://github.com/agentscope-ai/agentscope/blob/main/docs/changelog.md)"
  },
  {
    "path": "docs/changelog.md",
    "content": "# CHANGELOG of v1.0.0\n\n> ➡️ change; ✅ new feature; ❌ deprecate\n\nThe overall changes from v0.x.x to v1.0.0 are summarized below.\n\n## Overview\n- ✅ Support asynchronous execution throughout the library\n- ✅ Support tools API thoroughly\n\n\n## ✨Session\n- ✅ Support automatic state management\n- ✅ Support session/application-level state management\n\n\n## ✨Tracing\n- ✅ Support OpenTelemetry-based tracing\n- ✅ Support third-party tracing platforms, e.g. Arize-Phoenix, Langfuse, etc.\n\n\n## ✨MCP\n- ✅ Support both client- and function-level control over MCP by a new MCP module\n- ✅ Support both \"pay-as-you-go\" and persistent session management\n- ✅ Support streamable HTTP, SSE and StdIO transport protocols\n\n\n## ✨Memory\n- ✅ Support long-term memory by providing a `LongTermMemoryBase` class\n- ✅ Provide a Mem0-based long-term memory implementation\n- ✅ Support both static- and agent-controlled long-term memory modes\n\n\n## Formatter\n- ✅ Support prompt construction/formatting with token count estimation\n- ✅ Support tools API in multi-agent prompt formatting\n\n\n## Model\n- ❌ Deprecate model configuration, use explicit object instantiation instead\n- ✅ Provide a new `ModelResponse` class for structured model responses\n- ✅ Support asynchronous model invocation\n- ✅ Support reasoning models\n- ✅ Support any combination of streaming/non-streaming, reasoning/non-reasoning and tools API\n\n\n## Agent\n- ❌ Deprecate `DialogAgent`, `DictDialogAgent` and prompt-based ReAct agent class\n- ➡️ Expose memory, formatter interfaces to the agent's constructor in ReActAgent\n- ➡️ Unify the signature of pre- and post- agent hooks\n- ✅ Support pre-/post-reasoning and pre-/post-acting hooks in ReActAgent class\n- ✅ Support asynchronous agent execution\n- ✅ Support interrupting agent's replying and customized interruption handling\n- ✅ Support automatic state management\n- ✅ Support parallel tool calls\n- ✅ Support two-modes long-term memory in ReActAgent class\n\n\n## Tool\n- ✅ Provide a more powerful `Toolkit` class for tools management\n- ✅ Provide a new `ToolResponse` class for structured and multimodal tool responses\n- ✅ Support group-wise tool management\n- ✅ Support agent to manage tools by itself\n- ✅ Support post-processing of tool responses\n- Tool function\n  - ✅ Support both async and sync functions\n  - ✅ Support both streaming and non-streaming return\n\n\n## Evaluation\n- ✅ Support ReAct agent-oriented evaluation\n- ✅ Support Ray-based distributed and concurrent evaluation\n- ✅ Support statistical analysis over evaluation results\n\n\n## AgentScope Studio\n- ✅ Support runtime tracing\n- ✅ Provide a built-in copilot agent named Friday\n\n\n## Logging\n- ❌ Deprecate `loguru` and use Python native `logging` module instead\n\n\n## Distribution\n- ❌ Deprecate distribution functionality momentarily, a new distribution module is coming soon\n\n\n## RAG\n- ❌ Deprecate RAG functionality momentarily, a new RAG module is coming soon\n\n\n## Parsers\n- ❌ Deprecate parsers module\n\n\n## WebBrowser\n- ❌ Deprecate the `WebBrowser` class and shift to MCP-based web browsing\n"
  },
  {
    "path": "docs/roadmap.md",
    "content": "# Roadmap\n\n## Long-term Goals\n\nOffering **agent-oriented programming (AOP)** as a new programming paradigm to organize the design and implementation of next-generation LLM-empowered applications.\n\n## Current Focus (January 2026 - )\n\n### 🎙️ Voice Agent\n\n**Voice agents** are a domain we are highly focused on, and AgentScope will continue to invest in this direction.\n\nAgentScope aims to build **production-ready** voice agents rather than demonstration prototypes. This means our voice agents will:\n\n- Support **production-grade** deployment, including seamless frontend integration\n- Support **tool invocation**, not just voice conversations\n- Support **multi-agent** voice interactions\n\n#### Development Roadmap\n\nOur development strategy for voice agents consists of **three progressive milestones**:\n\n1. **TTS Models** → 2. **Multimodal Models** → 3. **Real-time Multimodal Models**\n\n---\n\n#### Phase 1: TTS (Text-to-Speech) Models\n\n- **Build TTS model base class infrastructure**\n  - Design and implement a unified TTS model base class\n  - Establish standardized interfaces for TTS model integration\n\n- **Horizontal API expansion**\n  - Support mainstream TTS APIs (e.g., OpenAI TTS, Google TTS, Azure TTS, ElevenLabs, etc.)\n  - Ensure consistent behavior across different TTS providers\n\n---\n\n#### Phase 2: Multimodal Models (Non-Realtime)\n\n- **Enable ReAct agents with multimodal support**\n  - Integrate multimodal models (e.g., qwen3-omni, gpt-audio) into existing ReAct agent framework\n  - Support audio input/output in non-realtime mode\n\n- **Advanced multimodal agent capabilities**\n  - Enable tool invocation within multimodal conversations\n  - Support multi-agent workflows with multimodal communication\n\n---\n\n#### Phase 3: Real-time Multimodal Models\n\n\n- **Beyond request-response**: Explore streaming, interrupt handling, and concurrent multimodal processing\n- **New programming paradigms**: Design agent programming models specifically tailored for real-time interactions\n- **Production readiness**: Ensure low-latency performance, stability, and scalability for production deployment\n\n### 🛠️ Agent Skill\n\nProvide **production-ready** agent skill integration solutions.\n\n### 🌐 Ecosystem Expansion\n\n- **A2UI (Agent-to-UI)**: Enable seamless agent-to-user interface interactions\n- **A2A (Agent-to-Agent)**: Enhance agent-to-agent communication capabilities\n\n### 🚀 Agentic RL\n\n- Support using [Tinker](https://tinker-docs.thinkingmachines.ai/) backend to tune agent applications on devices without GPU.\n- Support tuning agent applications based on their run history.\n- Integrate with AgentScope Runtime to provide better environment abstraction.\n- Add more tutorials and examples on how to build complex judge functions with the help of evaluation module.\n- Add more tutorials and examples on data selection and augmentation.\n\n### 📈 Code Quality\n\nContinuous refinement and improvement of code quality and maintainability.\n\n# Completed Milestones\n\n### AgentScope V1.0.0 Roadmap\n\nWe are deeply grateful for the continuous support from the open-source community that has witnessed AgentScope's\ngrowth. Throughout our journey, we have maintained **developer-centric transparency** as our core principle,\nwhich will continue to guide our future development.\n\nAs the AI agent ecosystem rapidly evolves, we recognize the need to adapt AgentScope to meet emerging trends and\nrequirements. We are excited to announce the upcoming release of AgentScope v1.0.0, which marks a significant shift\ntowards deployment-focused and secondary development direction. This new version will provide comprehensive support for agent developers\nwith enhanced deployment capabilities and practical features. Specifically, the update will include:\n\n- ✨New Features\n  - 🛠️ Tool/MCP\n    - Support both sync/async tool functions\n    - Support streaming tool function\n    - Support parallel execution of tool functions\n    - Provide more flexible support for the MCP server\n\n  - 💾 Memory\n    - Enhance the existing short-term memory\n    - Support long-term memory\n\n  - 🤖 Agent\n    - Provide powerful ReAct-based out-of-the-box agents\n\n- 👨‍💻 Development\n  - Provide enhanced AgentScope Studio with visual components for developing, tracing and debugging\n  - Provide a built-in copilot for developing/drafting AgentScope applications\n\n- 🔍 Evaluation\n  - Provide built-in benchmarking and evaluation toolkit for agents\n  - Support result visualization\n\n- 🏗️ Deployment\n  - Support asynchronous agent execution\n  - Support session/state management\n  - Provide sandbox for tool execution\n\nStay tuned for our detailed release notes and beta version, which will be available soon. Follow our GitHub\nrepository and official channels for the latest updates. We look forward to your valuable feedback and continued\nsupport in shaping the future of AgentScope."
  },
  {
    "path": "docs/tutorial/_static/css/gallery.css",
    "content": ".sphx-glr-download-link-note.admonition.note {\n    display: none;\n}\n\n.sphx-glr-footer {\n    display: flex;\n    flex-direction: row;\n    gap: 8px;\n}\n\n.sphx-glr-download-zip {\n    display: none;\n}\n\n.bordered-image {\n    border: 1px solid #e5e5e5;\n}\n\n:root {\n    --item-card-width: 200px;\n    --item-card-margin: 10px;\n    --item-card-title-height: 50px;\n\n    --item-card-img-length: calc(var(--item-card-width) - 2*var(--item-card-margin));\n    --item-card-title-width: calc(var(--item-card-width) - 2*var(--item-card-margin));\n    --item-card-title-margin-top: var(--item-card-margin);\n\n    --item-card-height: calc(var(--item-card-margin) * 3 + var(--item-card-img-length) + var(--item-card-title-height));\n}\n\ntd .highlight-python.notranslate {\n    margin-bottom: 0 !important;\n}\n\n/*cite {*/\n/*    background: rgba(229, 229, 229, 0.69) !important;*/\n/*    padding-left: 0.25rem !important;*/\n/*    padding-right: 0.25rem !important;*/\n/*    border-radius: 4px !important;*/\n/*    font-style: normal !important;*/\n/*    font-weight: 600 !important;*/\n/*}*/\n\n.sidebar-brand-text {\n    display: flex;\n    justify-content: center;\n}\n\n.sidebar-logo-container .sidebar-logo {\n    max-height: 170px;\n    width: auto;\n    display: block;\n}\n\n.gallery-item {\n    position: relative;\n    display: inline-block;\n    width: var(--item-card-width);\n    height: var(--item-card-height);\n    box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);\n    margin: 7px;\n}\n\n.docutils.align-default {\n    white-space: normal !important;\n    max-width: 100% !important;\n    width: 100% !important;\n    td {\n        white-space: normal !important;\n    }\n}\n\n.sphx-glr-script-out.highlight-none.notranslate .highlight pre{\n    /*正常打印回车*/\n    white-space: pre-wrap !important;\n    /*white-space: normal !important;*/\n    max-width: 100% !important;\n    width: 100% !important;\n}\n\n.gallery-item-card {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: var(--item-card-width);\n    height: var(--item-card-height);\n    display: flex;\n    flex-direction: column;\n    margin: var(--item-card-margin);\n}\n\n.gallery-item-card-img {\n    height: var(--item-card-img-length);\n    width: var(--item-card-img-length);\n    min-width: var(--item-card-img-length);\n    min-height: var(--item-card-img-length);\n    display: block;\n}\n\n.gallery-item-card-title {\n    text-align: center;\n    margin-top: var(--item-card-margin);\n    font-weight: bold;\n    min-height: var(--item-card-title-height);\n    height: var(--item-card-title-height);\n    width: var(--item-card-title-width);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.gallery-item-description {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background-color: rgba(255, 255, 255, 0.9);\n    /*background-color: #1e8449;*/\n    color: black;\n    display: none;\n    justify-content: center;\n    align-items: flex-start;\n}\n\n.gallery-item:hover .gallery-item-description {\n    display: flex;\n    padding: 10px;\n    border: 1px solid rgba(0, 0, 0, 0.22);\n}\n\n.language-switch-button {\n    background: transparent;\n    display: flex;\n    align-content: center;\n    justify-content: center;\n    font-size: 15px;\n    margin-top: 0;\n    margin-bottom: 4px;\n    border: none;\n    color: rgb(4, 4, 4);\n    height: 20px;\n    width: 20px;\n    border-radius: 6px;\n    font-weight: 325;\n}\n\n.language-switch-button:hover {\n    color: #2758DA;\n}\n\n.version-select {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    background: transparent;\n    font-size: 14px;\n    border: 1px solid rgb(238, 235, 238);\n    margin-inline: 16px;\n    padding: 8px;\n    height: fit-content;\n    box-sizing: border-box;\n    font-weight: 600;\n    border-radius: 6px;\n    fill: rgb(238, 235, 238);\n}\n"
  },
  {
    "path": "docs/tutorial/_static/language_switch.js",
    "content": "function switchV1Language() {\n    if (window.location.href.includes(\"zh_CN\")) {\n        window.location.href = \"https://doc.agentscope.io\";\n    } else {\n        window.location.href = \"https://doc.agentscope.io/zh_CN\";\n    }\n}\n\n\nfunction navigateToV0(version) {\n    if (version === \"v0\") {\n        const suffix = window.location.href.includes(\"zh_CN\") ? \"/zh_CN\" : \"/en\";\n        window.location.href = \"https://doc.agentscope.io/v0\" + suffix;\n    }\n}\n"
  },
  {
    "path": "docs/tutorial/_templates/components/language-switch.html",
    "content": "{# _templates/components/language-switch.html #}\n<button onclick=\"switchV1Language()\" class=\"language-switch-button\" title=\"Switch to Chinese\">\n  <script>\n    document.write(window.location.href.includes(\"zh_CN\") ? \"En\": \"中\");\n  </script>\n</button>"
  },
  {
    "path": "docs/tutorial/_templates/module.rst_t",
    "content": "{{ basename |  heading }}\n.. automodule:: {{ qualname }}\n{%- for option in automodule_options %}\n   :{{ option }}:\n{%- endfor %}"
  },
  {
    "path": "docs/tutorial/_templates/package.rst_t",
    "content": "{%- macro automodule(modname, options) -%}\n.. automodule:: {{ modname }}\n{%- for option in options %}\n   :{{ option }}:\n{%- endfor %}\n{%- endmacro %}\n\n{{- pkgname | heading }}\n\n{{ automodule(pkgname, automodule_options) }}"
  },
  {
    "path": "docs/tutorial/_templates/page.html",
    "content": "{% extends \"base.html\" %}\n\n{% block body -%}\n{{ super() }}\n{% include \"partials/icons.html\" %}\n\n<input type=\"checkbox\" class=\"sidebar-toggle\" name=\"__navigation\" id=\"__navigation\">\n<input type=\"checkbox\" class=\"sidebar-toggle\" name=\"__toc\" id=\"__toc\">\n<label class=\"overlay sidebar-overlay\" for=\"__navigation\">\n  <div class=\"visually-hidden\">Hide navigation sidebar</div>\n</label>\n<label class=\"overlay toc-overlay\" for=\"__toc\">\n  <div class=\"visually-hidden\">Hide table of contents sidebar</div>\n</label>\n\n<a class=\"skip-to-content muted-link\" href=\"#furo-main-content\">\n  {%- trans -%}\n  Skip to content\n  {%- endtrans -%}\n</a>\n\n{% if theme_announcement -%}\n<div class=\"announcement\">\n  <aside class=\"announcement-content\">\n    {% block announcement %} {{ theme_announcement }} {% endblock announcement %}\n  </aside>\n</div>\n{%- endif %}\n\n<div class=\"page\">\n  <header class=\"mobile-header\">\n    <div class=\"header-left\">\n      <label class=\"nav-overlay-icon\" for=\"__navigation\">\n        <div class=\"visually-hidden\">Toggle site navigation sidebar</div>\n        <i class=\"icon\"><svg><use href=\"#svg-menu\"></use></svg></i>\n      </label>\n    </div>\n    <div class=\"header-center\">\n      <a href=\"{{ pathto(master_doc) }}\"><div class=\"brand\">{{ docstitle if docstitle else project }}</div></a>\n    </div>\n    <div class=\"header-right\">\n      <div class=\"theme-toggle-container theme-toggle-header\">\n        <button class=\"theme-toggle\">\n          <div class=\"visually-hidden\">Toggle Light / Dark / Auto color theme</div>\n          <svg class=\"theme-icon-when-auto-light\"><use href=\"#svg-sun-with-moon\"></use></svg>\n          <svg class=\"theme-icon-when-auto-dark\"><use href=\"#svg-moon-with-sun\"></use></svg>\n          <svg class=\"theme-icon-when-dark\"><use href=\"#svg-moon\"></use></svg>\n          <svg class=\"theme-icon-when-light\"><use href=\"#svg-sun\"></use></svg>\n        </button>\n      </div>\n      <label class=\"toc-overlay-icon toc-header-icon{% if furo_hide_toc %} no-toc{% endif %}\" for=\"__toc\">\n        <div class=\"visually-hidden\">Toggle table of contents sidebar</div>\n        <i class=\"icon\"><svg><use href=\"#svg-toc\"></use></svg></i>\n      </label>\n    </div>\n  </header>\n  <aside class=\"sidebar-drawer\">\n    <div class=\"sidebar-container\">\n      {% block left_sidebar %}\n      <div class=\"sidebar-sticky\">\n        {%- for sidebar_section in sidebars %}\n          {%- include sidebar_section %}\n        {%- endfor %}\n      </div>\n      {% endblock left_sidebar %}\n    </div>\n  </aside>\n  <div class=\"main\">\n    <div class=\"content\">\n      <div class=\"article-container\">\n        <a href=\"#\" class=\"back-to-top muted-link\">\n          <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\">\n            <path d=\"M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8v12z\"></path>\n          </svg>\n          <span>{% trans %}Back to top{% endtrans %}</span>\n        </a>\n        <div class=\"content-icon-container\">\n          {% if theme_top_of_page_button != \"edit\" -%}\n            {{ warning(\"Got configuration for 'top_of_page_button': this is deprecated.\") }}\n          {%- endif -%}\n\n          {%- if theme_top_of_page_buttons == \"\" -%}\n            {% if theme_top_of_page_button == None -%}\n              {#- We respect the old configuration of disabling all the buttons -#}\n              {%- set theme_top_of_page_buttons = [] -%}\n            {% else %}\n              {%- set theme_top_of_page_buttons = [\"view\", \"edit\"] -%}\n            {%- endif -%}\n          {% else -%}\n            {% if theme_top_of_page_button != \"edit\" -%}\n              {%- set theme_top_of_page_buttons = [] -%}\n              {{ warning(\"Got configuration for both 'top_of_page_button' and 'top_of_page_buttons', ignoring both and removing all top of page buttons.\") }}\n            {%- endif -%}\n          {%- endif -%}\n          {%- include \"components/language-switch.html\" with context -%}\n          {% for button in theme_top_of_page_buttons -%}\n            {% if button == \"view\" %}\n            {%- include \"components/view-this-page.html\" with context -%}\n            {% elif button == \"edit\" %}\n            {%- include \"components/edit-this-page.html\" with context -%}\n            {% else %}\n            {{ warning(\"Got an unsupported value in 'top_of_page_buttons' for theme configuration\") }}\n            {% endif %}\n          {%- endfor -%}\n          {#- Theme toggle -#}\n          <div class=\"theme-toggle-container theme-toggle-content\">\n            <button class=\"theme-toggle\">\n              <div class=\"visually-hidden\">Toggle Light / Dark / Auto color theme</div>\n              <svg class=\"theme-icon-when-auto-light\"><use href=\"#svg-sun-with-moon\"></use></svg>\n              <svg class=\"theme-icon-when-auto-dark\"><use href=\"#svg-moon-with-sun\"></use></svg>\n              <svg class=\"theme-icon-when-dark\"><use href=\"#svg-moon\"></use></svg>\n              <svg class=\"theme-icon-when-light\"><use href=\"#svg-sun\"></use></svg>\n            </button>\n          </div>\n          <label class=\"toc-overlay-icon toc-content-icon{% if furo_hide_toc %} no-toc{% endif %}\" for=\"__toc\">\n            <div class=\"visually-hidden\">Toggle table of contents sidebar</div>\n            <i class=\"icon\"><svg><use href=\"#svg-toc\"></use></svg></i>\n          </label>\n        </div>\n        <article role=\"main\" id=\"furo-main-content\">\n          {% block content %}{{ body }}{% endblock %}\n        </article>\n      </div>\n      <footer>\n        {% block footer %}\n        <div class=\"related-pages\">\n          {% if next -%}\n            <a class=\"next-page\" href=\"{{ next.link }}\">\n              <div class=\"page-info\">\n                <div class=\"context\">\n                  <span>{{ _(\"Next\") }}</span>\n                </div>\n                <div class=\"title\">{{ next.title }}</div>\n              </div>\n              <svg class=\"furo-related-icon\"><use href=\"#svg-arrow-right\"></use></svg>\n            </a>\n          {%- endif %}\n          {% if prev -%}\n            <a class=\"prev-page\" href=\"{{ prev.link }}\">\n              <svg class=\"furo-related-icon\"><use href=\"#svg-arrow-right\"></use></svg>\n              <div class=\"page-info\">\n                <div class=\"context\">\n                  <span>{{ _(\"Previous\") }}</span>\n                </div>\n                {% if prev.link == pathto(master_doc) %}\n                <div class=\"title\">{{ _(\"Home\") }}</div>\n                {% else %}\n                <div class=\"title\">{{ prev.title }}</div>\n                {% endif %}\n              </div>\n            </a>\n          {%- endif %}\n        </div>\n        <div class=\"bottom-of-page\">\n          <div class=\"left-details\">\n            {%- if show_copyright %}\n            <div class=\"copyright\">\n              {%- if hasdoc('copyright') %}\n                {% trans path=pathto('copyright'), copyright=copyright|e -%}\n                  <a href=\"{{ path }}\">Copyright</a> &#169; {{ copyright }}\n                {%- endtrans %}\n              {%- else %}\n                {% trans copyright=copyright|e -%}\n                  Copyright &#169; {{ copyright }}\n                {%- endtrans %}\n              {%- endif %}\n            </div>\n            {%- endif %}\n            {% trans %}Made with {% endtrans -%}\n            {%- if show_sphinx -%}\n            {% trans %}<a href=\"https://www.sphinx-doc.org/\">Sphinx</a> and {% endtrans -%}\n            <a class=\"muted-link\" href=\"https://pradyunsg.me\">@pradyunsg</a>'s\n            {% endif -%}\n            {% trans %}\n            <a href=\"https://github.com/pradyunsg/furo\">Furo</a>\n            {% endtrans %}\n            {%- if last_updated -%}\n            <div class=\"last-updated\">\n              {% trans last_updated=last_updated|e -%}\n                Last updated on {{ last_updated }}\n              {%- endtrans -%}\n            </div>\n            {%- endif %}\n          </div>\n          <div class=\"right-details\">\n            {% if theme_footer_icons or READTHEDOCS -%}\n            <div class=\"icons\">\n              {% if theme_footer_icons -%}\n              {% for icon_dict in theme_footer_icons -%}\n              <a class=\"muted-link {{ icon_dict.class }}\" href=\"{{ icon_dict.url }}\" aria-label=\"{{ icon_dict.name }}\">\n                {{- icon_dict.html -}}\n              </a>\n              {% endfor %}\n              {%- else -%}\n              {#- Show Read the Docs project -#}\n              {%- if READTHEDOCS and slug -%}\n              <a class=\"muted-link\" href=\"https://readthedocs.org/projects/{{ slug }}\" aria-label=\"On Read the Docs\">\n                <svg x=\"0px\" y=\"0px\" viewBox=\"-125 217 360 360\" xml:space=\"preserve\">\n                  <path fill=\"currentColor\" d=\"M39.2,391.3c-4.2,0.6-7.1,4.4-6.5,8.5c0.4,3,2.6,5.5,5.5,6.3 c0,0,18.5,6.1,50,8.7c25.3,2.1,54-1.8,54-1.8c4.2-0.1,7.5-3.6,7.4-7.8c-0.1-4.2-3.6-7.5-7.8-7.4c-0.5,0-1,0.1-1.5,0.2 c0,0-28.1,3.5-50.9,1.6c-30.1-2.4-46.5-7.9-46.5-7.9C41.7,391.3,40.4,391.1,39.2,391.3z M39.2,353.6c-4.2,0.6-7.1,4.4-6.5,8.5 c0.4,3,2.6,5.5,5.5,6.3c0,0,18.5,6.1,50,8.7c25.3,2.1,54-1.8,54-1.8c4.2-0.1,7.5-3.6,7.4-7.8c-0.1-4.2-3.6-7.5-7.8-7.4 c-0.5,0-1,0.1-1.5,0.2c0,0-28.1,3.5-50.9,1.6c-30.1-2.4-46.5-7.9-46.5-7.9C41.7,353.6,40.4,353.4,39.2,353.6z M39.2,315.9 c-4.2,0.6-7.1,4.4-6.5,8.5c0.4,3,2.6,5.5,5.5,6.3c0,0,18.5,6.1,50,8.7c25.3,2.1,54-1.8,54-1.8c4.2-0.1,7.5-3.6,7.4-7.8 c-0.1-4.2-3.6-7.5-7.8-7.4c-0.5,0-1,0.1-1.5,0.2c0,0-28.1,3.5-50.9,1.6c-30.1-2.4-46.5-7.9-46.5-7.9 C41.7,315.9,40.4,315.8,39.2,315.9z M39.2,278.3c-4.2,0.6-7.1,4.4-6.5,8.5c0.4,3,2.6,5.5,5.5,6.3c0,0,18.5,6.1,50,8.7 c25.3,2.1,54-1.8,54-1.8c4.2-0.1,7.5-3.6,7.4-7.8c-0.1-4.2-3.6-7.5-7.8-7.4c-0.5,0-1,0.1-1.5,0.2c0,0-28.1,3.5-50.9,1.6 c-30.1-2.4-46.5-7.9-46.5-7.9C41.7,278.2,40.4,278.1,39.2,278.3z M-13.6,238.5c-39.6,0.3-54.3,12.5-54.3,12.5v295.7 c0,0,14.4-12.4,60.8-10.5s55.9,18.2,112.9,19.3s71.3-8.8,71.3-8.8l0.8-301.4c0,0-25.6,7.3-75.6,7.7c-49.9,0.4-61.9-12.7-107.7-14.2 C-8.2,238.6-10.9,238.5-13.6,238.5z M19.5,257.8c0,0,24,7.9,68.3,10.1c37.5,1.9,75-3.7,75-3.7v267.9c0,0-19,10-66.5,6.6 C59.5,536.1,19,522.1,19,522.1L19.5,257.8z M-3.6,264.8c4.2,0,7.7,3.4,7.7,7.7c0,4.2-3.4,7.7-7.7,7.7c0,0-12.4,0.1-20,0.8 c-12.7,1.3-21.4,5.9-21.4,5.9c-3.7,2-8.4,0.5-10.3-3.2c-2-3.7-0.5-8.4,3.2-10.3c0,0,0,0,0,0c0,0,11.3-6,27-7.5 C-16,264.9-3.6,264.8-3.6,264.8z M-11,302.6c4.2-0.1,7.4,0,7.4,0c4.2,0.5,7.2,4.3,6.7,8.5c-0.4,3.5-3.2,6.3-6.7,6.7 c0,0-12.4,0.1-20,0.8c-12.7,1.3-21.4,5.9-21.4,5.9c-3.7,2-8.4,0.5-10.3-3.2c-2-3.7-0.5-8.4,3.2-10.3c0,0,11.3-6,27-7.5 C-20.5,302.9-15.2,302.7-11,302.6z M-3.6,340.2c4.2,0,7.7,3.4,7.7,7.7s-3.4,7.7-7.7,7.7c0,0-12.4-0.1-20,0.7 c-12.7,1.3-21.4,5.9-21.4,5.9c-3.7,2-8.4,0.5-10.3-3.2c-2-3.7-0.5-8.4,3.2-10.3c0,0,11.3-6,27-7.5C-16,340.1-3.6,340.2-3.6,340.2z\" />\n                </svg>\n              </a>\n              {%- endif -%}\n              {#- Show GitHub repository home -#}\n              {%- if READTHEDOCS and display_github and github_user != \"None\" and github_repo != \"None\" -%}\n              <a class=\"muted-link\" href=\"https://github.com/{{ github_user }}/{{ github_repo }}\" aria-label=\"On GitHub\">\n                <svg stroke=\"currentColor\" fill=\"currentColor\" stroke-width=\"0\" viewBox=\"0 0 16 16\">\n                  <path fill-rule=\"evenodd\" d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z\"></path>\n                </svg>\n              </a>\n              {%- endif -%}\n              {%- endif %}\n            </div>\n            {%- endif %}\n          </div>\n        </div>\n        {% endblock footer %}\n      </footer>\n    </div>\n    <aside class=\"toc-drawer{% if furo_hide_toc %} no-toc{% endif %}\">\n      {% block right_sidebar %}\n      {% if not furo_hide_toc %}\n      <div class=\"toc-sticky toc-scroll\">\n        <div class=\"toc-title-container\">\n          <span class=\"toc-title\">\n            {{ _(\"On this page\") }}\n          </span>\n        </div>\n        <div class=\"toc-tree-container\">\n          <div class=\"toc-tree\">\n            {{ toc }}\n          </div>\n        </div>\n      </div>\n      {% endif %}\n      {% endblock right_sidebar %}\n    </aside>\n  </div>\n</div>\n{%- endblock %}\n"
  },
  {
    "path": "docs/tutorial/_templates/sidebar/navigation.html",
    "content": "<div class=\"sidebar-tree\">\n    <p class=\"caption\" role=\"heading\">\n        <span class=\"caption-text\">\n            Version\n        </span>\n        <ul>\n            <li>\n                <select class=\"version-select\" onchange=\"navigateToV0(this.value)\">\n                    <option value=\"v1\" selected>Stable(v1.0)</option>\n                    <option value=\"v0\">v0.1.x</option>\n                </select>\n            </li>\n        </ul>\n    </p>\n    {{ furo_navigation_tree }}\n</div>\n"
  },
  {
    "path": "docs/tutorial/en/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?=\nSPHINXBUILD   ?= sphinx-build\nSOURCEDIR     = source\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "docs/tutorial/en/build.sh",
    "content": "#!/bin/bash\n\nset -e\n\n# Clean old build files\nrm -rf build/ doctrees/\n\n# Build the html\nsphinx-build -M html ./ build\n\n# Remove temporary files (double insurance)\nrm -rf build/html/.doctrees\nrm -f build/html/.buildinfo\nfind build/html -name \"*.pickle\" -delete\nfind build/html -name \"__pycache__\" -delete\nfind build/html -name \"*.pyc\" -delete\n\necho \"✅ English docs built successfully, temporary files cleaned\""
  },
  {
    "path": "docs/tutorial/en/conf.py",
    "content": "# -*- coding: utf-8 -*-\n# Configuration file for the Sphinx documentation builder.\n#\n# For the full list of built-in configuration values, see the documentation:\n# https://www.sphinx-doc.org/en/master/usage/configuration.html\n\n# -- Project information -----------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information\n\nproject = \"AgentScope\"\ncopyright = \"2025, Alibaba\"\nauthor = \"Alibaba Tongyi Lab\"\n\n# -- General configuration ---------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration\n\nextensions = [\n    \"myst_parser\",\n    \"sphinx_gallery.gen_gallery\",\n    \"sphinx.ext.autodoc\",\n    \"sphinx.ext.viewcode\",\n    \"sphinx.ext.napoleon\",\n]\n\nmyst_enable_extensions = [\n    \"colon_fence\",\n]\n\nsphinx_gallery_conf = {\n    \"download_all_examples\": False,\n    \"examples_dirs\": [\n        \"src\",\n    ],\n    \"gallery_dirs\": [\n        \"tutorial\",\n    ],\n    \"filename_pattern\": \"src/.*\\.py\",\n    \"example_extensions\": [\".py\"],\n}\n\ntemplates_path = [\"../_templates\"]\nexclude_patterns = [\"_build\", \"Thumbs.db\", \".DS_Store\"]\n\nlanguages = [\"en\", \"zh_CN\"]\nlanguage = \"en\"\n\n# -- Options for HTML output -------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output\n\nhtml_theme = \"furo\"\nhtml_title = (\n    \"<span style='font-weight: 700; color: #2196f3;'>AgentScope</span>\"\n)\nhtml_logo = \"../_static/images/logo.svg\"\nhtml_favicon = \"../_static/images/logo.svg\"\nhtml_static_path = [\"../_static\"]\nhtml_css_files = [\n    \"css/gallery.css\",\n]\n\nhtml_js_files = [\n    \"language_switch.js\",\n]\n\nhtml_theme_options = {\n    \"footer_icons\": [\n        {\n            \"name\": \"GitHub\",\n            \"url\": \"https://github.com/agentscope-ai/agentscope\",\n            \"html\": \"\"\"\n                <svg stroke=\"currentColor\" fill=\"currentColor\" stroke-width=\"0\" viewBox=\"0 0 16 16\">\n                    <path fill-rule=\"evenodd\" d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z\"></path>\n                </svg>\n            \"\"\",\n            \"class\": \"\",\n        },\n        {\n            \"name\": \"Discord\",\n            \"url\": \"https://discord.gg/eYMpfnkG8h\",\n            \"html\": \"\"\"\n                <svg stroke=\"currentColor\" fill=\"currentColor\" stroke-width=\"0\" t=\"1753331148815\" class=\"icon\" viewBox=\"0 0 1024 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" p-id=\"5721\" width=\"200\" height=\"200\">\n                    <path d=\"M723.903423 359.138018c-69.65045-52.952793-136.256577-51.476757-136.256576-51.476757l-6.088649 7.564685c83.027027 25.738378 121.127207 62.085766 121.127207 62.085766a387.459459 387.459459 0 0 0-145.297297-46.956397 418.179459 418.179459 0 0 0-98.340901 1.752793 73.801802 73.801802 0 0 1-7.564684 1.476036 357.385225 357.385225 0 0 0-110.702703 30.258739 278.786306 278.786306 0 0 0-28.782703 13.653333S353.049369 339.488288 440.873514 313.657658l-4.612613-6.088649s-66.513874-1.476036-136.164324 51.476757A654.252973 654.252973 0 0 0 230.630631 642.167928s40.867748 71.126486 148.341621 73.801802c0 0 16.697658-22.694054 31.827027-40.867748-62.085766-18.45045-84.77982-57.565405-84.77982-57.565405a130.998198 130.998198 0 0 0 13.653334 7.564684s0 1.568288 1.476036 1.568289c1.476036 1.476036 3.044324 1.476036 4.52036 3.044324a238.748829 238.748829 0 0 0 34.779099 16.605405 513.199279 513.199279 0 0 0 71.218739 21.218018 350.558559 350.558559 0 0 0 125.555315 0 329.894054 329.894054 0 0 0 69.650451-21.218018A247.328288 247.328288 0 0 0 702.685405 618.09009s-24.262342 39.391712-87.824144 57.565405c13.653333 18.45045 31.827027 39.299459 31.827027 39.29946 107.473874-2.952072 148.341622-73.801802 146.773334-72.602523a654.990991 654.990991 0 0 0-69.558199-283.214414zM421.131532 596.77982a54.705586 54.705586 0 0 1 0-109.042162 54.705586 54.705586 0 0 1 0 109.042162z m177.124324 0a54.705586 54.705586 0 1 1 49.908468-54.521081 52.491532 52.491532 0 0 1-49.908468 54.521081z\" p-id=\"5722\"></path><path d=\"M512 1024A512 512 0 1 1 1024 512 512.645766 512.645766 0 0 1 512 1024z m0-972.892252a461.261261 461.261261 0 1 0 461.261261 461.261261 461.261261 461.261261 0 0 0-461.261261-461.261261z\" p-id=\"5723\"></path>\n                </svg>\n            \"\"\",\n            \"class\": \"\",\n        },\n        {\n            \"name\": \"DingTalk\",\n            \"url\": \"https://qr.dingtalk.com/action/joingroup?code=v1,k1,OmDlBXpjW+I2vWjKDsjvI9dhcXjGZi3bQiojOq3dlDw=&_dt_no_comment=1&origin=11\",\n            \"html\": \"\"\"\n                <svg stroke=\"currentColor\" fill=\"currentColor\" stroke-width=\"0\" viewBox=\"0 0 1024 1024\">\n                    <path d=\"M512 0C229.205333 0 0 229.205333 0 512s229.205333 512 512 512 512-229.205333 512-512S794.794667 0 512 0z m237.312 480.810667c-1.109333 4.48-3.712 11.093333-7.424 18.986666h0.128l-0.426667 0.682667c-21.504 46.037333-77.610667 136.106667-77.610666 136.106667l-0.298667-0.597334-16.384 28.501334h79.018667l-150.912 200.917333 34.304-136.533333h-62.208l21.589333-90.282667c-17.493333 4.224-38.101333 10.026667-62.592 17.92 0 0-33.109333 19.370667-95.317333-37.333333 0 0-41.984-36.992-17.578667-46.165334 10.410667-3.925333 50.304-8.917333 81.706667-13.226666 42.410667-5.674667 68.48-8.789333 68.48-8.789334s-130.773333 2.005333-161.792-2.901333c-30.976-4.906667-70.4-56.704-78.805334-102.186667 0 0-12.970667-25.002667 27.904-13.226666 40.917333 11.818667 210.005333 46.08 210.005334 46.08S321.109333 411.434667 306.517333 394.922667c-14.634667-16.469333-43.093333-89.770667-39.424-134.869334 0 0 1.621333-11.221333 13.098667-8.192 0 0 162.602667 74.282667 273.792 114.986667 111.104 40.704 207.786667 61.397333 195.328 114.005333z\" opacity=\".65\" p-id=\"6077\"></path>\n                </svg>\n            \"\"\",\n            \"class\": \"\",\n        },\n    ],\n    \"light_css_variables\": {\n        \"color-brand-primary\": \"#2196f3\",\n        \"color-brand-content\": \"#2196f3\",\n        \"color-admonition-background\": \"#f8f9fa\",\n    },\n    \"dark_css_variables\": {\n        \"color-link\": \"#2196f3\",\n        \"color-link--hover\": \"#2196f3\",\n        \"color-brand-primary\": \"#64b5f6\",\n        \"color-brand-content\": \"#64b5f6\",\n    },\n}\n\nsource_suffix = [\".md\", \".rst\"]\n\n\n# -- Options for API documentation -------------------------------------------\n\nautodoc_member_order = \"bysource\"\nautodoc_typehints = \"description\"\nautodoc_class_signature = \"separated\"\nautodoc_default_options = {\n    \"special-members\": \"__call__\",\n}\n\nadd_module_names = False\npython_display_short_literal_types = True\n\n\ndef skip_member(app, what, name, obj, skip, options):\n    if name in [\n        \"__call__\",\n        \"_format\",\n        \"_format_agent_message\",\n        \"_format_tool_sequence\",\n    ]:\n        return False\n\n    return skip\n\n\ndef setup(app):\n    app.connect(\"autodoc-skip-member\", skip_member)\n"
  },
  {
    "path": "docs/tutorial/en/index.rst",
    "content": ".. AgentScope Doc documentation master file, created by\n   sphinx-quickstart on Thu Aug  8 15:07:21 2024.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to AgentScope's documentation!\n==========================================\n\n.. toctree::\n   :maxdepth: 1\n   :caption: Tutorial\n\n   tutorial/quickstart_installation\n   tutorial/quickstart_key_concept\n   tutorial/quickstart_message\n   tutorial/quickstart_agent\n\n.. toctree::\n   :maxdepth: 1\n   :caption: Workflow\n\n   tutorial/workflow_conversation\n   tutorial/workflow_multiagent_debate\n   tutorial/workflow_concurrent_agents\n   tutorial/workflow_routing\n   tutorial/workflow_handoffs\n\n.. toctree::\n   :maxdepth: 1\n   :caption: FAQ\n\n   tutorial/faq\n\n.. toctree::\n   :maxdepth: 1\n   :caption: Model and Context\n\n   tutorial/task_model\n   tutorial/task_prompt\n   tutorial/task_token\n   tutorial/task_memory\n   tutorial/task_long_term_memory\n\n.. toctree::\n   :maxdepth: 1\n   :caption: Tool\n\n   tutorial/task_tool\n   tutorial/task_mcp\n   tutorial/task_agent_skill\n\n.. toctree::\n   :maxdepth: 1\n   :caption: Agent\n\n   tutorial/task_agent\n   tutorial/task_state\n   tutorial/task_hook\n   tutorial/task_middleware\n   tutorial/task_a2a\n   tutorial/task_realtime\n\n.. toctree::\n   :maxdepth: 1\n   :caption: Features\n\n   tutorial/task_pipeline\n   tutorial/task_plan\n   tutorial/task_rag\n   tutorial/task_studio\n   tutorial/task_tracing\n   tutorial/task_eval\n   tutorial/task_eval_openjudge\n   tutorial/task_embedding\n   tutorial/task_tts\n   tutorial/task_tuner\n"
  },
  {
    "path": "docs/tutorial/en/make.bat",
    "content": "@ECHO OFF\n\npushd %~dp0\n\nREM Command file for Sphinx documentation\n\nif \"%SPHINXBUILD%\" == \"\" (\n\tset SPHINXBUILD=sphinx-build\n)\nset SOURCEDIR=source\nset BUILDDIR=build\n\n%SPHINXBUILD% >NUL 2>NUL\nif errorlevel 9009 (\n\techo.\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\n\techo.installed, then set the SPHINXBUILD environment variable to point\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\n\techo.may add the Sphinx directory to PATH.\n\techo.\n\techo.If you don't have Sphinx installed, grab it from\n\techo.https://www.sphinx-doc.org/\n\texit /b 1\n)\n\nif \"%1\" == \"\" goto help\n\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\ngoto end\n\n:help\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\n\n:end\npopd\n"
  },
  {
    "path": "docs/tutorial/en/src/README.md",
    "content": ""
  },
  {
    "path": "docs/tutorial/en/src/faq.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _faq:\n\nFAQ\n========================================\n\nAbout AgentScope\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n*What is AgentScope?*\n    AgentScope is a multi-agent framework, aiming to provide a simple yet efficient way to build LLM-empowered agent applications.\n\n*What is the difference between AgentScope v1.0 and v0.x?*\n    AgentScope v1.0 is a complete refactoring of the framework, equipped with new features and improvements. Refer to for detailed changes.\n\n\nAbout Model\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n*How to integrate my own model with AgentScope?*\n    Create your own model by inheriting ``agentscope.model.ChatModelBase`` and implement the ``__call__`` method.\n\n*What models are supported by AgentScope?*\n    Currently, AgentScope has built-in support for DashScope, Gemini, OpenAI, Anthropic, and Ollama APIs, and the ``OpenAIChatModel`` compatible with DeepSeek and vLLMs models.\n\n*How to monitor the token usage in AgentScope?*\n    In AgentScope Studio, we provide visualization of token usage and tracing. Refer :ref:`studio` section for more details.\n\n\nAbout Agent\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n*How to create my own agent?*\n    You can choose to use the ``ReActAgent`` class directly, or create your own agent by inheriting from ``AgentBase`` or ``ReActAgentBase`` classes. Refer to the :ref:`agent` section for more details.\n\n\n*How to forward the (streaming) output of agents to my own frontend or application?*\n    Use the pre hook of the ``print`` function to forward printing messages. Refer to the :ref:`hook` section.\n\n\nAbout Tools\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n*How many tools are provided by AgentScope?*\n    AgentScope provides a set of built-in tools, including ``execute_python_code``, ``execute_shell_command``, ``write_text_file`` , etc. You can find them under ``agentscope.tool`` module.\n\n\nAbout Reporting Bugs\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n*How can I report a bug in AgentScope?*\n    If you encounter a bug while using AgentScope, please report it by opening an issue on our GitHub repository.\n\n*How can I report a security bug in AgentScope?*\n    If you discover a security issue in AgentScope, please report it to us through the `Alibaba Security Response Center (ASRC) <https://security.alibaba.com/>`_.\n\n\"\"\"\n"
  },
  {
    "path": "docs/tutorial/en/src/quickstart_agent.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _react-agent:\n\nCreate ReAct Agent\n====================\n\nAgentScope provides out-of-the-box ReAct agent ``ReActAgent`` under ``agentscope.agent`` that can be used directly.\n\nIt supports the following features at the same time:\n\n- ✨ Basic features\n    - Support **hooks** around ``reply``, ``observe``, ``print``, ``_reasoning`` and ``_acting`` functions\n    - Support structured output\n- ✋ Realtime Steering\n    - Support user **interrupt**\n    - Support customized **interruption handling**\n- 🛠️ Tools\n    - Support both **sync/async** tool functions\n    - Support **streaming** tool response\n    - Support **stateful** tools management\n    - Support **parallel** tool calls\n    - Support **MCP** server\n- 💾 Memory\n    - Support **agent-controlled** long-term memory management\n    - Support static long-term memory management\n\n.. tip:: Refer to the :ref:`agent` section for more details about these\n features. In quickstart, we focus on how to create a ReAct agent and run it.\n\n\"\"\"\n\nfrom agentscope.agent import ReActAgent, AgentBase\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nimport asyncio\nimport os\n\nfrom agentscope.tool import Toolkit, execute_python_code\n\n\n# %%\n# Creating ReAct Agent\n# ------------------------------\n# To improve the flexibility, the ``ReActAgent`` class exposes the following parameters in its constructor:\n#\n# .. list-table:: Initialization parameters of ``ReActAgent`` class\n#   :header-rows: 1\n#\n#   * - Parameter\n#     - Further Reading\n#     - Description\n#   * - ``name`` (required)\n#     -\n#     - The name of the agent\n#   * - ``sys_prompt`` (required)\n#     -\n#     - The system prompt of the agent\n#   * - ``model`` (required)\n#     - :ref:`model`\n#     - The model used by the agent to generate responses\n#   * - ``formatter`` (required)\n#     - :ref:`prompt`\n#     - The prompt construction strategy, should be consistent with the model\n#   * - ``toolkit``\n#     - :ref:`tool`\n#     - The toolkit to register/call tool functions.\n#   * - ``memory``\n#     - :ref:`memory`\n#     - The short-term memory used to store the conversation history\n#   * - ``long_term_memory``\n#     - :ref:`long-term-memory`\n#     - The long-term memory\n#   * - ``long_term_memory_mode``\n#     - :ref:`long-term-memory`\n#     - The mode of the long-term memory:\n#\n#       - ``agent_control``: allow agent to control the long-term memory by itself\n#       - ``static_control``: retrieving and recording from/to long-term memory will happen in the beginning/end of each reply.\n#       - ``both``: activate the above two modes at the same time\n#   * - ``enable_meta_tool``\n#     - :ref:`tool`\n#     - Whether to enable the meta tool, which allows the agent to manage tools by itself\n#   * - ``parallel_tool_calls``\n#     - :ref:`agent`\n#     - Whether to allow parallel tool calls\n#   * - ``max_iters``\n#     -\n#     - The maximum number of iterations for the agent to generate a response\n#   * - ``plan_notebook``\n#     - :ref:`plan`\n#     - The plan notebook to manage the plans\n#   * - ``print_hint_msg``\n#     -\n#     - Whether to print the hint message generated by the plan notebook at each step\n#\n# Taking DashScope API as example, we create an agent object as follows:\n\n\nasync def creating_react_agent() -> None:\n    \"\"\"Create a ReAct agent and run a simple task.\"\"\"\n    # Prepare tools\n    toolkit = Toolkit()\n    toolkit.register_tool_function(execute_python_code)\n\n    jarvis = ReActAgent(\n        name=\"Jarvis\",\n        sys_prompt=\"You're a helpful assistant named Jarvis\",\n        model=DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            stream=True,\n            enable_thinking=False,\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n        memory=InMemoryMemory(),\n    )\n\n    msg = Msg(\n        name=\"user\",\n        content=\"Hi! Jarvis, run Hello World in Python.\",\n        role=\"user\",\n    )\n\n    await jarvis(msg)\n\n\nasyncio.run(creating_react_agent())\n\n# %%\n# Creating From Scratch\n# --------------------------------\n# You may want to create an agent from scratch, AgentScope provides two base classes for you to inherit from:\n#\n# .. list-table::\n#   :header-rows: 1\n#\n#   * - Class\n#     - Abstract Methods\n#     - Description\n#   * - ``AgentBase``\n#     - | ``reply``\n#       | ``observe``\n#       | ``handle_interrupt``\n#     - - The base class for all agents, supporting pre- and post- hooks around ``reply``, ``observe`` and ``print`` functions.\n#       - Implement the realtime steering within the ``__call__`` method.\n#   * - ``ReActAgentBase``\n#     - | ``reply``\n#       | ``observe``\n#       | ``handle_interrupt``\n#       | ``_reasoning``\n#       | ``_acting``\n#     - Add two abstract functions ``_reasoning`` and ``_acting`` on the basis of ``AgentBase``, as well as their hooks.\n#\n# Please refer to the :ref:`agent` section for more details about the agent class.\n#\n# Taking the ``AgentBase`` class as an example, we can create a custom agent\n# class by inheriting from it and implementing the ``reply`` method.\n\n\nclass MyAgent(AgentBase):\n    \"\"\"A custom agent class\"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"Initialize the agent\"\"\"\n        super().__init__()\n\n        self.name = \"Friday\"\n        self.sys_prompt = \"You're a helpful assistant named Friday.\"\n        self.model = DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            stream=False,\n        )\n        self.formatter = DashScopeChatFormatter()\n        self.memory = InMemoryMemory()\n\n    async def reply(self, msg: Msg | list[Msg] | None) -> Msg:\n        \"\"\"Reply to the message.\"\"\"\n        await self.memory.add(msg)\n\n        # Prepare the prompt\n        prompt = await self.formatter.format(\n            [\n                Msg(\"system\", self.sys_prompt, \"system\"),\n                *await self.memory.get_memory(),\n            ],\n        )\n\n        # Call the model\n        response = await self.model(prompt)\n\n        msg = Msg(\n            name=self.name,\n            content=response.content,\n            role=\"assistant\",\n        )\n\n        # Record the response in memory\n        await self.memory.add(msg)\n\n        # Print the message\n        await self.print(msg)\n        return msg\n\n    async def observe(self, msg: Msg | list[Msg] | None) -> None:\n        \"\"\"Observe the message.\"\"\"\n        # Store the message in memory\n        await self.memory.add(msg)\n\n    async def handle_interrupt(self) -> Msg:\n        \"\"\"Postprocess the interrupt.\"\"\"\n        # Taking a fixed response as example\n        return Msg(\n            name=self.name,\n            content=\"I noticed you interrupted me, how can I help you?\",\n            role=\"assistant\",\n        )\n\n\nasync def run_custom_agent() -> None:\n    \"\"\"Run the custom agent.\"\"\"\n    agent = MyAgent()\n    msg = Msg(\n        name=\"user\",\n        content=\"Who are you?\",\n        role=\"user\",\n    )\n    await agent(msg)\n\n\nasyncio.run(run_custom_agent())\n\n# %%\n#\n# Further Reading\n# ---------------------\n# - :ref:`agent`\n# - :ref:`model`\n# - :ref:`prompt`\n# - :ref:`tool`\n#\n"
  },
  {
    "path": "docs/tutorial/en/src/quickstart_installation.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _installation:\n\nInstallation\n============================\n\nAgentScope requires Python 3.10 or higher. You can install from source or pypi.\n\nFrom PyPI\n----------------\n.. code-block:: bash\n\n    pip install agentscope\n\nFrom Source\n----------------\nTo install AgentScope from source, you need to clone the repository from\nGitHub and install by the following commands\n\n.. code-block:: bash\n\n    git clone -b main https://github.com/agentscope-ai/agentscope\n    cd agentscope\n    pip install -e .\n\nTo ensure AgentScope is installed successfully, check via executing the following code:\n\"\"\"\n\nimport agentscope\n\nprint(agentscope.__version__)\n\n# %%\n# Extra Dependencies\n# ----------------------------\n#\n# To satisfy the requirements of different functionalities, AgentScope provides\n# extra dependencies that can be installed based on your needs.\n#\n# - full: Including extra dependencies for model APIs and tool functions\n# - dev: Development dependencies, including testing and documentation tools\n#\n# For example, when installing the full dependencies, the installation command varies depending on your operating system.\n#\n# For Windows users:\n#\n# .. code-block:: bash\n#\n#       pip install agentscope[full]\n#\n# For Mac and Linux users:\n#\n# .. code-block:: bash\n#\n#       pip install agentscope\\[full\\]\n"
  },
  {
    "path": "docs/tutorial/en/src/quickstart_key_concept.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. key-concepts:\n\nKey Concepts\n====================================\n\nThis chapter establishes key concepts from an engineering\nperspective to introduce AgentScope's design.\n\n.. note:: The goal of introducing the key concepts in AgentScope is to claim what practical problems AgentScope addresses and how it supports developers, rather than to offer formal definitions.\n\nState\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nIn AgentScope, state management is a fundamental building block that maintains snapshots of objects' runtime data.\n\nAgentScope separates object initialization from state management, allowing\nobject to be restored to different states after initialization through\n``load_state_dict`` and ``state_dict`` methods.\n\nIn AgentScope, agent, memory, long-term memory and toolkit are all stateful\nobjects. AgentScope links the state management of these objects together by supporting nested state management.\n\nMessage\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nIn AgentScope, message is the fundamental data structure,\nused to\n\n- exchange information between agents,\n- display information in the user interface,\n- store information in memory,\n- act as a unified medium between AgentScope and different LLM APIs.\n\nTool\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nA tool in AgentScope refers to callable object, no matter it's a\n\n- function,\n- partial function,\n- instance method,\n- class method,\n- static method, or\n- callable instance with ``__call__`` method.\n\nBesides, the callable object can be either\n\n- async or sync,\n- streaming or non-streaming.\n\nSo feel free to use any callable object as a tool in AgentScope.\n\nAgent\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nIn AgentScope, the agent behaviors are abstracted into three core functions in\n``AgentBase`` class:\n\n- ``reply``: Handle incoming message(s) and generate a response message.\n- ``observe``: Receive message(s) from the environment or other agents without returning a response.\n- ``print``: Display message(s) to the target terminal, web interface, etc.\n\nTo support realtime steering, an additional ``handle_interrupt`` function is\nprovided to handle user interrupts during the agent's reply process.\n\nAdditionally, ReAct agent is the most important agent in AgentScope, where\nthe agent's reply process is divided into two stages:\n\n- reasoning: thinking and generating tool calls by calling the LLM\n- acting: execute the tool functions.\n\nThus, we provide two additional core functions in ``ReActAgentBase`` class,\n``_reasoning`` and ``_acting``.\n\nFormatter\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nFormatter is the core component for LLM compatibility in AgentScope,\nresponsible for converting message objects into the required format for\nLLM APIs.\n\nBesides, additional functionality such as prompt engineering, truncation,\nand message validation can also be implemented in the formatter.\n\nWithin the formatter, the \"multi-agent\" (or \"multi-identity\") concept differs\nfrom the common multi-agent orchestration concept.\nIt focuses on the scenario where multiple identities are involved in the\ngiven messages, so that the common used ``role`` field (usually \"role\",\n\"assistant\" or \"system\") in LLM APIs cannot distinguish them.\n\nTherefore, AgentScope provides multi-agent formatter to handle\nthis scenario, usually used in games, multi-person chats, and social\nsimulations.\n\n.. note:: Multi-agent workflow **!=** multi-agent in formatter.\n For example, even if the following code snippet may involve multiple\n agents (the ``tool_agent`` and the ``tool_function`` caller), the input query\n is wrapped into a **user** message, so the ``role`` field can still distinguish\n between them.\n\n .. code-block:: python\n\n    async def tool_function(query: str) -> str:\n        \\\"\\\"\\\"Tool function calling another agent\\\"\\\"\\\"\n        msg = Msg(\"user\", query, role=\"user\")\n        tool_agent = Agent(name=\"Programmer\")\n        return await tool_agent(msg)\n\n Understanding this distinction helps developers better grasp AgentScope's formatter design.\n\n\nLong-Term Memory\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nAlthough providing different base classes for short- and\nlong-term memory, there are no strict distinctions between them in AgentScope.\n\nIn our view, everything should be **requirement-driven**. As long as your\nneeds are excellently met, developers can completely use just one powerful\nmemory system.\n\nFor ensuring the flexibility of AgentScope, we provide a two mode long-term\nmemory system, allowing the agent to manage (record and retrieve) the\nlong-term memory by its own.\n\"\"\"\n"
  },
  {
    "path": "docs/tutorial/en/src/quickstart_message.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _message:\n\nCreate Message\n====================\n\nMessage is the core concept in AgentScope, used to support multimodal data, tools API, information storage/exchange and prompt construction.\n\nA message consists of four fields:\n\n- ``name``,\n- ``role``,\n- ``content``, and\n- ``metadata``\n\nThe types and descriptions of these fields are as follows:\n\n.. list-table:: The fields in a message object\n    :header-rows: 1\n\n    * - Field\n      - Type\n      - Description\n    * - name\n      - ``str``\n      - The name/identity of the message sender\n    * - role\n      - | ``Literal[``\n        |     ``\"system\",``\n        |     ``\"assistant\",``\n        |     ``\"user\"``\n        | ``]``\n      - The role of the message sender, which must be one of \"system\", \"assistant\", or \"user\".\n    * - content\n      - ``str | list[ContentBlock]``\n      - The data of the message, which can be a string or a list of blocks.\n    * - metadata\n      - ``dict[str, JSONSerializableObject] | None``\n      - A dict containing additional metadata about the message, usually used for structured output.\n\n.. tip:: - In application with multiple identities, the ``name`` field is used to distinguish between different identities.\n - The ``metadata`` field is recommended for structured output, which won't be included in the prompt construction.\n\nNext, we introduce the supported blocks in the ``content`` field by their corresponding scenarios.\n\"\"\"\n\nfrom agentscope.message import (\n    Msg,\n    Base64Source,\n    TextBlock,\n    ThinkingBlock,\n    ImageBlock,\n    AudioBlock,\n    VideoBlock,\n    ToolUseBlock,\n    ToolResultBlock,\n)\nimport json\n\n# %%\n# Creating Textual Message\n# -----------------------------\n# Creating a message object by providing the ``name``, ``role``, and ``content`` fields.\n#\n\nmsg = Msg(\n    name=\"Jarvis\",\n    role=\"assistant\",\n    content=\"Hi! How can I help you?\",\n)\n\nprint(f\"The name of the sender: {msg.name}\")\nprint(f\"The role of the sender: {msg.role}\")\nprint(f\"The content of the message: {msg.content}\")\n\n# %%\n# Creating Multimodal Message\n# --------------------------------------\n# The message class supports multimodal content by providing different content blocks:\n#\n# .. list-table:: Multimodal content blocks in AgentScope\n#     :header-rows: 1\n#\n#     * - Class\n#       - Description\n#       - Example\n#     * - TextBlock\n#       - Pure text data\n#       - .. code-block:: python\n#\n#             TextBlock(\n#                type=\"text\",\n#                text=\"Hello, world!\"\n#             )\n#     * - ImageBlock\n#       - The image data\n#       - .. code-block:: python\n#\n#             ImageBlock(\n#                type=\"image\",\n#                source=URLSource(\n#                    type=\"url\",\n#                    url=\"https://example.com/image.jpg\"\n#                )\n#             )\n#     * - AudioBlock\n#       - The audio data\n#       - .. code-block:: python\n#\n#             AudioBlock(\n#                type=\"audio\",\n#                source=URLSource(\n#                    type=\"url\",\n#                    url=\"https://example.com/audio.mp3\"\n#                )\n#             )\n#     * - VideoBlock\n#       - The video data\n#       - .. code-block:: python\n#\n#             VideoBlock(\n#                type=\"video\",\n#                source=URLSource(\n#                    type=\"url\",\n#                    url=\"https://example.com/video.mp4\"\n#                )\n#             )\n#\n# For ``ImageBlock``, ``AudioBlock`` and ``VideoBlock``, you can use either a base64 encoded string as the source:\n#\n\nmsg = Msg(\n    name=\"Jarvis\",\n    role=\"assistant\",\n    content=[\n        TextBlock(\n            type=\"text\",\n            text=\"This is a multimodal message with base64 encoded data.\",\n        ),\n        ImageBlock(\n            type=\"image\",\n            source=Base64Source(\n                type=\"base64\",\n                media_type=\"image/jpeg\",\n                data=\"/9j/4AAQSkZ...\",\n            ),\n        ),\n        AudioBlock(\n            type=\"audio\",\n            source=Base64Source(\n                type=\"base64\",\n                media_type=\"audio/mpeg\",\n                data=\"SUQzBAAAAA...\",\n            ),\n        ),\n        VideoBlock(\n            type=\"video\",\n            source=Base64Source(\n                type=\"base64\",\n                media_type=\"video/mp4\",\n                data=\"AAAAIGZ0eX...\",\n            ),\n        ),\n    ],\n)\n\n# %%\n# Creating Thinking Message\n# --------------------------------------\n# The ``ThinkingBlock`` is to support reasoning models, containing the thinking process of the model.\n#\n\nmsg_thinking = Msg(\n    name=\"Jarvis\",\n    role=\"assistant\",\n    content=[\n        ThinkingBlock(\n            type=\"thinking\",\n            thinking=\"I'm building an example for thinking block in AgentScope.\",\n        ),\n        TextBlock(\n            type=\"text\",\n            text=\"This is an example for thinking block.\",\n        ),\n    ],\n)\n\n# %%\n# .. _tool-block:\n#\n# Creating Tool Use/Result Message\n# --------------------------------------\n# The ``ToolUseBlock`` and ``ToolResultBlock`` are to support tools API:\n#\n\nmsg_tool_call = Msg(\n    name=\"Jarvis\",\n    role=\"assistant\",\n    content=[\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"343\",\n            name=\"get_weather\",\n            input={\n                \"location\": \"Beijing\",\n            },\n        ),\n    ],\n)\n\nmsg_tool_res = Msg(\n    name=\"system\",\n    role=\"system\",\n    content=[\n        ToolResultBlock(\n            type=\"tool_result\",\n            id=\"343\",\n            name=\"get_weather\",\n            output=\"The weather in Beijing is sunny with a temperature of 25°C.\",\n        ),\n    ],\n)\n\n\n# %%\n# .. tip:: Refer to the :ref:`tool` section for more information about tools API in AgentScope.\n#\n# Serialization and Deserialization\n# ------------------------------------------------\n# Message object can be serialized and deserialized by ``to_dict`` and ``from_dict`` methods, respectively.\n\nserialized_msg = msg.to_dict()\n\nprint(type(serialized_msg))\nprint(json.dumps(serialized_msg, indent=4))\n\n# %%\n# Deserialize a message from a string in JSON format.\n\nnew_msg = Msg.from_dict(serialized_msg)\n\nprint(type(new_msg))\nprint(f'The sender of the message: \"{new_msg.name}\"')\nprint(f'The role of the sender: \"{new_msg.role}\"')\nprint(f'The content of the message: \"{json.dumps(new_msg.content, indent=4)}\"')\n\n# %%\n# Property Functions\n# ------------------------------------------------\n# To ease the use of message object, AgentScope provides these functions:\n#\n# .. list-table:: Functions of the message object\n#   :header-rows: 1\n#\n#   * - Function\n#     - Parameters\n#     - Description\n#   * - get_text_content\n#     - \\-\n#     - Gather content from all ``TextBlock`` in to a single string (separated by \"\\\\n\").\n#   * - get_content_blocks\n#     - ``block_type``\n#     - Return a list of content blocks of the specified type. If ``block_type`` not provided, return content in blocks format.\n#   * - has_content_blocks\n#     - ``block_type``\n#     - Check whether the message has content blocks of the specified type. The ``str`` content is considered as a ``TextBlock`` type.\n"
  },
  {
    "path": "docs/tutorial/en/src/task_a2a.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _a2a:\n\nA2A Agent\n============================\n\nA2A (Agent-to-Agent) is an open standard protocol for enabling interoperable communication between different AI agents.\n\nAgentScope provides support for the A2A protocol at two levels: obtaining Agent Card information and connecting to remote agents. The related APIs are as follows:\n\n.. list-table:: A2A Related Classes\n    :header-rows: 1\n\n    * - Class\n      - Description\n    * - ``A2AAgent``\n      - Agent class for communicating with remote A2A agents\n    * - ``A2AChatFormatter``\n      - Formatter for converting between AgentScope messages and A2A message/task formats\n    * - ``AgentCardResolverBase``\n      - Base class for Agent Card resolvers\n    * - ``FileAgentCardResolver``\n      - Resolver for loading Agent Cards from local JSON files\n    * - ``WellKnownAgentCardResolver``\n      - Resolver for fetching Agent Cards from the well-known path of a URL\n    * - ``NacosAgentCardResolver``\n      - Resolver for fetching Agent Cards from the Nacos Agent Registry\n\nThis section demonstrates how to create an ``A2AAgent`` and communicate with remote A2A agents.\n\n.. note:: Note that A2A support is an **experimental feature** and may change in future versions. Due to limitations of the A2A protocol itself, ``A2AAgent`` cannot fully align with local agents like ``ReActAgent``, including:\n\n - Only supports chatbot scenarios, i.e., only supports conversations between one user and one agent (does not affect handoff/router usage patterns)\n - Does not support real-time interruption during conversations\n - Does not support agentic structured output\n - In the current implementation, messages received by the ``observe`` method are stored locally and sent to the remote agent together when the ``reply`` method is called. Therefore, if several ``observe`` calls are made without a subsequent ``reply`` call, those messages will not be seen by the remote agent\n\n\n\"\"\"\n\nfrom a2a.types import AgentCard, AgentCapabilities\nfrom v2.nacos import ClientConfig\n\nfrom agentscope.a2a import WellKnownAgentCardResolver, NacosAgentCardResolver\nfrom agentscope.agent import A2AAgent, UserAgent\nfrom agentscope.message import Msg, TextBlock\nfrom agentscope.tool import ToolResponse\n\n# %%\n# Obtaining Agent Cards\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# First, we need to obtain an Agent Card to connect to the corresponding agent. An Agent Card contains information such as the agent's name, description, capabilities, and connection details.\n#\n# Manually Creating Agent Card\n# --------------------------------\n#\n# If you know all the information of an Agent Card, you can directly create an Agent Card object from `a2a.types.AgentCard`.\n#\n\n# Create an Agent Card object\nagent_card = AgentCard(\n    name=\"Friday\",  # Agent name\n    description=\"A fun chatting companion\",  # Agent description\n    url=\"http://localhost:8000\",  # Agent's RPC service URL\n    version=\"1.0.0\",  # Agent version\n    capabilities=AgentCapabilities(  # Agent capability configuration\n        push_notifications=False,\n        state_transition_history=True,\n        streaming=True,\n    ),\n    default_input_modes=[\"text/plain\"],  # Supported input formats\n    default_output_modes=[\"text/plain\"],  # Supported output formats\n    skills=[],  # Agent skill list\n)\n\n# %%\n#\n# Fetching from Remote Services\n# --------------------------------\n# AgentScope also supports fetching from the standard path of remote services (well-known server).\n# Here's an example using ``WellKnownAgentCardResolver`` to fetch an Agent Card from the standard path of a remote service:\n#\n\n\nasync def agent_card_from_well_known_website() -> AgentCard:\n    \"\"\"Example of fetching an Agent Card from the well-known path of a remote service.\"\"\"\n    # Create an Agent Card resolver\n    resolver = WellKnownAgentCardResolver(\n        base_url=\"http://localhost:8000\",\n    )\n    # Fetch and return the Agent Card\n    return await resolver.get_agent_card()\n\n\n# %%\n# Loading Agent Cards from Local Files\n# --------------------------------\n#\n# The ``FileAgentCardResolver`` class supports loading Agent Cards from local JSON files, suitable for configuration file management scenarios.\n# An example of an Agent Card in JSON format is shown below:\n#\n# .. code-block:: json\n#     :caption: Example Agent Card JSON file content\n#\n#     {\n#         \"name\": \"RemoteAgent\",\n#         \"url\": \"http://localhost:8000\",\n#         \"description\": \"Remote A2A Agent\",\n#         \"version\": \"1.0.0\",\n#         \"capabilities\": {},\n#         \"default_input_modes\": [\"text/plain\"],\n#         \"default_output_modes\": [\"text/plain\"],\n#         \"skills\": []\n#     }\n#\n# You can easily load this file using ``FileAgentCardResolver``:\n#\n\n\nasync def agent_card_from_file() -> AgentCard:\n    \"\"\"Example of loading an Agent Card from a local JSON file.\"\"\"\n    from agentscope.a2a import FileAgentCardResolver\n\n    # Load Agent Card from JSON file\n    resolver = FileAgentCardResolver(\n        file_path=\"./agent_card.json\",  # JSON file path\n    )\n    # Fetch and return the Agent Card\n    return await resolver.get_agent_card()\n\n\n# %%\n# Fetching Agent Cards from Nacos Registry\n# --------------------------------\n#\n# Nacos is an open-source dynamic service discovery, configuration management, and service management platform. In version 3.1.0, it introduced the Agent Registry feature, supporting distributed registration, discovery, and version management of A2A agents.\n#\n# .. important:: The prerequisite for using ``NacosAgentCardResolver`` is that the user has deployed a Nacos server version 3.1.0 or higher. For deployment and registration procedures, please refer to the `official documentation <https://nacos.io/docs/latest/quickstart/quick-start>`_.\n#\n\n\nasync def agent_card_from_nacos() -> AgentCard:\n    \"\"\"Example of fetching an Agent Card from the Nacos registry.\"\"\"\n\n    # Create a Nacos Agent Card resolver\n    resolver = NacosAgentCardResolver(\n        remote_agent_name=\"my-remote-agent\",  # Agent name registered in Nacos\n        nacos_client_config=ClientConfig(\n            server_addresses=\"http://localhost:8848\",  # Nacos server address\n            # Other optional configuration items\n        ),\n    )\n    # Fetch and return the Agent Card\n    return await resolver.get_agent_card()\n\n\n# %%\n# Building an A2A Agent\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# The ``A2AAgent`` class provided by AgentScope is used to communicate with remote A2A agents, and its usage is similar to regular agents.\n\nagent = A2AAgent(agent_card=agent_card)\n\n# %%\n# Using ``A2AAgent``, developers can build chatbot scenario conversations, or encapsulate it as a tool function to build more complex application scenarios such as handoff/router.\n# Currently, the format protocol conversion supported by ``A2AAgent`` is handled by ``agentscope.formatter.A2AChatFormatter``, which supports:\n#\n# - Converting AgentScope's ``Msg`` messages to A2A protocol's ``Message`` format\n# - Converting A2A protocol responses back to AgentScope's ``Msg`` format\n# - Converting A2A protocol's ``Task`` responses to AgentScope's ``Msg`` format\n# - Supporting multiple content types such as text, images, audio, and video\n#\n\n\nasync def a2a_in_chatbot() -> None:\n    \"\"\"Example of chatting using A2AAgent.\"\"\"\n\n    user = UserAgent(\"user\")\n\n    msg = None\n    while True:\n        msg = await user(msg)\n        if msg.get_text_content() == \"exit\":\n            break\n        msg = await agent(msg)\n\n\n# %%\n# Or encapsulate it as a tool function for invocation:\n\n\nasync def create_worker(query: str) -> ToolResponse:\n    \"\"\"Complete a given task through a sub-agent\n\n    Args:\n        query (`str`):\n            Description of the task to be completed by the sub-agent\n    \"\"\"\n    res = await agent(\n        Msg(\"user\", query, \"user\"),\n    )\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=res.get_text_content(),\n            ),\n        ],\n    )\n"
  },
  {
    "path": "docs/tutorial/en/src/task_agent.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _agent:\n\nAgent\n=========================\n\nIn this tutorial, we first focus on introducing the ReAct agent in AgentScope,\nthen we briefly introduce how to customize your own agent from scratch.\n\nReAct Agent\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nIn AgentScope, the ``ReActAgent`` class integrates various features into a final implementation, including\n\n.. list-table:: Features of ``ReActAgent``\n    :header-rows: 1\n\n    * - Feature\n      - Reference\n    * - Support realtime steering\n      -\n    * - Support memory compression\n      -\n    * - Support parallel tool calls\n      -\n    * - Support structured output\n      -\n    * - Support fine-grained MCP control\n      - :ref:`mcp`\n    * - Support agent-controlled tools management (Meta tool)\n      - :ref:`tool`\n    * - Support self-controlled long-term memory\n      - :ref:`long-term-memory`\n    * - Support automatic state management\n      - :ref:`state`\n\n\nDue to limited space, in this tutorial we only demonstrate the first three\nfeatures of ``ReActAgent`` class, leaving the others to the corresponding sections\nlisted above.\n\n\"\"\"\n\nimport asyncio\nimport json\nimport os\nfrom datetime import datetime\nimport time\n\nfrom pydantic import BaseModel, Field\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.message import TextBlock, Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import Toolkit, ToolResponse\n\n\n# %%\n# Realtime Steering\n# ---------------------------------------\n#\n# The realtime steering allows user to interrupt the agent's reply at any time,\n# which is implemented based on the asyncio cancellation mechanism.\n#\n# Specifically, when calling the ``interrupt`` method of the agent, it will\n# cancel the current reply task, and execute the ``handle_interrupt`` method\n# for postprocessing.\n#\n# .. hint:: With the feature of supporting streaming tool results in\n#  :ref:`tool`, users can interrupt the tool execution if it takes too long or\n#  deviates from user expectations by Ctrl+C in the terminal or calling the\n#  ``interrupt`` method of the agent in your code.\n#\n# The interruption logic has been implemented in the ``AgentBase`` class as a\n# basic feature, leaving a ``handle_interrupt`` method for users to customize the\n# post-processing of interruption as follows:\n#\n# .. code-block:: python\n#\n#     # code snippet of AgentBase\n#     class AgentBase:\n#         ...\n#         async def __call__(self, *args: Any, **kwargs: Any) -> Msg:\n#             ...\n#             reply_msg: Msg | None = None\n#             try:\n#                 self._reply_task = asyncio.current_task()\n#                 reply_msg = await self.reply(*args, **kwargs)\n#\n#             except asyncio.CancelledError:\n#                 # Catch the interruption and handle it by the handle_interrupt method\n#                 reply_msg = await self.handle_interrupt(*args, **kwargs)\n#\n#             ...\n#\n#         @abstractmethod\n#         async def handle_interrupt(self, *args: Any, **kwargs: Any) -> Msg:\n#             pass\n#\n#\n# In ``ReActAgent`` class, we return a fixed message \"I noticed that you have\n# interrupted me. What can I do for you?\" as follows:\n#\n# .. figure:: ../../_static/images/interruption_en.gif\n#     :width: 100%\n#     :align: center\n#     :class: bordered-image\n#     :alt: Example of interruption\n#\n#     Example of interruption\n#\n# You can override it with your own implementation, for example, calling the LLM\n# to generate a simple response to the interruption.\n#\n#\n# Memory Compression\n# ----------------------------------------\n# As conversations grow longer, the token count in memory can exceed model context\n# limits or slow down inference. ``ReActAgent`` provides an automatic memory compression\n# feature to address this issue.\n#\n# **Basic Usage**\n#\n# To enable memory compression, provide a ``CompressionConfig`` instance when initializing\n# the ``ReActAgent``:\n#\n# .. code-block:: python\n#\n#     from agentscope.agent import ReActAgent\n#     from agentscope.token import CharTokenCounter\n#\n#     agent = ReActAgent(\n#         name=\"Assistant\",\n#         sys_prompt=\"You are a helpful assistant.\",\n#         model=model,\n#         formatter=formatter,\n#         compression_config=ReActAgent.CompressionConfig(\n#             enable=True,\n#             agent_token_counter=CharTokenCounter(),  # The token counter for the agent\n#             trigger_threshold=10000,  # Trigger compression when exceeding 10000 tokens\n#             keep_recent=3,            # Keep the most recent 3 messages uncompressed\n#         ),\n#     )\n#\n# When memory compression is enabled, the agent monitors the token count in its memory.\n# Once it exceeds the ``trigger_threshold``, the agent automatically:\n#\n# 1. Identifies messages that haven't been compressed yet (via ``exclude_mark``)\n# 2. Keeps the most recent ``keep_recent`` messages uncompressed (to preserve recent context)\n# 3. Sends older messages to an LLM to generate a structured summary\n# 4. Marks the compressed messages with ``MemoryMark.COMPRESSED`` (via ``update_messages_mark``)\n# 5. Stores the summary in memory (via ``update_compressed_summary``)\n#\n# .. important:: The compression uses a **marking mechanism** rather than replacing messages. Old messages are marked as compressed and excluded from future retrievals via ``exclude_mark=MemoryMark.COMPRESSED``, while the generated summary is stored separately and retrieved when needed. This approach preserves the original messages and allows flexible memory management. For more details about the mark functionality, please refer to :ref:`memory`.\n#\n# By default, the compressed summary is structured into five key fields:\n#\n# - **task_overview**: The user's core request and success criteria\n# - **current_state**: What has been completed so far, including files and outputs\n# - **important_discoveries**: Technical constraints, decisions, errors, and failed approaches\n# - **next_steps**: Specific actions needed to complete the task\n# - **context_to_preserve**: User preferences, domain details, and promises made\n#\n# **Customizing Compression**\n#\n# You can customize how compression works by specifying ``summary_schema``,\n# ``summary_template``, and ``compression_prompt`` parameters.\n#\n# - **compression_prompt**: Guides the LLM on how to generate the summary\n# - **summary_schema**: Defines the structure of the compressed summary using a Pydantic model\n# - **summary_template**: Formats how the compressed summary is presented back to the agent\n#\n# Here's an example of customizing the compression:\n#\n# .. code-block:: python\n#\n#     from pydantic import BaseModel, Field\n#\n#     # Define a custom summary structure\n#     class CustomSummary(BaseModel):\n#         main_topic: str = Field(\n#             max_length=200,\n#             description=\"The main topic of the conversation\"\n#         )\n#         key_points: str = Field(\n#             max_length=400,\n#             description=\"Important points discussed\"\n#         )\n#         pending_tasks: str = Field(\n#             max_length=200,\n#             description=\"Tasks that remain to be done\"\n#         )\n#\n#     # Create agent with custom compression configuration\n#     agent = ReActAgent(\n#         name=\"Assistant\",\n#         sys_prompt=\"You are a helpful assistant.\",\n#         model=model,\n#         formatter=formatter,\n#         compression_config=ReActAgent.CompressionConfig(\n#             enable=True,\n#             agent_token_counter=CharTokenCounter(),\n#             trigger_threshold=10000,\n#             keep_recent=3,\n#             # Custom schema for structured summary\n#             summary_schema=CustomSummary,\n#             # Custom prompt to guide compression\n#             compression_prompt=(\n#                 \"<system-hint>Please summarize the above conversation \"\n#                 \"focusing on the main topic, key discussion points, \"\n#                 \"and any pending tasks.</system-hint>\"\n#             ),\n#             # Custom template to format the summary\n#             summary_template=(\n#                 \"<system-info>Conversation Summary:\\n\"\n#                 \"Main Topic: {main_topic}\\n\\n\"\n#                 \"Key Points:\\n{key_points}\\n\\n\"\n#                 \"Pending Tasks:\\n{pending_tasks}\"\n#                 \"</system-info>\"\n#             ),\n#         ),\n#     )\n#\n# The ``summary_template`` uses the fields defined in ``summary_schema`` as placeholders\n# (e.g., ``{main_topic}``, ``{key_points}``). After the LLM generates the structured summary,\n# these placeholders will be replaced with the actual values.\n#\n# .. note:: The agent ensures that tool use and tool result pairs are kept together during compression to maintain the integrity of the conversation flow.\n#\n# .. tip:: You can use a smaller, faster model for compression by specifying a different ``compression_model`` and ``compression_formatter`` to reduce costs and latency.\n#\n#\n#\n# Parallel Tool Calls\n# ----------------------------------------\n# ``ReActAgent`` supports parallel tool calls by providing a ``parallel_tool_calls``\n# argument in its constructor.\n# When multiple tool calls are generated, and ``parallel_tool_calls`` is set to ``True``,\n# they will be executed in parallel by the ``asyncio.gather`` function.\n#\n# .. note:: The parallel tool execution in ``ReActAgent`` is implemented based on ``asyncio.gather``. Therefore, to maximize the effect of parallel tool execution, both the tool function itself and the logic within it must be asynchronous.\n#\n# .. note:: When running, please ensure that parallel tool calling is supported at the model level and the corresponding parameters are set correctly (can be passed through ``generate_kwargs``). For example, for the DashScope API, you need to set ``parallel_tool_calls`` to ``True``, otherwise parallel tool calling will not be possible.\n\n\n# prepare a tool function\nasync def example_tool_function(tag: str) -> ToolResponse:\n    \"\"\"A sample example tool function\"\"\"\n    start_time = datetime.now().strftime(\"%H:%M:%S.%f\")\n\n    # Sleep for 3 seconds to simulate a long-running task\n    await asyncio.sleep(3)\n\n    end_time = datetime.now().strftime(\"%H:%M:%S.%f\")\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=f\"Tag {tag} started at {start_time} and ended at {end_time}. \",\n            ),\n        ],\n    )\n\n\ntoolkit = Toolkit()\ntoolkit.register_tool_function(example_tool_function)\n\n# Create an ReAct agent\nagent = ReActAgent(\n    name=\"Jarvis\",\n    sys_prompt=\"You're a helpful assistant named Jarvis.\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        # Preset the generation kwargs to enable parallel tool calls\n        generate_kwargs={\n            \"parallel_tool_calls\": True,\n        },\n    ),\n    memory=InMemoryMemory(),\n    formatter=DashScopeChatFormatter(),\n    toolkit=toolkit,\n    parallel_tool_calls=True,\n)\n\n\nasync def example_parallel_tool_calls() -> None:\n    \"\"\"Example of parallel tool calls\"\"\"\n    # prompt the agent to generate two tool calls at once\n    await agent(\n        Msg(\n            \"user\",\n            \"Generate two tool calls of the 'example_tool_function' function with tag as 'tag1' and 'tag2' AT ONCE so that they can execute in parallel.\",\n            \"user\",\n        ),\n    )\n\n\nasyncio.run(example_parallel_tool_calls())\n\n# %%\n# Structured Output\n# ----------------------------------------\n# To generate a structured output, the ``ReActAgent`` instance receives a child class\n# of the ``pydantic.BaseModel`` as the ``structured_model`` argument in its ``__call__`` function.\n# Then we can get the structured output from the ``metadata`` field of the returned message.\n#\n#\n# Taking introducing Einstein as an example:\n#\n\n# Create an ReAct agent\nagent = ReActAgent(\n    name=\"Jarvis\",\n    sys_prompt=\"You're a helpful assistant named Jarvis.\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        # Preset the generation kwargs to enable parallel tool calls\n        generate_kwargs={\n            \"parallel_tool_calls\": True,\n        },\n    ),\n    memory=InMemoryMemory(),\n    formatter=DashScopeChatFormatter(),\n    toolkit=Toolkit(),\n    parallel_tool_calls=True,\n)\n\n\n# The structured model\nclass Model(BaseModel):\n    name: str = Field(description=\"The name of the person\")\n    description: str = Field(\n        description=\"A one-sentence description of the person\",\n    )\n    age: int = Field(description=\"The age\")\n    honor: list[str] = Field(description=\"A list of honors of the person\")\n\n\nasync def example_structured_output() -> None:\n    \"\"\"The example structured output\"\"\"\n    res = await agent(\n        Msg(\n            \"user\",\n            \"Introduce Einstein\",\n            \"user\",\n        ),\n        structured_model=Model,\n    )\n    print(\"\\nThe structured output:\")\n    print(json.dumps(res.metadata, indent=4))\n\n\nasyncio.run(example_structured_output())\n\n# %%\n# Customizing Agent\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope provides two base classes, ``AgentBase`` and ``ReActAgentBase``, which\n# differ in the abstract methods they define and the hooks they support.\n# Specifically, the ``ReActAgentBase`` extends ``AgentBase`` with additional ``_reasoning`` and ``_acting``\n# abstract methods, as well as their pre- and post- hooks.\n#\n# Developers can choose to inherit from either of these base classes based on their needs.\n# We summarize the agent under ``agentscope.agent`` module as follows:\n#\n# .. list-table:: Agent classes in AgentScope\n#     :header-rows: 1\n#\n#     * - Class\n#       - Abstract Method\n#       - Support Hooks\n#       - Description\n#     * - ``AgentBase``\n#       - | ``reply``\n#         | ``observe``\n#         | ``print``\n#         | ``handle_interrupt``\n#       - | pre\\_/post_reply\n#         | pre\\_/post_observe\n#         | pre\\_/post_print\n#       - The base class for all agents, providing the basic interface and hooks.\n#     * - ``ReActAgentBase``\n#       - | ``reply``\n#         | ``observe``\n#         | ``print``\n#         | ``handle_interrupt``\n#         | ``_reasoning``\n#         | ``_acting``\n#       - | pre\\_/post_reply\n#         | pre\\_/post_observe\n#         | pre\\_/post_print\n#         | pre\\_/post_reasoning\n#         | pre\\_/post_acting\n#       - The abstract class for ReAct agent, extending ``AgentBase`` with reasoning and acting abstract methods and their hooks.\n#     * - ``ReActAgent``\n#       - \\-\n#       - | pre\\_/post_reply\n#         | pre\\_/post_observe\n#         | pre\\_/post_print\n#         | pre\\_/post_reasoning\n#         | pre\\_/post_acting\n#       - An implementation of ``ReActAgentBase``\n#     * - ``UserAgent``\n#       -\n#       -\n#       - A special agent that represents the user, used to interact with the agent\n#     * - ``A2aAgent``\n#       - \\-\n#       - | pre\\_/post_reply\n#         | pre\\_/post_observe\n#         | pre\\_/post_print\n#       - Agent for communicating with remote A2A agents, see :ref:`a2a`\n#\n#\n#\n# Further Reading\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# - :ref:`tool`\n# - :ref:`hook`\n# - :ref:`a2a`\n#\n"
  },
  {
    "path": "docs/tutorial/en/src/task_agent_skill.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _agent_skill:\n\nAgent Skill\n============================\n\n`Agent skill <https://claude.com/blog/skills>`_ is an approach proposed by\nAnthropic to improve agent capabilities on specific tasks.\n\nAgentScope provides built-in support for Agent Skills through the ``Toolkit``\nclass, allowing users to easily register and manage agent skills.\n\nThe related APIs are as follows:\n\n.. list-table:: Agent skill API in ``Toolkit`` class\n    :header-rows: 1\n\n    * - API\n      - Description\n    * - ``register_agent_skill``\n      - Register agent skills from a given directory.\n    * - ``remove_agent_skill``\n      - Remove a registered agent skill by name.\n    * - ``get_agent_skill_prompt``\n      - Get the prompt for all registered agent skills, which can be\n        attached to the system prompt for the agent.\n\nIn this section we demonstrate how to register agent skills and use them in an\nReAct agent.\n\"\"\"\nimport os\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import Toolkit\n\n# %%\n# Registering Agent Skills\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# First, we need to prepare an agent skill directory, which follows the\n# requirements specified in the `Anthropic blog <https://claude.com/blog/skills>`_.\n#\n# .. note:: The skill directory must contain a ``SKILL.md`` file containing\n#  YAML frontmatter and instructions.\n#\n# Here, we fake an example skill directory ``sample_skill`` with the following files:\n#\n# .. code-block:: markdown\n#\n#   ---\n#   name: sample_skill\n#   description: A sample agent skill for demonstration.\n#   ---\n#\n#   # Sample Skill\n#   ...\n#\n\nos.makedirs(\"sample_skill\", exist_ok=True)\nwith open(\"sample_skill/SKILL.md\", \"w\", encoding=\"utf-8\") as f:\n    f.write(\n        \"\"\"---\nname: sample_skill\ndescription: A sample agent skill for demonstration.\n---\n\n# Sample Skill\n...\n\"\"\",\n    )\n\n# %%\n# Then, we can register the skill using the ``register_agent_skill`` API of\n# the ``Toolkit`` class.\n#\n\ntoolkit = Toolkit()\n\ntoolkit.register_agent_skill(\"sample_skill\")\n\n# %%\n# After that, we can get the prompt for all registered agent skills using the\n# ``get_agent_skill_prompt`` API\n\nagent_skill_prompt = toolkit.get_agent_skill_prompt()\nprint(\"Agent Skill Prompt:\")\nprint(agent_skill_prompt)\n\n# %%\n# Of course, we can customize the prompt template when creating the ``Toolkit``\n# instance.\n\ntoolkit = Toolkit(\n    # The instruction that introduces how to use the skill to the agent/llm\n    agent_skill_instruction=\"<system-info>You're provided a collection of skills, each in a directory and described by a SKILL.md file.</system-info>\\n\",\n    # The template for formatting each skill's prompt, must contain\n    # {name}, {description}, and {dir} fields\n    agent_skill_template=\"- {name}({dir}): {description}\",\n)\n\ntoolkit.register_agent_skill(\"sample_skill\")\nagent_skill_prompt = toolkit.get_agent_skill_prompt()\nprint(\"Customized Agent Skill Prompt:\")\nprint(agent_skill_prompt)\n\n# %%\n# Integrating Agent Skills with ReActAgent\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# The `ReActAgent` class in AgentScope will attach the agent skill prompt to\n# the system prompt automatically.\n#\n# We can create a ReAct agent with the registered agent skills as follows:\n#\n# .. important:: When using agent skills, the agent must be equipped with text\n#  file reading or shell command tools to access the skill instructions in\n#  `SKILL.md` files.\n#\n\nagent = ReActAgent(\n    name=\"Friday\",\n    sys_prompt=\"You are a helpful assistant named Friday.\",\n    model=DashScopeChatModel(\n        model_name=\"qwen3-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n    ),\n    memory=InMemoryMemory(),\n    formatter=DashScopeChatFormatter(),\n    toolkit=toolkit,\n)\n\nprint(\"Agent's System Prompt with Agent Skills:\")\nprint(agent.sys_prompt)\n"
  },
  {
    "path": "docs/tutorial/en/src/task_embedding.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _embedding:\n\nEmbedding\n=========================\n\nIn AgentScope, the embedding module provides a unified interface for vector representation generation, which features:\n\n- Support **caching embeddings** to avoid redundant API calls\n- Support **multiple embedding providers** with a consistent API\n\nAgentScope has built-in embedding classes for the following API providers:\n\n.. list-table::\n    :header-rows: 1\n\n    * - Provider\n      - Class\n    * - OpenAI\n      - ``OpenAITextEmbedding``\n    * - Gemini\n      - ``GeminiTextEmbedding``\n    * - DashScope\n      - ``DashScopeTextEmbedding``, ``DashScopeMultiModalEmbedding``\n    * - Ollama\n      - ``OllamaTextEmbedding``\n\nAll classes inherit from ``EmbeddingModelBase``, implementing the ``__call__`` method and generating ``EmbeddingResponse`` object with the embeddings and usage information.\nThe ``DashScopeMultiModalEmbedding`` supports multi-modal embeddings for text, images, and videos.\n\nTaking the DashScope embedding class as an example, you can use it as follows:\n\"\"\"\n\nimport asyncio\nimport os\nimport tempfile\n\nfrom agentscope.embedding import DashScopeTextEmbedding, FileEmbeddingCache\n\n\nasync def example_dashscope_embedding() -> None:\n    \"\"\"Example usage of DashScope text embedding.\"\"\"\n    texts = [\n        \"What is the capital of France?\",\n        \"Paris is the capital city of France.\",\n    ]\n\n    # Initialize the DashScope text embedding instance\n    embedding_model = DashScopeTextEmbedding(\n        model_name=\"text-embedding-v2\",\n        api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n    )\n\n    # Get the embedding from the model\n    response = await embedding_model(texts)\n\n    print(\"The embedding ID: \", response.id)\n    print(\"The embedding create at: \", response.created_at)\n    print(\"The embedding usage: \", response.usage)\n    print(\"The embedding:\")\n    print(response.embeddings)\n\n\nasyncio.run(example_dashscope_embedding())\n\n# %%\n# You can customize your embedding model by subclassing ``EmbeddingModelBase`` and implementing the ``__call__`` method.\n#\n# Embedding Cache\n# ---------------------\n# AgentScope provides a base class ``EmbeddingCacheBase`` for caching embeddings, as well as a file-based implementation ``FileEmbeddingCache``.\n# It works as follows in the embedding module:\n#\n# .. image:: ../../_static/images/embedding_cache.png\n#   :align: center\n#   :width: 90%\n#\n# To use caching, just pass an instance of ``FileEmbeddingCache`` (or your custom cache) to the embedding model's constructor as follows:\n#\n\n\nasync def example_embedding_cache() -> None:\n    \"\"\"Demonstrate embedding with cache functionality.\"\"\"\n    # Example texts\n    texts = [\n        \"What is the capital of France?\",\n        \"Paris is the capital city of France.\",\n    ]\n\n    # Create a temporary directory for cache demonstration\n    # In real applications, you might want to use a persistent directory\n    cache_dir = tempfile.mkdtemp(prefix=\"embedding_cache_\")\n    print(f\"Using cache directory: {cache_dir}\")\n\n    # Initialize the embedding model with cache\n    # We limit the cache to 100 files and 10MB for demonstration purposes\n    embedder = DashScopeTextEmbedding(\n        model_name=\"text-embedding-v3\",\n        api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n        embedding_cache=FileEmbeddingCache(\n            cache_dir=cache_dir,\n            max_file_number=100,\n            max_cache_size=10,  # Maximum cache size in MB\n        ),\n    )\n\n    # First call - will fetch from API and store in cache\n    print(\"\\n=== First API Call (No Cache Hit) ===\")\n    start_time = asyncio.get_event_loop().time()\n    response1 = await embedder(texts)\n    elapsed_time1 = asyncio.get_event_loop().time() - start_time\n    print(f\"Source: {response1.source}\")  # Should be 'api'\n    print(f\"Time taken: {elapsed_time1:.4f} seconds\")\n    print(f\"Tokens used: {response1.usage.tokens}\")\n\n    # Second call with the same texts - should use cache\n    print(\"\\n=== Second API Call (Cache Hit Expected) ===\")\n    start_time = asyncio.get_event_loop().time()\n    response2 = await embedder(texts)\n    elapsed_time2 = asyncio.get_event_loop().time() - start_time\n    print(f\"Source: {response2.source}\")  # Should be 'cache'\n    print(f\"Time taken: {elapsed_time2:.4f} seconds\")\n    print(\n        f\"Tokens used: {response2.usage.tokens}\",\n    )  # Should be 0 for cached results\n    print(\n        f\"Speed improvement: {elapsed_time1 / elapsed_time2:.1f}x faster with cache\",\n    )\n\n\nasyncio.run(example_embedding_cache())\n"
  },
  {
    "path": "docs/tutorial/en/src/task_eval.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _eval:\n\nEvaluation\n=========================\n\nAgentScope provides a built-in evaluation framework for assessing agent performance across different tasks and benchmarks, featuring:\n\n- `Ray <https://github.com/ray-project/ray>`_-based parallel and distributed evaluation\n- Support continuation after interruption\n- 🚧 Visualization of evaluation results\n\n.. note:: We are keeping integrating new benchmarks into AgentScope:\n\n - ✅ `ACEBench <https://github.com/ACEBench/ACEBench>`_\n - 🚧 `GAIA <https://huggingface.co/datasets/gaia-benchmark/GAIA/tree/main>`_ Benchmark\n\n\nOverview\n---------------------------\n\nThe AgentScope evaluation framework consists of several key components:\n\n- **Benchmark**: Collections of tasks for systematic evaluation\n    - **Task**: Individual evaluation units with inputs, ground truth, and metrics\n        - **Metric**: Measurement functions that assess solution quality\n- **Evaluator**: Engine that runs evaluation, aggregates results, and analyzes performance\n    - **Evaluator Storage**: Persistent storage for recording and retrieving evaluation results\n- **Solution**: The user-defined solution\n\n.. figure:: ../../_static/images/evaluation.png\n    :width: 90%\n    :alt: AgentScope Evaluation Framework\n\n    *AgentScope Evaluation Framework*\n\nThe current implementation in AgentScope includes:\n\n- Evaluator:\n    - ``RayEvaluator``: A ray-based evaluator that supports parallel and distributed evaluation.\n    - ``GeneralEvaluator``: A general evaluator that runs tasks sequentially, friendly for debugging.\n- Benchmark:\n    - ``ACEBench``: A benchmark for evaluating agent capabilities.\n\nWe have provided a toy example in our `GitHub repository <https://github.com/agentscope-ai/agentscope/tree/main/examples/evaluation/ace_bench>`_ with ``RayEvaluator`` and the agent multistep tasks in ACEBench.\n\nCore Components\n---------------\nWe are going to build a simple toy math question benchmark to demonstrate\nhow to use the AgentScope evaluation module.\n\"\"\"\n\nTOY_BENCHMARK = [\n    {\n        \"id\": \"math_problem_1\",\n        \"question\": \"What is 2 + 2?\",\n        \"ground_truth\": 4.0,\n        \"tags\": {\n            \"difficulty\": \"easy\",\n            \"category\": \"math\",\n        },\n    },\n    {\n        \"id\": \"math_problem_2\",\n        \"question\": \"What is 12345 + 54321 + 6789 + 9876?\",\n        \"ground_truth\": 83331,\n        \"tags\": {\n            \"difficulty\": \"medium\",\n            \"category\": \"math\",\n        },\n    },\n]\n\n# %%\n# From Tasks, Solutions and Metrics to Benchmark\n# ~~~~~~~~~~~~~~~~~~~\n#\n# - A ``SolutionOutput`` contains all information generated by the agent, including the trajectory and final output.\n# - A ``Metric`` represents a single evaluation callable instance that compares the generated solution (e.g., trajectory or final output) to the ground truth.\n# In the toy example, we define a metric that simply checks whether the ``output`` field in the solution matches the ground truth.\n\nfrom agentscope.evaluate import (\n    SolutionOutput,\n    MetricBase,\n    MetricResult,\n    MetricType,\n)\n\n\nclass CheckEqual(MetricBase):\n    def __init__(\n        self,\n        ground_truth: float,\n    ):\n        super().__init__(\n            name=\"math check number equal\",\n            metric_type=MetricType.NUMERICAL,\n            description=\"Toy metric checking if two numbers are equal\",\n            categories=[],\n        )\n        self.ground_truth = ground_truth\n\n    async def __call__(\n        self,\n        solution: SolutionOutput,\n    ) -> MetricResult:\n        if solution.output == self.ground_truth:\n            return MetricResult(\n                name=self.name,\n                result=1.0,\n                message=\"Correct\",\n            )\n        else:\n            return MetricResult(\n                name=self.name,\n                result=0.0,\n                message=\"Incorrect\",\n            )\n\n\n# %%\n# - A ``Task`` is a unit in the benchmark that includes all information for the agent to execute and evaluate (e.g., input/query and its ground truth).\n# - A ``Benchmark`` organizes multiple tasks for systematic evaluation.\n\nfrom typing import Generator\nfrom agentscope.evaluate import (\n    Task,\n    BenchmarkBase,\n)\n\n\nclass ToyBenchmark(BenchmarkBase):\n    def __init__(self):\n        super().__init__(\n            name=\"Toy bench\",\n            description=\"A toy benchmark for demonstrating the evaluation module.\",\n        )\n        self.dataset = self._load_data()\n\n    @staticmethod\n    def _load_data() -> list[Task]:\n        dataset = []\n        for item in TOY_BENCHMARK:\n            dataset.append(\n                Task(\n                    id=item[\"id\"],\n                    input=item[\"question\"],\n                    ground_truth=item[\"ground_truth\"],\n                    tags=item.get(\"tags\", {}),\n                    metrics=[\n                        CheckEqual(item[\"ground_truth\"]),\n                    ],\n                    metadata={},\n                ),\n            )\n        return dataset\n\n    def __iter__(self) -> Generator[Task, None, None]:\n        \"\"\"Iterate over the benchmark.\"\"\"\n        for task in self.dataset:\n            yield task\n\n    def __getitem__(self, index: int) -> Task:\n        \"\"\"Get a task by index.\"\"\"\n        return self.dataset[index]\n\n    def __len__(self) -> int:\n        \"\"\"Get the length of the benchmark.\"\"\"\n        return len(self.dataset)\n\n\n# %%\n# Evaluators\n# ~~~~~~~~~~\n#\n# Evaluators manage the evaluation process. They can automatically iterate through the\n# tasks in the benchmark and feed each task into a solution-generation function,\n# where developers need to define the logic for running agents and retrieving\n# the execution result and trajectory. Below is an example of\n# running ``GeneralEvaluator`` with our toy benchmark. If there is a large\n# benchmark and the developer wants to get the evaluation more efficiently\n# through parallelization, ``RayEvaluator`` is available as a built-in solution\n# as well.\n\n\nimport os\nimport asyncio\nfrom typing import Callable\nfrom pydantic import BaseModel\n\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.agent import ReActAgent\n\nfrom agentscope.evaluate import (\n    GeneralEvaluator,\n    FileEvaluatorStorage,\n)\n\n\nclass ToyBenchAnswerFormat(BaseModel):\n    answer_as_number: float\n\n\nasync def toy_solution_generation(\n    task: Task,\n    pre_hook: Callable,\n) -> SolutionOutput:\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"You are a helpful assistant named Friday. \"\n        \"Your target is to solve the given task with your tools. \"\n        \"Try to solve the task as best as you can.\",\n        model=DashScopeChatModel(\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            model_name=\"qwen-max\",\n            stream=False,\n        ),\n        formatter=DashScopeChatFormatter(),\n    )\n    agent.register_instance_hook(\n        \"pre_print\",\n        \"save_logging\",\n        pre_hook,\n    )\n    msg_input = Msg(\"user\", task.input, role=\"user\")\n    res = await agent(\n        msg_input,\n        structured_model=ToyBenchAnswerFormat,\n    )\n    return SolutionOutput(\n        success=True,\n        output=res.metadata.get(\"answer_as_number\", None),\n        trajectory=[],\n    )\n\n\nasync def main() -> None:\n    evaluator = GeneralEvaluator(\n        name=\"Toy benchmark evaluation\",\n        benchmark=ToyBenchmark(),\n        # Repeat how many times\n        n_repeat=1,\n        storage=FileEvaluatorStorage(\n            save_dir=\"./results\",\n        ),\n        # How many workers to use\n        n_workers=1,\n    )\n\n    # Run the evaluation\n    await evaluator.run(toy_solution_generation)\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "docs/tutorial/en/src/task_eval_openjudge.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nEvaluation with OpenJudge\n=========================\n\nThis guide introduces how to use [OpenJudge](https://github.com/agentscope-ai/OpenJudge) graders as AgentScope metrics to evaluate your multi-agent applications.\nOpenJudge is a comprehensive evaluation system designed to assess the quality of LLM applications. By integrating OpenJudge into AgentScope, you can extend AgentScope's native evaluation capabilities from basic execution checks to deep, semantic quality analysis.\n\n\n.. note::\n   Install dependencies before running:\n\n   .. code-block:: bash\n\n       pip install agentscope py-openjudge\n\n\nOverview\n--------\nWhile AgentScope provides a robust `MetricBase` for defining evaluation logic, implementing complex, semantic-level metrics (like \"Hallucination Detection\" or \"Response Relevance\") often requires\nsignificant effort in prompt engineering and pipeline construction.\n\nIntegrating OpenJudge brings three dimensions of capability extension to AgentScope:\n\n1.  **Enhance Evaluation Depth:**: Move beyond simple success/failure checks to multi-dimensional assessments (Accuracy, Safety, Tone, etc.).\n2.  **Leverage Verified Graders**: Instantly access 50+ pre-built, expert-level graders without writing custom evaluation prompts, see the [OpenJudge documentation](https://agentscope-ai.github.io/OpenJudge/built_in_graders/overview/) for details.\n3.  **Closed-loop Iteration**: Seamlessly embed OpenJudge into AgentScope's `Benchmark`, obtaining quantitative scores and qualitative reasoning.\n\n\nHow to Evaluate with OpenJudge\n--------------------\n\nWe are going to build a simple QA benchmark to demonstrate how to use the AgentScope evaluation module by integrating OpenJudge's graders.\n\"\"\"\n\n# %%\nQA_BENCHMARK_DATASET = [\n    {\n        \"id\": \"qa_task_1\",\n        \"question\": \"What are the health benefits of regular exercise?\",\n        \"reference_output\": \"Regular exercise improves cardiovascular health, strengthens muscles and bones, \"\n        \"helps maintain a healthy weight, and can improve mental health by reducing anxiety and depression.\",\n        \"ground_truth\": \"Answers should cover physical and mental health benefits\",\n        \"difficulty\": \"medium\",\n        \"category\": \"health\",\n    },\n    {\n        \"id\": \"qa_task_2\",\n        \"question\": \"Describe the main causes of climate change.\",\n        \"reference_output\": \"Climate change is primarily caused by increased concentrations of greenhouse gases \"\n        \"in the atmosphere due to human activities like burning fossil fuels, deforestation, and industrial processes.\",\n        \"ground_truth\": \"Answers should mention greenhouse gases and human activities\",\n        \"difficulty\": \"hard\",\n        \"category\": \"environment\",\n    },\n    {\n        \"id\": \"qa_task_3\",\n        \"question\": \"What is the significance of the Turing Test in AI?\",\n        \"reference_output\": \"The Turing Test, proposed by Alan Turing, is a measure of a machine's ability to exhibit\"\n        \" intelligent behavior equivalent to, or indistinguishable from, that of a human.\",\n        \"ground_truth\": \"Should mention Alan Turing, purpose of the test, and its implications for AI\",\n        \"difficulty\": \"hard\",\n        \"category\": \"technology\",\n    },\n]\n\n\n# %% [markdown]\n# AgentScope Metric vs. OpenJudge Grader\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# To make OpenJudge compatible with AgentScope, we need an adapter that inherits from\n# AgentScope's ``MetricBase`` and acts as a bridge to OpenJudge's ``BaseGrader``.\n#\n# * **AgentScope Metric**: A generic unit of evaluation that accepts a ``SolutionOutput`` and returns a ``MetricResult``.\n# * **OpenJudge Grader**: A specialized evaluation unit (e.g., ``RelevanceGrader``) that requires specific, semantic inputs (like ``query``, ``response``, ``context``), and returns a ``GraderResult``.\n#\n# This \"Adapter\" allows you to plug *any* OpenJudge grader into your AgentScope benchmark seamlessly.\n#\n\n# %%\nfrom openjudge.graders.base_grader import BaseGrader\nfrom openjudge.graders.schema import GraderScore, GraderError\nfrom openjudge.utils.mapping import parse_data_with_mapper\nfrom agentscope.evaluate import (\n    MetricBase,\n    MetricType,\n    MetricResult,\n    SolutionOutput,\n)\n\n\nclass OpenJudgeMetric(MetricBase):\n    \"\"\"\n    A wrapper that converts an OpenJudge grader into an AgentScope Metric.\n    \"\"\"\n\n    def __init__(\n        self,\n        grader_cls: type[BaseGrader],\n        data: dict,\n        mapper: dict,\n        name: str | None = None,\n        description: str | None = None,\n        **grader_kwargs,\n    ):\n        # Initialize the OpenJudge grader\n        self.grader = grader_cls(**grader_kwargs)\n\n        super().__init__(\n            name=name or self.grader.name,\n            metric_type=MetricType.NUMERICAL,\n            description=description or self.grader.description,\n        )\n\n        self.data = data\n        self.mapper = mapper\n\n    async def __call__(self, solution: SolutionOutput) -> MetricResult:\n        \"\"\"Execute the wrapped OpenJudge grader against the agent solution.\"\"\"\n        if not solution.success:\n            return MetricResult(\n                name=self.name,\n                result=0.0,\n                message=\"Solution failed\",\n            )\n\n        try:\n            # 1. Context Construction\n            # Combine Static Task Context (item) and Dynamic Agent Output (solution)\n            combined_data = {\n                \"data\": self.data,\n                \"solution\": {\n                    \"output\": solution.output,\n                    \"meta\": solution.meta,\n                    \"trajectory\": getattr(solution, \"trajectory\", []),\n                },\n            }\n\n            # 2. Data Mapping\n            # Use the mapper to extract 'query', 'response', 'context' from the combined data\n            grader_inputs = parse_data_with_mapper(\n                combined_data,\n                self.mapper,\n            )\n\n            # 3. Evaluation Execution\n            result = await self.grader.aevaluate(**grader_inputs)\n\n            # 4. Result Formatting\n            if isinstance(result, GraderScore):\n                return MetricResult(\n                    name=self.name,\n                    result=result.score,\n                    # We preserve the detailed reasoning provided by OpenJudge\n                    message=result.reason or \"\",\n                )\n            elif isinstance(result, GraderError):\n                return MetricResult(\n                    name=self.name,\n                    result=0.0,\n                    message=f\"Error: {result.error}\",\n                )\n            else:\n                return MetricResult(\n                    name=self.name,\n                    result=0.0,\n                    message=\"Unknown result type\",\n                )\n\n        except Exception as e:\n            return MetricResult(\n                name=self.name,\n                result=0.0,\n                message=f\"Exception: {str(e)}\",\n            )\n\n\n# %% [markdown]\n# From OpenJudge's Graders to AgentScope's Benchmark\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# OpenJudge provides a rich collection of built-in graders. In this example, we select two\n# common graders suitable for Question-Answering tasks:\n#\n# * **RelevanceGrader**: Evaluates whether the agent's response directly addresses the user's query.\n# * **CorrectnessGrader**: Verifies the factual accuracy of the response against a provided ground truth.\n#\n# .. tip::\n#    OpenJudge offers 50+ built-in graders covering diverse dimensions like **Hallucination**, **Safety**, **Code Quality**,\n#    and **JSON Formatting**. Please refer to the `OpenJudge Documentation <https://agentscope-ai.github.io/OpenJudge/built_in_graders/overview/>`_\n#    for the full list of available graders.\n#\n# .. note::\n#    Ensure you have set your ``DASHSCOPE_API_KEY`` environment variable before running the example below.\n\n# %%\nimport os\nfrom typing import Generator\nfrom openjudge.graders.common.relevance import RelevanceGrader\nfrom openjudge.graders.common.correctness import CorrectnessGrader\nfrom agentscope.evaluate import (\n    Task,\n    BenchmarkBase,\n)\n\n\nclass QABenchmark(BenchmarkBase):\n    \"\"\"A benchmark for QA tasks using OpenJudge metrics.\"\"\"\n\n    def __init__(self):\n        super().__init__(\n            name=\"QA Quality Benchmark\",\n            description=\"Benchmark to evaluate QA systems using OpenJudge grader classes\",\n        )\n        self.dataset = self._load_data()\n\n    def _load_data(self):\n        tasks = []\n        # Configuration for LLM-based graders\n        # Ensure OPENAI_API_KEY is set in your environment variables\n        model_config = {\n            \"model\": \"qwen3-32b\",\n            \"api_key\": os.environ.get(\"DASHSCOPE_API_KEY\"),\n            \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n        }\n\n        for data in QA_BENCHMARK_DATASET:\n            # Define the Mapping: Left is OpenJudge key, Right is AgentScope path\n            mapper = {\n                \"query\": \"data.input\",\n                \"response\": \"solution.output\",\n                \"context\": \"data.ground_truth\",\n                \"reference_response\": \"data.reference_output\",\n            }\n\n            # Instantiate Metrics via Wrapper\n            metrics = [\n                OpenJudgeMetric(\n                    grader_cls=RelevanceGrader,\n                    data=data,\n                    mapper=mapper,\n                    name=\"Relevance\",\n                    model=model_config,\n                ),\n                OpenJudgeMetric(\n                    grader_cls=CorrectnessGrader,\n                    data=data,\n                    mapper=mapper,\n                    name=\"Correctness\",\n                    model=model_config,\n                ),\n            ]\n\n            # Create Task\n            task = Task(\n                id=data[\"id\"],\n                input=data[\"question\"],\n                ground_truth=data[\"ground_truth\"],\n                metrics=metrics,\n            )\n\n            tasks.append(task)\n\n        return tasks\n\n    def __iter__(self) -> Generator[Task, None, None]:\n        \"\"\"Iterate over the benchmark.\"\"\"\n        yield from self.dataset\n\n    def __getitem__(self, index: int) -> Task:\n        \"\"\"Get a task by index.\"\"\"\n        return self.dataset[index]\n\n    def __len__(self) -> int:\n        \"\"\"Get the length of the benchmark.\"\"\"\n        return len(self.dataset)\n\n\n# %% [markdown]\n# Run Evaluation\n# ~~~~~~~~~~\n# Finally, use AgentScope's ``GeneralEvaluator`` to run the benchmark on a QA agent.\n# The results will include both the **Quantitative Score** and the **Qualitative Reasoning**\n# from the OpenJudge graders.\n\n# %%\n\nfrom typing import Callable\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.evaluate import GeneralEvaluator\nfrom agentscope.evaluate import FileEvaluatorStorage\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.message import Msg\nfrom agentscope.model import OpenAIChatModel\n\n\nasync def qa_agent(task: Task, pre_hook: Callable) -> SolutionOutput:\n    \"\"\"Solution function that generates answers to QA tasks.\"\"\"\n\n    model = OpenAIChatModel(\n        model_name=\"qwen3-32b\",\n        api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n    )\n\n    # Create a QA agent\n    agent = ReActAgent(\n        name=\"QAAgent\",\n        sys_prompt=\"You are an expert at answering questions. Provide clear, accurate, and comprehensive answers.\",\n        model=model,\n        formatter=DashScopeChatFormatter(),\n    )\n\n    # Generate response\n    msg_input = Msg(name=\"User\", content=task.input, role=\"user\")\n    response = await agent(msg_input)\n    response_text = response.content\n\n    return SolutionOutput(\n        success=True,\n        output=response_text,\n        trajectory=[\n            task.input,\n            response_text,\n        ],  # Store the interaction trajectory\n    )\n\n\nasync def main() -> None:\n    evaluator = GeneralEvaluator(\n        name=\"OpenJudge Integration Demo\",\n        benchmark=QABenchmark(),\n        # Repeat how many times\n        n_repeat=1,\n        storage=FileEvaluatorStorage(\n            save_dir=\"./results\",\n        ),\n        # How many workers to use\n        n_workers=1,\n    )\n\n    await evaluator.run(qa_agent)\n"
  },
  {
    "path": "docs/tutorial/en/src/task_hook.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _hook:\n\nAgent Hooks\n===========================\n\nHooks are extension points in AgentScope that allow developers to customize agent behaviors at specific execution points, providing a flexible way to modify or extend the agent's functionality without changing its core implementation.\n\nIn AgentScope, hooks are implemented around the agent's core functions:\n\n\n.. list-table:: Supported hook types in AgentScope\n    :header-rows: 1\n\n    * - Agent Class\n      - Core Function\n      - Hook Types\n      - Description\n    * - | ``AgentBase`` &\n        | its child classes\n      - ``reply``\n      - | ``pre_reply``\n        | ``post_reply``\n      - The hooks before/after agent replying to a message\n    * -\n      - ``print``\n      - | ``pre_print``\n        | ``post_print``\n      - The hook before/after printing a message to the target output (e.g., terminal, web interface)\n    * -\n      - ``observe``\n      - | ``pre_observe``\n        | ``post_observe``\n      - The hooks before/after observing a message from the environment or other agents\n    * - | ``ReActAgentBase`` &\n        | its child classes\n      - | ``reply``\n        | ``print``\n        | ``observe``\n      - | ``pre_reply``\n        | ``post_reply``\n        | ``pre_print``\n        | ``post_print``\n        | ``pre_observe``\n        | ``post_observe``\n      -\n    * -\n      - ``_reasoning``\n      - | ``pre_reasoning``\n        | ``post_reasoning``\n      - The hooks before/after the agent's reasoning process\n    * -\n      - ``_acting``\n      - | ``pre_acting``\n        | ``post_acting``\n      - The hooks before/after the agent's acting process\n\n.. tip:: Since hooks in AgentScope are implemented using a metaclass, they support inheritance.\n\nTo simplify the usage, AgentScope provides unified signatures for all hooks.\n\n\"\"\"\nimport asyncio\nfrom typing import Any, Type\n\nfrom agentscope.agent import ReActAgentBase, AgentBase\nfrom agentscope.message import Msg\n\n\n# %%\n# Hook Signature\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# AgentScope provides unified hook signatures for all pre- and post-hooks as follows:\n#\n# **Pre-Hook Signature**\n#\n# .. list-table:: The signature of all pre-hooks\n#   :header-rows: 1\n#\n#   * -\n#     - Name\n#     - Description\n#   * - Arguments\n#     - ``self: AgentBase | ReActAgentBase``\n#     - The agent instance\n#   * -\n#     - ``kwargs: dict[str, Any]``\n#     - | The input arguments of the target\n#       | function, or the modified arguments\n#       | from the most recent non-None return\n#       | value of previous hooks\n#   * - Returns\n#     - ``dict[str, Any] | None``\n#     - The modified arguments or None\n#\n# .. note:: All positional arguments and keyword arguments of the core function are passed as a single ``kwargs`` dict to the hook functions\n#\n# A pre-hook template is defined as follows:\n#\n\n\ndef pre_hook_template(\n    self: AgentBase | ReActAgentBase,\n    kwargs: dict[str, Any],\n) -> dict[str, Any] | None:  # The modified displayed message\n    \"\"\"Pre hook template.\"\"\"\n    pass\n\n\n# %%\n# **Post-Hook Signature**\n#\n# For post hooks, an additional ``output`` argument is added to the signature, which represents the output of the target function.\n# If the core function has no output, the ``output`` argument will be ``None``.\n#\n# .. list-table:: The signature of all post-hooks\n#   :header-rows: 1\n#\n#   * -\n#     - Name\n#     - Description\n#   * - Arguments\n#     - ``self: AgentBase | ReActAgentBase``\n#     - The agent instance\n#   * -\n#     - ``kwargs: dict[str, Any]``\n#     - | A dict that contains all the arguments\n#       | of the target function\n#   * -\n#     - ``output: Any``\n#     - | The output of the target function or\n#       | the most recent non-None return value\n#       | from previous hooks\n#   * - Returns\n#     - ``dict[str, Any] | None``\n#     - The modified arguments or None\n#\n\n\ndef post_hook_template(\n    self: AgentBase | ReActAgentBase,\n    kwargs: dict[str, Any],\n    output: Any,  # The output of the target function\n) -> Any:  # The modified output\n    \"\"\"Post hook template.\"\"\"\n    pass\n\n\n# %%\n# Hook Management\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# AgentScope provides both instance- and class-level hooks, depending on the effective scope of the hooks.\n# They execute in the following order:\n#\n# .. image:: ../../_static/images/sequential_hook.png\n#   :width: 90%\n#   :align: center\n#   :alt: Hooks in AgentScope\n#   :class: bordered-image\n#\n# AgentScope provides built-in methods to manage hooks at both instance and class levels as follows:\n#\n# .. list-table:: Hook management methods in AgentScope\n#   :header-rows: 1\n#\n#   * - Level\n#     - Method\n#     - Description\n#   * - Instance-level\n#     - ``register_instance_hook``\n#     - | Register a hook for the current object with\n#       | given hook type and name.\n#   * -\n#     - ``remove_instance_hook``\n#     - | Remove a hook for the current object with\n#       | given hook type and name.\n#   * -\n#     - ``clear_instance_hooks``\n#     - | Clear all hooks for the current object with\n#       | given hook type.\n#   * - Class-level\n#     - ``register_class_hook``\n#     - | Register a hook for all objects of the class\n#       | with given hook type and name.\n#   * -\n#     - ``remove_class_hook``\n#     - | Remove a hook for all objects of the class\n#       | with given hook type and name.\n#   * -\n#     - ``clear_class_hooks``\n#     - | Clear all hooks for all objects of the\n#       | class with given hook type.\n#\n# When using hooks, you MUST follow these rules:\n#\n# .. important:: **Execution Order**\n#\n#  - Hooks are executed in registration order\n#  - Multiple hooks can be chained together\n#  **Return Value Handling**\n#\n#  - For pre-hooks: Non-None return values are passed to the next hook or core function\n#   - When a hook returns None, the next hook will use the most recent non-None return value from previous hooks\n#   - If all previous hooks return None, the next hook receives a copy of the original arguments\n#   - The final non-None return value (or original arguments if all hooks return None) is passed to the core function\n#  - For post-hooks: Works the same way as pre-hooks.\n#  **Important**: Never call the core function (reply/speak/observe/_reasoning/_acting) within a hook to avoid infinite loops\n#\n# Taking the following agent as an example, we can see how to register, remove and clear hooks:\n#\n\n\n# Create a simple test agent class\nclass TestAgent(AgentBase):\n    \"\"\"A test agent for demonstrating hooks.\"\"\"\n\n    async def reply(self, msg: Msg) -> Msg:\n        \"\"\"Reply to the message.\"\"\"\n        return msg\n\n\n# %%\n# We create an instance-level hook and a class-level hook to modify the message content before replying.\n#\n\n\n# Create two pre-reply hooks\ndef instance_pre_reply_hook(\n    self: AgentBase,\n    kwargs: dict[str, Any],\n) -> dict[str, Any]:\n    \"\"\"A pre-reply hook that modifies the message content.\"\"\"\n    msg = kwargs[\"msg\"]\n    msg.content += \"[instance-pre-reply]\"\n    # return modified kwargs\n    return {\n        **kwargs,\n        \"msg\": msg,\n    }\n\n\ndef cls_pre_reply_hook(\n    self: AgentBase,\n    kwargs: dict[str, Any],\n) -> dict[str, Any]:\n    \"\"\"A pre-reply hook that modifies the message content.\"\"\"\n    msg = kwargs[\"msg\"]\n    msg.content += \"[cls-pre-reply]\"\n    # return modified kwargs\n    return {\n        **kwargs,\n        \"msg\": msg,\n    }\n\n\n# Register class hook\nTestAgent.register_class_hook(\n    hook_type=\"pre_reply\",\n    hook_name=\"test_pre_reply\",\n    hook=cls_pre_reply_hook,\n)\n\n# Register instance hook\nagent = TestAgent()\nagent.register_instance_hook(\n    hook_type=\"pre_reply\",\n    hook_name=\"test_pre_reply\",\n    hook=instance_pre_reply_hook,\n)\n\n\nasync def example_test_hook() -> None:\n    \"\"\"An example function to test the hooks.\"\"\"\n    msg = Msg(\n        name=\"user\",\n        content=\"Hello, world!\",\n        role=\"user\",\n    )\n    res = await agent(msg)\n    print(\"Response content:\", res.content)\n    TestAgent.clear_class_hooks()\n\n\nasyncio.run(example_test_hook())\n\n# %%\n# We can see that a \"[instance-pre-reply]\" and a \"[cls-pre-reply]\" are added to the message content.\n#\n"
  },
  {
    "path": "docs/tutorial/en/src/task_long_term_memory.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _long-term-memory:\n\nLong-Term Memory\n========================\n\nIn AgentScope, we provide a basic class for long-term memory (``LongTermMemoryBase``) and an implementation based on the `mem0 <https://github.com/mem0ai/mem0>`_ library (``Mem0LongTermMemory``).\nTogether with the design of ``ReActAgent`` class in :ref:`agent` section, we provide two long-term memory modes:\n\n- ``agent_control``: the agent autonomously manages long-term memory by tool calls, and\n- ``static_control``: the developer explicitly controls long-term memory operations.\n\nDevelopers can also use the ``both`` mode, which activates both memory management modes.\n\n.. hint:: These memory modes are suitable for different usage scenarios. Developers can choose the appropriate mode based on their needs.\n\nUsing mem0 Long-Term Memory\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n.. note:: We provide an example of using mem0 long-term memory in the GitHub repository under the ``examples/long_term_memory/mem0`` directory.\n\n\"\"\"\n\nimport os\nimport asyncio\n\nfrom agentscope.message import Msg\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import Toolkit\n\n\n# Create mem0 long-term memory instance\nfrom agentscope.memory import Mem0LongTermMemory\nfrom agentscope.embedding import DashScopeTextEmbedding\n\n\nlong_term_memory = Mem0LongTermMemory(\n    agent_name=\"Friday\",\n    user_name=\"user_123\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max-latest\",\n        api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n        stream=False,\n    ),\n    embedding_model=DashScopeTextEmbedding(\n        model_name=\"text-embedding-v2\",\n        api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n    ),\n    on_disk=False,\n)\n\n# %%\n# The ``Mem0LongTermMemory`` class provides two main methods for long-term memory operations:\n# ``record`` and ``retrieve``.\n# They take a list of messages as input and record/retrieve information from long-term memory.\n#\n# As an example, we first store a user preference and then retrieve related information from long-term memory.\n#\n\n\n# Basic usage example\nasync def basic_usage():\n    \"\"\"Basic usage example\"\"\"\n    # Record memory\n    await long_term_memory.record(\n        [Msg(\"user\", \"I like staying in homestays\", \"user\")],\n    )\n\n    # Retrieve memory\n    results = await long_term_memory.retrieve(\n        [Msg(\"user\", \"My accommodation preferences\", \"user\")],\n    )\n    print(f\"Retrieval results: {results}\")\n\n\nasyncio.run(basic_usage())\n\n# %%\n# Integration with ReAct Agent\n# ----------------------------------------\n# In AgentScope, the ``ReActAgent`` class receives a ``long_term_memory``\n# parameter in its constructor, as well as a ``long_term_memory_mode`` parameter\n# that specifies the long-term memory mode.\n#\n# If ``long_term_memory_mode`` is set to ``agent_control`` or ``both``, two\n# tool functions ``record_to_memory`` and ``retrieve_from_memory`` will be\n# registered in the agent's toolkit, allowing the agent to autonomously\n# manage long-term memory through tool calls.\n#\n# .. note:: To achieve the best results, the ``\"agent_control\"`` mode may require\n#  additional instructions in the system prompt.\n#\n\n# Create ReAct agent with long-term memory\nagent = ReActAgent(\n    name=\"Friday\",\n    sys_prompt=\"You are an assistant with long-term memory capabilities.\",\n    model=DashScopeChatModel(\n        api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n        model_name=\"qwen-max-latest\",\n    ),\n    formatter=DashScopeChatFormatter(),\n    toolkit=Toolkit(),\n    memory=InMemoryMemory(),\n    long_term_memory=long_term_memory,\n    long_term_memory_mode=\"static_control\",  # Use static_control mode\n)\n\n\nasync def record_preferences():\n    \"\"\"ReAct agent integration example\"\"\"\n    # Conversation example\n    msg = Msg(\n        \"user\",\n        \"When I travel to Hangzhou, I like staying in homestays\",\n        \"user\",\n    )\n    await agent(msg)\n\n\nasyncio.run(record_preferences())\n\n# %%\n# Then we clear the short-term memory and ask the agent about the user's preferences.\n#\n\n\nasync def retrieve_preferences():\n    \"\"\"Retrieve user preferences from long-term memory\"\"\"\n    # Clear short-term memory\n    await agent.memory.clear()\n    # The agent will remember previous conversations\n    msg2 = Msg(\"user\", \"What are my preferences? Answer briefly.\", \"user\")\n    await agent(msg2)\n\n\nasyncio.run(retrieve_preferences())\n\n\n# %%\n# Using ReMe Long-Term Memory\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# .. note:: We provide an example of using ReMe long-term memory in the GitHub repository under the ``examples/long_term_memory/reme`` directory.\n#\n# .. code-block:: python\n#     :caption: Example of ReMe long-term memory setup\n#\n#     from agentscope.memory import ReMePersonalLongTermMemory\n#\n#     # Create ReMe personal long-term memory instance\n#     reme_long_term_memory = ReMePersonalLongTermMemory(\n#         agent_name=\"Friday\",\n#         user_name=\"user_123\",\n#         model=DashScopeChatModel(\n#             model_name=\"qwen3-max\",\n#             api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n#             stream=False,\n#         ),\n#         embedding_model=DashScopeTextEmbedding(\n#             model_name=\"text-embedding-v4\",\n#             api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n#             dimensions=1024,\n#         ),\n#     )\n#\n#\n# The ``ReMePersonalLongTermMemory`` class provides four main methods for long-term memory operations.\n# They include ``record_to_memory`` and ``retrieve_from_memory`` for tool calls,\n# as well as ``record`` and ``retrieve`` for direct calls.\n#\n# As an example, we use ``record_to_memory`` to record user preferences.\n#\n# .. code-block:: python\n#     :caption: Example of recording to ReMe long-term memory\n#\n#     async def test_record_to_memory():\n#         \"\"\"Test record_to_memory tool function interface\"\"\"\n#         async with reme_long_term_memory:\n#             result = await reme_long_term_memory.record_to_memory(\n#                 thinking=\"The user is sharing their travel preferences and habits\",\n#                 content=[\n#                     \"I prefer to stay in homestays when traveling to Hangzhou\",\n#                     \"I like to visit the West Lake in the morning\",\n#                     \"I enjoy drinking Longjing tea\",\n#                 ],\n#             )\n#             # Extract result text\n#             result_text = \" \".join(\n#                 block.get(\"text\", \"\")\n#                 for block in result.content\n#                 if block.get(\"type\") == \"text\"\n#             )\n#             print(f\"Recording result: {result_text}\")\n#\n#\n#\n# Then we use ``retrieve_from_memory`` to retrieve related memories.\n#\n# .. code-block:: python\n#     :caption: Example of retrieving from ReMe long-term memory\n#\n#     async def test_retrieve_from_memory():\n#         \"\"\"Test retrieve_from_memory tool function interface\"\"\"\n#         async with reme_long_term_memory:\n#             # First record some content\n#             await reme_long_term_memory.record_to_memory(\n#                 thinking=\"User is sharing travel preferences\",\n#                 content=[\n#                     \"I prefer to stay in homestays when traveling to Hangzhou\",\n#                 ],\n#             )\n#\n#             # Then retrieve\n#             result = await reme_long_term_memory.retrieve_from_memory(\n#                 keywords=[\"Hangzhou travel\", \"tea preference\"],\n#             )\n#             retrieved_text = \" \".join(\n#                 block.get(\"text\", \"\")\n#                 for block in result.content\n#                 if block.get(\"type\") == \"text\"\n#             )\n#             print(f\"Retrieved memories: {retrieved_text}\")\n#\n#\n# Besides the tool function interface, we can also use the ``record`` method to directly record message conversations.\n#\n# .. code-block:: python\n#     :caption: Example of direct recording to ReMe long-term memory\n#\n#     async def test_record_direct():\n#         \"\"\"Test record direct recording method\"\"\"\n#         async with reme_long_term_memory:\n#             await reme_long_term_memory.record(\n#                 msgs=[\n#                     Msg(\n#                         role=\"user\",\n#                         content=\"I work as a software engineer and prefer remote work\",\n#                         name=\"user\",\n#                     ),\n#                     Msg(\n#                         role=\"assistant\",\n#                         content=\"Understood! You're a software engineer who values remote work flexibility.\",\n#                         name=\"assistant\",\n#                     ),\n#                     Msg(\n#                         role=\"user\",\n#                         content=\"I usually start my day at 9 AM with a cup of coffee\",\n#                         name=\"user\",\n#                     ),\n#                 ],\n#             )\n#             print(\"Successfully recorded conversation messages\")\n#\n#\n# Similarly, we use the ``retrieve`` method to retrieve related memories.\n#\n# .. code-block:: python\n#     :caption: Example of direct retrieval from ReMe long-term memory\n#\n#     async def test_retrieve_direct():\n#         \"\"\"Test retrieve direct retrieval method\"\"\"\n#         async with reme_long_term_memory:\n#             # First record some content\n#             await reme_long_term_memory.record(\n#                 msgs=[\n#                     Msg(\n#                         role=\"user\",\n#                         content=\"I work as a software engineer and prefer remote work\",\n#                         name=\"user\",\n#                     ),\n#                 ],\n#             )\n#\n#             # Then retrieve\n#             memories = await reme_long_term_memory.retrieve(\n#                 msg=Msg(\n#                     role=\"user\",\n#                     content=\"What do you know about my work preferences?\",\n#                     name=\"user\",\n#                 ),\n#             )\n#             print(\n#                 f\"Retrieved memories: {memories if memories else 'No memories found'}\",\n#             )\n#\n#\n# Integration with ReAct Agent\n# ----------------------------------------\n# In AgentScope, the ``ReActAgent`` class receives a ``long_term_memory``\n# parameter in its constructor, as well as a ``long_term_memory_mode`` parameter.\n#\n# If ``long_term_memory_mode`` is set to ``agent_control`` or ``both``,\n# ``record_to_memory`` and ``retrieve_from_memory`` tool functions will be\n# registered, allowing the agent to autonomously manage long-term memory through tool calls.\n#\n# .. note:: To achieve the best results, the ``\"agent_control\"`` mode may require\n#  additional instructions in the system prompt.\n#\n# .. code-block:: python\n#     :caption: Example of ReAct agent with ReMe long-term memory\n#\n#     # Create ReAct agent with long-term memory (agent_control mode)\n#     async def test_react_agent_with_reme():\n#         \"\"\"Test ReActAgent integration with ReMe personal memory\"\"\"\n#         async with reme_long_term_memory:\n#             agent_with_reme = ReActAgent(\n#                 name=\"Friday\",\n#                 sys_prompt=(\n#                     \"You are a helpful assistant named Friday with long-term memory capabilities. \"\n#                     \"\\n\\n## Memory Management Guidelines:\\n\"\n#                     \"1. **Recording Memories**: When users share personal information, preferences, \"\n#                     \"habits, or facts about themselves, ALWAYS record them using `record_to_memory` \"\n#                     \"for future reference.\\n\"\n#                     \"\\n2. **Retrieving Memories**: BEFORE answering questions about the user's preferences, \"\n#                     \"past information, or personal details, you MUST FIRST call `retrieve_from_memory` \"\n#                     \"to check if you have any relevant stored information. Do NOT rely solely on the \"\n#                     \"current conversation context.\\n\"\n#                     \"\\n3. **When to Retrieve**: Call `retrieve_from_memory` when:\\n\"\n#                     \"   - User asks questions like 'what do I like?', 'what are my preferences?', \"\n#                     \"'what do you know about me?'\\n\"\n#                     \"   - User asks about their past behaviors, habits, or preferences\\n\"\n#                     \"   - User refers to information they mentioned before\\n\"\n#                     \"   - You need context about the user to provide personalized responses\\n\"\n#                     \"\\nAlways check your memory first before claiming you don't know something about the user.\"\n#                 ),\n#                 model=DashScopeChatModel(\n#                     model_name=\"qwen3-max\",\n#                     api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n#                     stream=False,\n#                 ),\n#                 formatter=DashScopeChatFormatter(),\n#                 toolkit=Toolkit(),\n#                 memory=InMemoryMemory(),\n#                 long_term_memory=reme_long_term_memory,\n#                 long_term_memory_mode=\"agent_control\",  # Use agent_control mode\n#             )\n#\n#             # User shares preferences\n#             msg = Msg(\n#                 role=\"user\",\n#                 content=\"When I travel to Hangzhou, I prefer to stay in a homestay\",\n#                 name=\"user\",\n#             )\n#             response = await agent_with_reme(msg)\n#             print(f\"Agent response: {response.get_text_content()}\")\n#\n#             # Clear short-term memory to test long-term memory\n#             await agent_with_reme.memory.clear()\n#\n#             # Query preferences\n#             msg2 = Msg(\n#                 role=\"user\",\n#                 content=\"what preference do I have?\",\n#                 name=\"user\",\n#             )\n#             response2 = await agent_with_reme(msg2)\n#             print(f\"Agent response: {response2.get_text_content()}\")\n#\n#\n# Then we clear the short-term memory and ask the agent about the user's preferences.\n#\n# .. code-block:: python\n#     :caption: Example of retrieving preferences with ReAct agent and ReMe long-term memory\n#\n#     async def retrieve_reme_preferences():\n#         \"\"\"Retrieve user preferences from long-term memory\"\"\"\n#         async with reme_long_term_memory:\n#             # Create agent (reusing for demonstration completeness)\n#             agent_with_reme = ReActAgent(\n#                 name=\"Friday\",\n#                 sys_prompt=\"You are an assistant with long-term memory capabilities.\",\n#                 model=DashScopeChatModel(\n#                     api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n#                     model_name=\"qwen3-max\",\n#                     stream=False,\n#                 ),\n#                 formatter=DashScopeChatFormatter(),\n#                 toolkit=Toolkit(),\n#                 memory=InMemoryMemory(),\n#                 long_term_memory=reme_long_term_memory,\n#                 long_term_memory_mode=\"agent_control\",\n#             )\n#\n#             # Clear short-term memory\n#             await agent_with_reme.memory.clear()\n#             # The agent will remember previous conversations\n#             msg2 = Msg(\"user\", \"What are my preferences? Answer briefly.\", \"user\")\n#             await agent_with_reme(msg2)\n#\n# Customizing Long-Term Memory\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope provides the ``LongTermMemoryBase`` base class, which defines the basic\n#\n# Developers can inherit from ``LongTermMemoryBase`` to implement custom long-term\n# memory systems according to their needs：\n#\n# .. list-table:: Long-term memory classes in AgentScope\n#     :header-rows: 1\n#\n#     * - Class\n#       - Abstract Methods\n#       - Description\n#     * - ``LongTermMemoryBase``\n#       - | ``record``\n#         | ``retrieve``\n#         | ``record_to_memory``\n#         | ``retrieve_from_memory``\n#       - - For ``\"static_control\"`` mode, you must implement the ``record`` and ``retrieve`` methods.\n#         - For ``\"agent_control\"`` mode, the ``record_to_memory`` and ``retrieve_from_memory`` methods must be implemented.\n#     * - ``Mem0LongTermMemory``\n#       - | ``record``\n#         | ``retrieve``\n#         | ``record_to_memory``\n#         | ``retrieve_from_memory``\n#       - Long-term memory implementation based on the mem0 library, supporting vector storage and retrieval.\n#     * - ``ReMePersonalLongTermMemory``\n#       - | ``record``\n#         | ``retrieve``\n#         | ``record_to_memory``\n#         | ``retrieve_from_memory``\n#       - Personal memory implementation based on the ReMe framework, providing powerful memory management and retrieval capabilities.\n#\n#\n#\n#\n# Further Reading\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# - :ref:`memory` - Basic memory system\n# - :ref:`agent` - ReAct agent\n# - :ref:`tool` - Tool system\n"
  },
  {
    "path": "docs/tutorial/en/src/task_mcp.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _mcp:\n\nMCP\n=========================\n\nThe tutorial covers the following features of AgentScope in support of the MCP (Model Context Protocol):\n\n- Support both **HTTP** (streamable HTTP and SSE) and **StdIO** MCP servers\n- Provide both **stateful** and **stateless** MCP clients\n- Provide both **server-level** and **function-level** MCP tool management\n\nHere the stateful/stateless distinction refers to whether the client maintains a persistent session with the MCP server or not.\nThe table below summarizes the supported MCP client types and protocols:\n\n.. list-table:: Supported MCP client types and protocols\n    :header-rows: 1\n\n    * - Client Type\n      - HTTP (Streamable HTTP and SSE)\n      - StdIO\n    * - Stateful Client\n      - ``HttpStatefulClient``\n      - ``HttpStatelessClient``\n    * - Stateless Client\n      - ``StdIOStatefulClient``\n      -\n\n\"\"\"\nimport asyncio\nimport json\nimport os\n\nfrom agentscope.mcp import HttpStatefulClient, HttpStatelessClient\nfrom agentscope.tool import Toolkit\n\n# %%\n# MCP Client\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# In AgentScope, MCP clients are responsible for\n#\n# - connecting to the MCP server,\n# - obtaining tool functions from the server, and\n# - calling tool functions in the MCP server.\n#\n# There are two types of MCP clients in AgentScope: **Stateful** and **Stateless**.\n# They only differ in how to manage the session with the MCP server.\n#\n# - Stateful Client: The stateful MCP client **maintains a persistent session** with the MCP server within its lifetime. The developers should explicitly call ``connect()`` and ``close()`` methods to manage the connection lifecycle.\n# - Stateless Client: The stateless MCP client creates a new session when calling the tool function, and destroys the session right after the tool function call, which is much more lightweight.\n#\n# .. note:: - The StdIO MCP server only has stateful client, when ``connect()`` is called, it will start the MCP server locally and then connect to it.\n#  - For stateful clients, developers must ensure the client is connected when calling the tool functions.\n#  - When multiple `HttpStatefulClients` or `StdIOStatefulClients` are connected, they should be closed in Last In First Out (LIFO) order to prevent errors.\n#\n# Taking Gaode map MCP server as an example, the creation of stateful and stateless clients are very similar:\n#\n\nstateful_client = HttpStatefulClient(\n    # The name to identify the MCP\n    name=\"mcp_services_stateful\",\n    transport=\"streamable_http\",\n    url=f\"https://mcp.amap.com/mcp?key={os.environ['GAODE_API_KEY']}\",\n)\n\nstateless_client = HttpStatelessClient(\n    # The name to identify the MCP\n    name=\"mcp_services_stateless\",\n    transport=\"streamable_http\",\n    url=f\"https://mcp.amap.com/mcp?key={os.environ['GAODE_API_KEY']}\",\n)\n\n# %%\n# Both stateful and stateless clients provide the following methods:\n#\n# .. list-table:: MCP Client Methods\n#    :header-rows: 1\n#\n#    * - Method\n#      - Description\n#    * - ``list_tools``\n#      - List all tools available in the MCP server.\n#    * - ``get_callable_function``\n#      - Get a callable function object from the MCP server by its name.\n#\n# MCP as Tool\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope provides fine-grained management of MCP tools, including both server-level and function-level management.\n#\n# Server-Level Management\n# --------------------------------\n# You can register all tools from an MCP server into ``Toolkit`` as follows.\n#\n# .. tip:: Optionally, you can specify a group name to organize the tools. Refer to :ref:`tool` section for group-wise tools management.\n#\n\ntoolkit = Toolkit()\n\n\nasync def example_register_stateless_mcp() -> None:\n    \"\"\"Example of registering MCP tools from a stateless client.\"\"\"\n    # Register all tools from the MCP server\n    await toolkit.register_mcp_client(\n        stateless_client,\n        # group_name=\"map_services\",  # Optional group name\n    )\n\n    print(\n        \"Total number of MCP tools registered:\",\n        len(toolkit.get_json_schemas()),\n    )\n\n    maps_geo = next(\n        tool\n        for tool in toolkit.get_json_schemas()\n        if tool[\"function\"][\"name\"] == \"maps_geo\"\n    )\n    print(\"\\nThe example ``maps_geo`` function:\")\n    print(\n        json.dumps(\n            maps_geo,\n            indent=4,\n            ensure_ascii=False,\n        ),\n    )\n\n\nasyncio.run(example_register_stateless_mcp())\n\n# %%\n# To remove the registered tools, you can use the ``remove_tool_function`` to remove a specific tool function, or ``remove_mcp_clients`` to remove all tools from a specific MCP.\n#\n\n\nasync def example_remove_mcp_tools() -> None:\n    \"\"\"Example of removing MCP tools.\"\"\"\n    print(\n        \"Total number of tools before removal: \",\n        len(toolkit.get_json_schemas()),\n    )\n\n    # Remove a specific tool function by its name\n    toolkit.remove_tool_function(\"maps_geo\")\n    print(\"Number of tools: \", len(toolkit.get_json_schemas()))\n\n    # Remove all tools from the MCP client by its name\n    await toolkit.remove_mcp_clients(client_names=[\"mcp_services_stateless\"])\n    print(\"Number of tools: \", len(toolkit.get_json_schemas()))\n\n\nasyncio.run(example_remove_mcp_tools())\n\n# %%\n# Function-Level Management\n# --------------------------------\n# We notice the demand for more fine-grained control over MCP tools, such as post-processing the tool results, or use them to create a more complex tool function.\n#\n# Therefore, AgentScope supports to obtain the callable function object from MCP by its name, so that you can\n#\n# - call it directly,\n# - wrap it into your own function, or anyway you like.\n#\n# Additionally, you can specify whether to wrap the tool result into ``ToolResponse`` object in AgentScope, so that you can use it seamlessly with the ``Toolkit``.\n# If you set ``wrap_tool_result=False``, the raw result type ``mcp.types.CallToolResult`` will be returned.\n#\n# Taking the ``maps_geo`` function as an example, you can obtain it as a callable function object as follows:\n#\n\n\nasync def example_function_level_usage() -> None:\n    \"\"\"Example of using function-level MCP tool.\"\"\"\n    func_obj = await stateless_client.get_callable_function(\n        func_name=\"maps_geo\",\n        # Whether to wrap the tool result into ToolResponse in AgentScope\n        wrap_tool_result=True,\n    )\n\n    # You can obtain its name, description and json schema\n    print(\"Function name:\", func_obj.name)\n    print(\"Function description:\", func_obj.description)\n    print(\n        \"Function JSON schema:\",\n        json.dumps(func_obj.json_schema, indent=4, ensure_ascii=False),\n    )\n\n    # Call the function object directly\n    res = await func_obj(\n        address=\"Tiananmen Square\",\n        city=\"Beijing\",\n    )\n    print(\"\\nFunction call result:\")\n    print(res)\n\n\nasyncio.run(example_function_level_usage())\n\n# %%\n# Further Reading\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# For more details, see:\n#\n# - :ref:`tool`\n# - :ref:`agent`\n#\n"
  },
  {
    "path": "docs/tutorial/en/src/task_memory.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _memory:\n\nMemory\n========================\n\nThe memory module in AgentScope is responsible for\n\n- storing the messages and\n- managing them with specific marks\nin different storage implementations.\n\nThe **mark** is a string label associated with each message in the memory,\nwhich can be used to categorize, filter, and retrieve messages based on their\ncontext or purpose.\n\nIt's powerful for high-level memory management in agents. For example,\nIn `ReActAgent` class, the hint messages are stored with the\nmark \"hint\", and the memory compression functionality is also implemented\nbased on marks.\n\n.. note:: The memory module only provides storage and management\n functionalities. The algorithm logic such as compression is implemented in\n the agent level.\n\nCurrently, AgentScope provides the following memory storage implementations:\n\n.. list-table:: The built-in memory storage implementations in AgentScope\n    :header-rows: 1\n\n    * - Memory Class\n      - Description\n    * - ``InMemoryMemory``\n      - A simple in-memory implementation of memory storage.\n    * - ``AsyncSQLAlchemyMemory``\n      - An asynchronous SQLAlchemy-based implementation of memory storage, which supports various databases such as SQLite, PostgreSQL, MySQL, etc.\n    * - ``RedisMemory``\n      - A Redis-based implementation of memory storage.\n\n.. tip:: If you're interested in contributing new memory storage implementations, please refer to the\n `Contribution Guide <https://github.com/agentscope-ai/agentscope/blob/main/CONTRIBUTING.md#types-of-contributions>`_.\n\nAll the above memory classes inherit from the base class ``MemoryBase``, and\nprovide the following methods to manage the messages in the memory:\n\n.. list-table:: The methods provided by the memory classes\n    :header-rows: 1\n\n    * - Method\n      - Description\n    * - ``add(\n            memories: Msg | list[Msg] | None,\n            marks: str | list[str] | None = None,\n        ) -> None``\n      - Add ``Msg`` object(s) to the memory storage with the given mark(s) (if provided).\n    * - ``delete(msg_ids: list[str]) -> int``\n      - Delete messages from the memory storage by their IDs.\n    * - ``delete_by_mark(mark: str | list[str]) -> int``\n      - Delete messages from the memory by their marks.\n    * - ``size() -> int``\n        - Get the size of the memory storage.\n    * - ``clear() -> None``\n      - Clear the memory storage.\n    * - ``get_memory(\n            mark: str | None = None,\n            exclude_mark: str | None = None,\n        ) -> list[Msg]``\n      - Get the messages from the memory by mark (if provided). Otherwise, get all messages. If the ``update_compressed_summary`` method is used to store a compressed summary, it will be attached to the head of the returned messages.\n    * - ``update_messages_mark(\n            new_mark: str | None,\n            old_mark: str | None = None,\n            msg_ids: list[str] | None = None,\n        ) -> int``\n      - A unified method to update marks of messages in the storage (add, remove, or change marks).\n    * - ``update_compressed_summary(\n            summary: str,\n        ) -> None``\n      - Update the summary attribute stored in the memory.\n\"\"\"\nimport asyncio\nimport json\n\nimport fakeredis\nfrom sqlalchemy.ext.asyncio import create_async_engine\n\nfrom agentscope.memory import (\n    InMemoryMemory,\n    AsyncSQLAlchemyMemory,\n    RedisMemory,\n)\nfrom agentscope.message import Msg\n\n\n# %%\n# In-Memory Memory\n# ~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# The in-memory memory provides a simple way to store messages in memory.\n# Together with the :ref:`state` module, it can persist the memory content across\n# different users and sessions.\n\n\nasync def in_memory_example():\n    \"\"\"An example of using InMemoryMemory to store messages in memory.\"\"\"\n    memory = InMemoryMemory()\n    await memory.add(\n        Msg(\"Alice\", \"Generate a report about AgentScope\", \"user\"),\n    )\n\n    # Add a hint message with the mark \"hint\"\n    await memory.add(\n        [\n            Msg(\n                \"system\",\n                \"<system-hint>Create a plan first to collect information and \"\n                \"generate the report step by step.</system-hint>\",\n                \"system\",\n            ),\n        ],\n        marks=\"hint\",\n    )\n\n    msgs = await memory.get_memory(mark=\"hint\")\n    print(\"The messages with mark 'hint':\")\n    for msg in msgs:\n        print(f\"- {msg}\")\n\n    # All the stored messages can be exported and loaded via ``state_dict`` and ``load_state_dict`` methods.\n    state = memory.state_dict()\n    print(\"The state dict of the memory:\")\n    print(json.dumps(state, indent=2))\n\n    # delete messages by mark\n    deleted_count = await memory.delete_by_mark(\"hint\")\n    print(f\"Deleted {deleted_count} messages with mark 'hint'.\")\n\n    print(\"The state dict of the memory after deletion:\")\n    state = memory.state_dict()\n    print(json.dumps(state, indent=2))\n\n\nasyncio.run(in_memory_example())\n\n# %%\n# Relational Database Memory\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope provides a unified interface to work with relational databases via SQLAlchemy, supporting\n#\n# - various databases such as SQLite, PostgreSQL, MySQL, etc.\n# - user and session management, and\n# - connection pooling in the production environment\n#\n# Specifically, here we use a memory backed by SQLite as an example.\n\n\nasync def sqlalchemy_example() -> None:\n    \"\"\"An example of using AsyncSQLAlchemyMemory to store messages in a SQLite database.\"\"\"\n\n    # Create an async SQLAlchemy engine first\n    engine = create_async_engine(\"sqlite+aiosqlite:///./test_memory.db\")\n\n    # Then create the memory with the engine\n    memory = AsyncSQLAlchemyMemory(\n        engine_or_session=engine,\n        # Optionally specify user_id and session_id\n        user_id=\"user_1\",\n        session_id=\"session_1\",\n    )\n\n    await memory.add(\n        Msg(\"Alice\", \"Generate a report about AgentScope\", \"user\"),\n    )\n\n    await memory.add(\n        [\n            Msg(\n                \"system\",\n                \"<system-hint>Create a plan first to collect information and \"\n                \"generate the report step by step.</system-hint>\",\n                \"system\",\n            ),\n        ],\n        marks=\"hint\",\n    )\n\n    msgs = await memory.get_memory(mark=\"hint\")\n    print(\"The messages with mark 'hint':\")\n    for msg in msgs:\n        print(f\"- {msg}\")\n\n    # Close the engine when done\n    await memory.close()\n\n\nasyncio.run(sqlalchemy_example())\n\n# %%\n# Optionally, you can also use the ``AsyncSQLAlchemyMemory`` as an async context manager, and the session will be closed automatically when exiting the context.\n\n\nasync def sqlalchemy_context_example() -> None:\n    \"\"\"Example of using AsyncSQLAlchemyMemory as an async context manager.\"\"\"\n    engine = create_async_engine(\"sqlite+aiosqlite:///./test_memory.db\")\n    async with AsyncSQLAlchemyMemory(\n        engine_or_session=engine,\n        user_id=\"user_1\",\n        session_id=\"session_1\",\n    ) as memory:\n        await memory.add(\n            Msg(\"Alice\", \"Generate a report about AgentScope\", \"user\"),\n        )\n\n        msgs = await memory.get_memory()\n        print(\"All messages in the memory:\")\n        for msg in msgs:\n            print(f\"- {msg}\")\n\n\nasyncio.run(sqlalchemy_context_example())\n\n# %%\n# In production environment e.g. with FastAPI, the connection pooling can be enabled as follows:\n#\n# .. code-block:: python\n#    :caption: SQLAlchemy Memory with Connection Pooling in FastAPI\n#\n#    from typing import AsyncGenerator\n#\n#     from fastapi import FastAPI, Depends\n#     from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession\n#\n#     from agentscope.agent import ReActAgent\n#     from agentscope.pipeline import stream_printing_messages\n#\n#\n#     app = FastAPI()\n#\n#     # Create an async SQLAlchemy engine with connection pooling\n#     engine = create_async_engine(\n#         \"sqlite+aiosqlite:///./test_memory.db\",\n#         pool_size=10,\n#         max_overflow=20,\n#         pool_timeout=30,\n#         # ...  The other pool settings\n#     )\n#\n#     # Create a session maker\n#     async_session_marker = async_sessionmaker(\n#         engine,\n#         expire_on_commit=False,\n#         autocommit=False,\n#         autoflush=False,\n#     )\n#\n#     async def get_db() -> AsyncGenerator[AsyncSession, None]:\n#         async with async_session_marker() as session:\n#             try:\n#                 yield session\n#                 await session.commit()\n#             except Exception:\n#                 await session.rollback()\n#                 raise\n#             finally:\n#                 await session.close()\n#\n#     @app.post(\"/chat\")\n#     async def chat_endpoint(\n#         user_id: str,\n#         session_id: str,\n#         input: str,\n#         db_session: AsyncSession = Depends(get_db),\n#     ):\n#         # Some setup for the agent\n#         ...\n#\n#         # Create the agent with the SQLAlchemy memory\n#         agent = ReActAgent(\n#             # ...\n#             memory=AsyncSQLAlchemyMemory(\n#                 engine_or_session=db_session,\n#                 user_id=user_id,\n#                 session_id=session_id,\n#             ),\n#         )\n#\n#         # Handle the chat with the agent\n#         async for msg, _ in stream_printing_messages(\n#             agents=[agent],\n#             coroutine_task=agent(Msg(\"user\", input, \"user\")),\n#         ):\n#             # yield msg to the client\n#             ...\n#\n#\n# NoSQL Database Memory\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope also provides memory implementations based on NoSQL databases such as Redis.\n# It also supports user and session management, and connection pooling in the production environment.\n#\n# First, we can initialize the Redis memory as follows:\n\n\nasync def redis_memory_example() -> None:\n    \"\"\"An example of using RedisMemory to store messages in Redis.\"\"\"\n    # Use fakeredis for in-memory testing without a real Redis server\n    fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True)\n    # Create the Redis memory\n    memory = RedisMemory(\n        # Using fake redis for demonstration\n        connection_pool=fake_redis.connection_pool,\n        # You can also connect to a real Redis server by specifying host and port\n        # host=\"localhost\",\n        # port=6379,\n        # Optionally specify user_id and session_id\n        user_id=\"user_1\",\n        session_id=\"session_1\",\n    )\n\n    # Add a message to the memory\n    await memory.add(\n        Msg(\n            \"Alice\",\n            \"Generate a report about AgentScope\",\n            \"user\",\n        ),\n    )\n\n    # Add a hint message with the mark \"hint\"\n    await memory.add(\n        Msg(\n            \"system\",\n            \"<system-hint>Create a plan first to collect information and \"\n            \"generate the report step by step.</system-hint>\",\n            \"system\",\n        ),\n        marks=\"hint\",\n    )\n\n    # Retrieve messages with the mark \"hint\"\n    msgs = await memory.get_memory(mark=\"hint\")\n    print(\"The messages with mark 'hint':\")\n    for msg in msgs:\n        print(f\"- {msg}\")\n\n\nasyncio.run(redis_memory_example())\n\n# %%\n# Similarly, the `RedisMemory` can also be used with connection pooling in the production environment, e.g., with FastAPI.\n#\n# .. code-block:: python\n#    :caption: Redis Memory with Connection Pooling in FastAPI\n#\n#     from fastapi import FastAPI, HTTPException\n#     from redis.asyncio import ConnectionPool\n#     from contextlib import asynccontextmanager\n#\n#     # Global Redis connection pool\n#     redis_pool: ConnectionPool | None = None\n#\n#\n#     # Use the lifespan event to manage the Redis connection pool\n#     @asynccontextmanager\n#     async def lifespan(app: FastAPI):\n#         global redis_pool\n#         redis_pool = ConnectionPool(\n#             host=\"localhost\",\n#             port=6379,\n#             db=0,\n#             password=None,\n#             decode_responses=True,\n#             max_connections=10,\n#             encoding=\"utf-8\",\n#         )\n#         print(\"✅ Redis connection established\")\n#\n#         yield\n#\n#         await redis_pool.disconnect()\n#         print(\"✅ Redis connection closed\")\n#\n#\n#     app = FastAPI(lifespan=lifespan)\n#\n#\n#     @app.post(\"/chat_endpoint\")\n#     async def chat_endpoint(\n#         user_id: str, session_id: str, input: str\n#     ):  # ✅ 直接使用BaseModel\n#         \"\"\"A chat endpoint\"\"\"\n#         global redis_pool\n#         if redis_pool is None:\n#             raise HTTPException(\n#                 status_code=500,\n#                 detail=\"Redis connection pool is not initialized.\",\n#             )\n#\n#         # Create the Redis memory\n#         memory = RedisMemory(\n#             connection_pool=redis_pool,\n#             user_id=user_id,\n#             session_id=session_id,\n#         )\n#\n#         ...\n#\n#         # Close the Redis client connection when done\n#         client = memory.get_client()\n#         await client.aclose()\n#\n#\n#\n# Customizing Memory\n# ~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# To customize your own memory, just inherit from ``MemoryBase`` and implement the following methods:\n#\n# .. list-table::\n#     :header-rows: 1\n#\n#     * - Method\n#       - Description\n#     * - ``add``\n#       - Add ``Msg`` objects to the memory\n#     * - ``delete``\n#       - Delete ``Msg`` objects from the memory\n#     * - ``delete_by_mark``\n#       - Delete ``Msg`` objects from the memory by their marks\n#     * - ``size``\n#       - The size of the memory\n#     * - ``clear``\n#       - Clear the memory content\n#     * - ``get_memory``\n#       - Get the memory content as a list of ``Msg`` objects\n#     * - ``update_messages_mark``\n#       - Update marks of messages in the memory\n#     * - ``state_dict``\n#       - Get the state dictionary of the memory\n#     * - ``load_state_dict``\n#       - Load the state dictionary of the memory\n#\n# Further Reading\n# ~~~~~~~~~~~~~~~~~~~~~~~~\n# - :ref:`agent`\n# - :ref:`long-term-memory`\n"
  },
  {
    "path": "docs/tutorial/en/src/task_middleware.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _middleware:\n\nMiddleware\n===========================\n\nAgentScope provides a flexible middleware system that allows developers to intercept and modify the execution of various operations.\nCurrently, middleware support is available for **tool execution** in the ``Toolkit`` class.\n\nThe middleware system follows an **onion model**, where each middleware wraps around the previous one, forming layers.\nThis allows developers to:\n\n- Perform **pre-processing** before the operation\n- **Intercept and modify** responses during execution\n- Perform **post-processing** after the operation completes\n- **Skip** the operation execution entirely based on conditions\n\n.. tip:: Future versions of AgentScope will expand middleware support to other components such as agents and models.\n\n\"\"\"\nimport asyncio\nfrom typing import AsyncGenerator, Callable\n\nfrom agentscope.message import TextBlock, ToolUseBlock\nfrom agentscope.tool import ToolResponse, Toolkit\n\n\n# %%\n# Tool Execution Middleware\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# The ``Toolkit`` class supports middleware for tool execution via the ``register_middleware`` method.\n# Each middleware can intercept the tool call and modify the input or output.\n#\n# Middleware Signature\n# ------------------------------\n#\n# A middleware function should have the following signature:\n#\n# .. code-block:: python\n#\n#     async def middleware(\n#         kwargs: dict,\n#         next_handler: Callable,\n#     ) -> AsyncGenerator[ToolResponse, None]:\n#         # Access parameters from kwargs\n#         tool_call = kwargs[\"tool_call\"]\n#\n#         # Pre-processing\n#         # ...\n#\n#         # Call the next middleware or tool function\n#         async for response in await next_handler(**kwargs):\n#             # Post-processing\n#             yield response\n#\n# .. list-table:: Middleware Parameters\n#    :header-rows: 1\n#\n#    * - Parameter\n#      - Type\n#      - Description\n#    * - ``kwargs``\n#      - ``dict``\n#      - Context parameters. Currently, includes ``tool_call`` (ToolUseBlock). May include additional parameters in future versions.\n#    * - ``next_handler``\n#      - ``Callable``\n#      - A callable that accepts kwargs dict and returns a coroutine yielding AsyncGenerator of ToolResponse objects\n#    * - **Returns**\n#      - ``AsyncGenerator[ToolResponse, None]``\n#      - An async generator that yields ToolResponse objects\n#\n# Basic Example\n# ------------------------------\n#\n# Here is a simple middleware that logs tool calls:\n#\n\n\nasync def logging_middleware(\n    kwargs: dict,\n    next_handler: Callable,\n) -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"A middleware that logs tool execution.\"\"\"\n    # Access the tool call from kwargs\n    tool_call = kwargs[\"tool_call\"]\n\n    # Pre-processing: log before tool execution\n    print(f\"[Middleware] Calling tool: {tool_call['name']}\")\n    print(f\"[Middleware] Input: {tool_call['input']}\")\n\n    # Call the next handler (either another middleware or the actual tool)\n    async for response in await next_handler(**kwargs):\n        # Post-processing: log the response\n        print(f\"[Middleware] Response: {response.content[0]['text']}\")\n        yield response\n\n    # This will execute after all responses are yielded\n    print(f\"[Middleware] Tool {tool_call['name']} completed\")\n\n\n# %%\n# Let's register this middleware with a toolkit and test it:\n#\n\n\nasync def search_tool(query: str) -> ToolResponse:\n    \"\"\"A simple search tool.\n\n    Args:\n        query (`str`):\n            The search query.\n\n    Returns:\n        `ToolResponse`:\n            The search result.\n    \"\"\"\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=f\"Search results for '{query}'\",\n            ),\n        ],\n    )\n\n\nasync def example_logging_middleware() -> None:\n    \"\"\"Example of using logging middleware.\"\"\"\n    # Create a toolkit and register the tool\n    toolkit = Toolkit()\n    toolkit.register_tool_function(search_tool)\n\n    # Register the middleware\n    toolkit.register_middleware(logging_middleware)\n\n    # Call the tool\n    result = await toolkit.call_tool_function(\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"1\",\n            name=\"search_tool\",\n            input={\"query\": \"AgentScope\"},\n        ),\n    )\n\n    async for response in result:\n        print(f\"\\n[Final] {response.content[0]['text']}\\n\")\n\n\nprint(\"=\" * 60)\nprint(\"Example 1: Logging Middleware\")\nprint(\"=\" * 60)\nasyncio.run(example_logging_middleware())\n\n# %%\n# Modifying Input and Output\n# ------------------------------\n#\n# Middleware can also modify the tool call input and the response content:\n#\n\n\nasync def transform_middleware(\n    kwargs: dict,\n    next_handler: Callable,\n) -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"A middleware that transforms input and output.\"\"\"\n    # Access the tool call from kwargs\n    tool_call = kwargs[\"tool_call\"]\n\n    # Pre-processing: modify the input\n    original_query = tool_call[\"input\"][\"query\"]\n    tool_call[\"input\"][\"query\"] = f\"[TRANSFORMED] {original_query}\"\n\n    async for response in await next_handler(**kwargs):\n        # Post-processing: modify the response\n        original_text = response.content[0][\"text\"]\n        response.content[0][\"text\"] = f\"{original_text} [MODIFIED]\"\n        yield response\n\n\nasync def example_transform_middleware() -> None:\n    \"\"\"Example of transforming middleware.\"\"\"\n    toolkit = Toolkit()\n    toolkit.register_tool_function(search_tool)\n    toolkit.register_middleware(transform_middleware)\n\n    result = await toolkit.call_tool_function(\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"2\",\n            name=\"search_tool\",\n            input={\"query\": \"middleware\"},\n        ),\n    )\n\n    async for response in result:\n        print(f\"Result: {response.content[0]['text']}\")\n\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"Example 2: Transform Middleware\")\nprint(\"=\" * 60)\nasyncio.run(example_transform_middleware())\n\n# %%\n# Authorization Middleware\n# ------------------------------\n#\n# You can use middleware to implement authorization checks and skip tool execution if not authorized:\n#\n\n\nasync def authorization_middleware(\n    kwargs: dict,\n    next_handler: Callable,\n) -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"A middleware that checks authorization.\"\"\"\n    # Access the tool call from kwargs\n    tool_call = kwargs[\"tool_call\"]\n\n    # Check if the tool is authorized (simple example)\n    authorized_tools = {\"search_tool\"}\n\n    if tool_call[\"name\"] not in authorized_tools:\n        # Skip execution and return error directly\n        print(f\"[Auth] Tool {tool_call['name']} is not authorized\")\n        yield ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=f\"Error: Tool '{tool_call['name']}' is not authorized\",  # noqa: E501\n                ),\n            ],\n        )\n        return\n\n    # Tool is authorized, proceed\n    print(f\"[Auth] Tool {tool_call['name']} is authorized\")\n    async for response in await next_handler(**kwargs):\n        yield response\n\n\nasync def unauthorized_tool(data: str) -> ToolResponse:\n    \"\"\"An unauthorized tool.\n\n    Args:\n        data (`str`):\n            Some data.\n\n    Returns:\n        `ToolResponse`:\n            The result.\n    \"\"\"\n    return ToolResponse(\n        content=[TextBlock(type=\"text\", text=f\"Processing {data}\")],\n    )\n\n\nasync def example_authorization_middleware() -> None:\n    \"\"\"Example of authorization middleware.\"\"\"\n    toolkit = Toolkit()\n    toolkit.register_tool_function(search_tool)\n    toolkit.register_tool_function(unauthorized_tool)\n    toolkit.register_middleware(authorization_middleware)\n\n    # Try authorized tool\n    print(\"\\nCalling authorized tool:\")\n    result = await toolkit.call_tool_function(\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"3\",\n            name=\"search_tool\",\n            input={\"query\": \"test\"},\n        ),\n    )\n    async for response in result:\n        print(f\"Result: {response.content[0]['text']}\")\n\n    # Try unauthorized tool\n    print(\"\\nCalling unauthorized tool:\")\n    result = await toolkit.call_tool_function(\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"4\",\n            name=\"unauthorized_tool\",\n            input={\"data\": \"test\"},\n        ),\n    )\n    async for response in result:\n        print(f\"Result: {response.content[0]['text']}\")\n\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"Example 3: Authorization Middleware\")\nprint(\"=\" * 60)\nasyncio.run(example_authorization_middleware())\n\n# %%\n# Multiple Middleware (Onion Model)\n# ------------------------------\n#\n# When multiple middleware are registered, they form an onion-like structure.\n# The execution order follows the onion model:\n#\n# - **Pre-processing**: Executes in the order middleware are registered\n# - **Post-processing**: Executes in reverse order (inner to outer)\n#\n# This is because the actual tool response object is passed through the middleware chain,\n# and each middleware modifies it in place.\n#\n\n\nasync def middleware_1(\n    kwargs: dict,\n    next_handler: Callable,\n) -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"First middleware.\"\"\"\n    # Access the tool call from kwargs\n    tool_call = kwargs[\"tool_call\"]\n\n    # Pre-processing\n    print(\"[M1] Pre-processing\")\n    tool_call[\"input\"][\"query\"] += \" [M1]\"\n\n    async for response in await next_handler(**kwargs):\n        # Post-processing\n        response.content[0][\"text\"] += \" [M1]\"\n        print(\"[M1] Post-processing\")\n        yield response\n\n\nasync def middleware_2(\n    kwargs: dict,\n    next_handler: Callable,\n) -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"Second middleware.\"\"\"\n    # Access the tool call from kwargs\n    tool_call = kwargs[\"tool_call\"]\n\n    # Pre-processing\n    print(\"[M2] Pre-processing\")\n    tool_call[\"input\"][\"query\"] += \" [M2]\"\n\n    async for response in await next_handler(**kwargs):\n        # Post-processing\n        response.content[0][\"text\"] += \" [M2]\"\n        print(\"[M2] Post-processing\")\n        yield response\n\n\nasync def example_multiple_middleware() -> None:\n    \"\"\"Example of multiple middleware.\"\"\"\n    toolkit = Toolkit()\n    toolkit.register_tool_function(search_tool)\n\n    # Register middleware in order\n    toolkit.register_middleware(middleware_1)\n    toolkit.register_middleware(middleware_2)\n\n    result = await toolkit.call_tool_function(\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"5\",\n            name=\"search_tool\",\n            input={\"query\": \"test\"},\n        ),\n    )\n\n    async for response in result:\n        print(f\"\\nFinal result: {response.content[0]['text']}\")\n\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"Example 4: Multiple Middleware (Onion Model)\")\nprint(\"=\" * 60)\nprint(\"\\nExecution flow:\")\nprint(\"M1 Pre → M2 Pre → Tool → M2 Post → M1 Post\")\nprint()\nasyncio.run(example_multiple_middleware())\n\n# %%\n# Use Cases\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# The middleware system is useful for various scenarios:\n#\n# - **Logging and Monitoring**: Track tool usage and performance\n# - **Authorization**: Control access to specific tools\n# - **Rate Limiting**: Limit the frequency of tool calls\n# - **Caching**: Cache tool responses for repeated calls\n# - **Error Handling**: Add retry logic or graceful degradation\n# - **Input Validation**: Validate and sanitize tool inputs\n# - **Output Transformation**: Format or filter tool outputs\n# - **Metrics Collection**: Collect statistics about tool usage\n#\n# .. note::\n#     - Middleware are applied in the order they are registered\n#     - The same ``ToolResponse`` object is passed through the middleware chain and modified in place\n#     - Middleware can completely skip tool execution by not calling ``next_handler``\n#     - All middleware must be async generator functions that yield ``ToolResponse`` objects\n"
  },
  {
    "path": "docs/tutorial/en/src/task_model.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _model:\n\nModel\n====================\n\nIn this tutorial, we introduce the model APIs integrated in AgentScope, how to use them and how to integrate new model APIs.\nThe supported model APIs and providers include:\n\n.. list-table::\n    :header-rows: 1\n\n    * - API\n      - Class\n      - Compatible\n      - Streaming\n      - Tools\n      - Vision\n      - Reasoning\n    * - OpenAI\n      - ``OpenAIChatModel``\n      - vLLM, DeepSeek\n      - ✅\n      - ✅\n      - ✅\n      - ✅\n    * - DashScope\n      - ``DashScopeChatModel``\n      -\n      - ✅\n      - ✅\n      - ✅\n      - ✅\n    * - Anthropic\n      - ``AnthropicChatModel``\n      -\n      - ✅\n      - ✅\n      - ✅\n      - ✅\n    * - Gemini\n      - ``GeminiChatModel``\n      -\n      - ✅\n      - ✅\n      - ✅\n      - ✅\n    * - Ollama\n      - ``OllamaChatModel``\n      -\n      - ✅\n      - ✅\n      - ✅\n      - ✅\n\n.. note:: When using vLLM, you need to configure the appropriate tool calling parameters for different models during deployment, such as ``--enable-auto-tool-choice``, ``--tool-call-parser``, etc. For more details, refer to the `official vLLM documentation <https://docs.vllm.ai/en/latest/features/tool_calling.html>`_.\n\n.. note:: For OpenAI-compatible models (e.g. vLLM, Deepseek), developers can use the ``OpenAIChatModel`` class, and specify the API endpoint by the ``client_kwargs`` parameter: ``client_kwargs={\"base_url\": \"http://your-api-endpoint\"}``. For example:\n\n    .. code-block:: python\n\n        OpenAIChatModel(client_kwargs={\"base_url\": \"http://localhost:8000/v1\"})\n\n.. note:: Model behavior parameters (such as temperature, maximum length, etc.) can be preset in the constructor function via the ``generate_kwargs`` parameter. For example:\n\n    .. code-block:: python\n\n        OpenAIChatModel(generate_kwargs={\"temperature\": 0.3, \"max_tokens\": 1000})\n\nTo provide unified model interfaces, the above model classes has the following common methods:\n\n- The first three arguments of the ``__call__`` method are ``messages`` , ``tools`` and ``tool_choice``, representing the input messages, JSON schema of tool functions, and tool selection mode, respectively.\n- The return type are either a ``ChatResponse`` instance or an async generator of ``ChatResponse`` in streaming mode.\n\n.. note:: Different model APIs differ in the input message format, refer to :ref:`prompt` for more details.\n\nThe ``ChatResponse`` instance contains the generated thinking/text/tool use content, identity, created time and usage information.\n\"\"\"\nimport asyncio\nimport json\nimport os\n\nfrom agentscope.message import TextBlock, ToolUseBlock, ThinkingBlock, Msg\nfrom agentscope.model import ChatResponse, DashScopeChatModel\n\nresponse = ChatResponse(\n    content=[\n        ThinkingBlock(\n            type=\"thinking\",\n            thinking=\"I should search for AgentScope on Google.\",\n        ),\n        TextBlock(type=\"text\", text=\"I'll search for AgentScope on Google.\"),\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"642n298gjna\",\n            name=\"google_search\",\n            input={\"query\": \"AgentScope?\"},\n        ),\n    ],\n)\n\nprint(response)\n\n# %%\n# Taking ``DashScopeChatModel`` as an example, we can use it to create a chat model instance and call it with messages and tools:\n\n\nasync def example_model_call() -> None:\n    \"\"\"An example of using the DashScopeChatModel.\"\"\"\n    model = DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        stream=False,\n    )\n\n    res = await model(\n        messages=[\n            {\"role\": \"user\", \"content\": \"Hi!\"},\n        ],\n    )\n\n    # You can directly create a ``Msg`` object with the response content\n    msg_res = Msg(\"Friday\", res.content, \"assistant\")\n\n    print(\"The response:\", res)\n    print(\"The response as Msg:\", msg_res)\n\n\nasyncio.run(example_model_call())\n\n# %%\n# Streaming\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# To enable streaming model, set the ``stream`` parameter in the model constructor to ``True``.\n# When streaming is enabled, the ``__call__`` method will return an **async generator** that yields ``ChatResponse`` instances as they are generated by the model.\n#\n# .. note:: The streaming mode in AgentScope is designed to be **cumulative**, meaning the content in each chunk contains all the previous content plus the newly generated content.\n#\n\n\nasync def example_streaming() -> None:\n    \"\"\"An example of using the streaming model.\"\"\"\n    model = DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        stream=True,\n    )\n\n    generator = await model(\n        messages=[\n            {\n                \"role\": \"user\",\n                \"content\": \"Count from 1 to 20, and just report the number without any other information.\",\n            },\n        ],\n    )\n    print(\"The type of the response:\", type(generator))\n\n    i = 0\n    async for chunk in generator:\n        print(f\"Chunk {i}\")\n        print(f\"\\ttype: {type(chunk.content)}\")\n        print(f\"\\t{chunk}\\n\")\n        i += 1\n\n\nasyncio.run(example_streaming())\n\n# %%\n# Reasoning\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope supports reasoning models by providing the ``ThinkingBlock``.\n#\n\n\nasync def example_reasoning() -> None:\n    \"\"\"An example of using the reasoning model.\"\"\"\n    model = DashScopeChatModel(\n        model_name=\"qwen-turbo\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        enable_thinking=True,\n    )\n\n    res = await model(\n        messages=[\n            {\"role\": \"user\", \"content\": \"Who am I?\"},\n        ],\n    )\n\n    last_chunk = None\n    async for chunk in res:\n        last_chunk = chunk\n    print(\"The final response:\")\n    print(last_chunk)\n\n\nasyncio.run(example_reasoning())\n\n# %%\n# Tools API\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# Different model providers differ in their tools APIs, e.g. the tools JSON schema, the tool call/response format.\n# To provide a unified interface, AgentScope solves the problem by:\n#\n# - Providing unified tool call block :ref:`ToolUseBlock <tool-block>` and tool response block :ref:`ToolResultBlock <tool-block>`, respectively.\n# - Providing a unified tools interface in the ``__call__`` method of the model classes, that accepts a list of tools JSON schemas as follows:\n#\n\njson_schemas = [\n    {\n        \"type\": \"function\",\n        \"function\": {\n            \"name\": \"google_search\",\n            \"description\": \"Search for a query on Google.\",\n            \"parameters\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"query\": {\n                        \"type\": \"string\",\n                        \"description\": \"The search query.\",\n                    },\n                },\n                \"required\": [\"query\"],\n            },\n        },\n    },\n]\n\n# %%\n# Further Reading\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# - :ref:`message`\n# - :ref:`prompt`\n#\n"
  },
  {
    "path": "docs/tutorial/en/src/task_pipeline.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _pipeline:\n\nPipeline\n========================\n\nFor multi-agent orchestration, AgentScope provides the ``agentscope.pipeline`` module\nas syntax sugar for chaining agents together, including\n\n- **MsgHub**: a message hub for broadcasting messages among multiple agents\n- **sequential_pipeline** and **SequentialPipeline**: a functional and class-based implementation that chains agents in a sequential manner\n- **fanout_pipeline** and **FanoutPipeline**: a functional and class-based implementation that distributes the same input to multiple agents\n- **stream_printing_messages**: a utility function that convert the printing messages from agent(s) into an async generator\n\n\"\"\"\n\nimport os, asyncio\n\nfrom agentscope.formatter import DashScopeMultiAgentFormatter\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.agent import ReActAgent\nfrom agentscope.pipeline import MsgHub, stream_printing_messages\n\n\n# %%\n# Broadcasting with MsgHub\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# The ``MsgHub`` class is an **async context manager**, receiving a list of agents as its participants.\n# When one participant generates a replying message, all other participants will receive this message by calling their ``observe`` method.\n#\n# That means within a ``MsgHub`` context, developers don't need to manually send a replying message from one agent to another.\n# The broadcasting is automatically handled.\n#\n# Here we create four agents: Alice, Bob, Charlie and David.\n# Then we start a meeting with Alice, Bob and Charlie by introducing themselves.\n# Note David is not included in this meeting.\n\n\ndef create_agent(name: str, age: int, career: str) -> ReActAgent:\n    \"\"\"Create agent object by the given information.\"\"\"\n    return ReActAgent(\n        name=name,\n        sys_prompt=f\"You're {name}, a {age}-year-old {career}\",\n        model=DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        ),\n        formatter=DashScopeMultiAgentFormatter(),\n    )\n\n\nalice = create_agent(\"Alice\", 50, \"teacher\")\nbob = create_agent(\"Bob\", 35, \"engineer\")\ncharlie = create_agent(\"Charlie\", 28, \"designer\")\ndavid = create_agent(\"David\", 30, \"developer\")\n\n# %%\n# Then we start a meeting and let them introduce themselves without manual message passing:\n#\n# .. hint:: The message in ``announcement`` will be broadcasted to all participants when entering the ``MsgHub`` context.\n#\n\n\nasync def example_broadcast_message():\n    \"\"\"Example of broadcasting messages with MsgHub.\"\"\"\n\n    # Create a message hub\n    async with MsgHub(\n        participants=[alice, bob, charlie],\n        announcement=Msg(\n            \"user\",\n            \"Now introduce yourself in one sentence, including your name, age and career.\",\n            \"user\",\n        ),\n    ) as hub:\n        # Group chat without manual message passing\n        await alice()\n        await bob()\n        await charlie()\n\n\nasyncio.run(example_broadcast_message())\n\n# %%\n# Now let's check if Bob, Charlie and David received Alice's message.\n#\n\n\nasync def check_broadcast_message():\n    \"\"\"Check if the messages are broadcast correctly.\"\"\"\n    user_msg = Msg(\n        \"user\",\n        \"Do you know who's Alice, and what she does? Answer me briefly.\",\n        \"user\",\n    )\n\n    await bob(user_msg)\n    await charlie(user_msg)\n    await david(user_msg)\n\n\nasyncio.run(check_broadcast_message())\n\n# %%\n# Now we observe that Bob and Charlie know Alice and her profession, while David has no idea\n# about Alice since he is not included in the ``MsgHub`` context.\n#\n#\n# Dynamic Participant Management\n# ---------------------------------------\n# Additionally, ``MsgHub`` supports to dynamically manage participants by the following methods:\n#\n# - ``add``: add one or multiple agents as new participants\n# - ``delete``: remove one or multiple agents from participants, and they will no longer receive broadcasted messages\n# - ``broadcast``: broadcast a message to all current participants\n#\n# .. note:: The newly added participants will not receive the previous messages.\n#\n# .. code-block:: python\n#\n#       async with MsgHub(participants=[alice]) as hub:\n#           # Add new participants\n#           hub.add(david)\n#\n#           # Remove participants\n#           hub.delete(alice)\n#\n#           # Broadcast to all current participants\n#           await hub.broadcast(\n#               Msg(\"system\", \"Now we begin to ...\", \"system\"),\n#           )\n#\n#\n# Pipeline\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# Pipeline serves as a syntax sugar for multi-agent orchestration.\n#\n# Currently, AgentScope provides three main pipeline implementations:\n#\n# 1. **Sequential Pipeline**: Execute agents one by one in a predefined order\n# 2. **Fanout Pipeline**: Distribute the same input to multiple agents and collect their responses\n# 3. **Stream Printing Messages**: Convert the printing messages from an agent into an async generator\n#\n# Sequential Pipeline\n# ------------------------\n# The sequential pipeline executes agents one by one, where the output of the previous agent\n# becomes the input of the next agent.\n#\n# For example, the two following code snippets are equivalent:\n#\n#\n# .. code-block:: python\n#     :caption: Code snippet 1: Manually call agents one by one\n#\n#     msg = None\n#     msg = await alice(msg)\n#     msg = await bob(msg)\n#     msg = await charlie(msg)\n#     msg = await david(msg)\n#\n#\n# .. code-block:: python\n#     :caption: Code snippet 2: Use sequential pipeline\n#\n#     from agentscope.pipeline import sequential_pipeline\n#     msg = await sequential_pipeline(\n#         # List of agents to be executed in order\n#         agents=[alice, bob, charlie, david],\n#         # The first input message, can be None\n#         msg=None\n#     )\n#\n\n# %%\n# Fanout Pipeline\n# ------------------------\n# The fanout pipeline distributes the same input message to multiple agents simultaneously and collects all their responses. This is useful when you want to gather different perspectives or expertise on the same topic.\n#\n# For example, the two following code snippets are equivalent:\n#\n#\n# .. code-block:: python\n#     :caption: Code snippet 3: Manually call agents one by one\n#\n#     from copy import deepcopy\n#\n#     msgs = []\n#     msg = None\n#     for agent in [alice, bob, charlie, david]:\n#         msgs.append(await agent(deepcopy(msg)))\n#\n#\n# .. code-block:: python\n#     :caption: Code snippet 4: Use fanout pipeline\n#\n#     from agentscope.pipeline import fanout_pipeline\n#     msgs = await fanout_pipeline(\n#         # List of agents to be executed in order\n#         agents=[alice, bob, charlie, david],\n#         # The first input message, can be None\n#         msg=None,\n#         enable_gather=False,\n#     )\n#\n# .. note::\n#     The ``enable_gather`` parameter controls the execution mode of the fanout pipeline:\n#\n#     - ``enable_gather=True`` (default): Executes all agents **concurrently** using ``asyncio.gather()``. This provides better performance for I/O-bound operations like API calls, as agents run in parallel.\n#     - ``enable_gather=False``: Executes agents **sequentially** one by one. This is useful when you need deterministic execution order or want to avoid overwhelming external services with concurrent requests.\n#\n#     Choose concurrent execution for better performance, or sequential execution for predictable ordering and resource control.\n#\n# .. tip::\n#     By combining ``MsgHub`` and ``sequential_pipeline`` or ``fanout_pipeline``, you can create more complex workflows very easily.\n#\n#\n# Stream Printing Messages\n# -------------------------------------\n# The ``stream_printing_messages`` function converts the printing messages from agent(s) into an async generator.\n# It will help you to obtain the intermediate messages from the agent(s) in a streaming way.\n#\n# It accepts a list of agents and a coroutine task, then returns an async generator that yields tuples of ``(Msg, bool)``,\n# containing the printing message during execution of the coroutine task.\n#\n# Note the messages with the same ``id`` are considered as the same message, and the ``last`` flag indicates whether it's the last chunk of this message.\n#\n# Taking the following code snippet as an example:\n\n\nasync def run_example_pipeline() -> None:\n    \"\"\"Run an example of streaming printing messages.\"\"\"\n    agent = create_agent(\"Alice\", 20, \"student\")\n\n    # We disable the terminal printing to avoid messy outputs\n    agent.set_console_output_enabled(False)\n\n    async for msg, last in stream_printing_messages(\n        agents=[agent],\n        coroutine_task=agent(\n            Msg(\"user\", \"Hello, who are you?\", \"user\"),\n        ),\n    ):\n        print(msg, last)\n        if last:\n            print()\n\n\nasyncio.run(run_example_pipeline())\n\n\n# %%\n# Advanced Pipeline Features\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# Additionally, for reusability, we also provide a class-based implementation:\n#\n# .. code-block:: python\n#     :caption: Using SequentialPipeline class\n#\n#     from agentscope.pipeline import SequentialPipeline\n#\n#     # Create a pipeline object\n#     pipeline = SequentialPipeline(agents=[alice, bob, charlie, david])\n#\n#     # Call the pipeline\n#     msg = await pipeline(msg=None)\n#\n#     # Reuse the pipeline with different input\n#     msg = await pipeline(msg=Msg(\"user\", \"Hello!\", \"user\"))\n#\n#\n# .. code-block:: python\n#     :caption: Using FanoutPipeline class\n#\n#     from agentscope.pipeline import FanoutPipeline\n#\n#     # Create a pipeline object\n#     pipeline = FanoutPipeline(agents=[alice, bob, charlie, david])\n#\n#     # Call the pipeline\n#     msgs = await pipeline(msg=None)\n#\n#     # Reuse the pipeline with different input\n#     msgs = await pipeline(msg=Msg(\"user\", \"Hello!\", \"user\"))\n#\n"
  },
  {
    "path": "docs/tutorial/en/src/task_plan.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _plan:\n\nPlan\n=========================\n\nThe Plan Module enables agents to formally break down complex tasks into manageable sub-tasks and execute them systematically. Key features include:\n\n- Support **manual plan specification**\n- Comprehensive plan management capabilities:\n   - **Creating, modifying, abandoning, and restoring** plans\n   - **Switching** between multiple plans\n   - **Gracefully handling interruptions** by temporarily suspending plans to address user queries or urgent tasks\n- **Real-time visualization and monitoring** of plan execution\n\n.. note:: The current plan module has the following limitations, and we are working on improving them:\n\n - The subtasks in a plan must be executed sequentially\n\nSpecifically, the plan module works by\n\n- providing tool functions for plan management\n- inserting hint messages to guide the ReAct agent to complete the plan\n\nThe following figure illustrates how the plan module works with the ReAct agent:\n\n.. figure:: ../../_static/images/plan.png\n    :width: 90%\n    :alt: Plan module\n    :class: bordered-image\n    :align: center\n\n    How the plan module works with the ReAct agent\n\n\"\"\"\nimport asyncio\nimport os\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.plan import PlanNotebook, Plan, SubTask\n\n# %%\n# PlanNotebook\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# The `PlanNotebook` class is the core of the plan module, responsible for providing\n#\n# - plan-related tool functions\n# - hint messages to guide the agent to finish the plan\n#\n# The `PlanNotebook` class can be instantiated with the following parameters:\n#\n# .. list-table:: Parameters of the `PlanNotebook` constructor\n#   :header-rows: 1\n#\n#   * - Name\n#     - Type\n#     - Description\n#   * - ``max_subtasks``\n#     - ``int | None``\n#     - The maximum number of subtasks allowed in a plan, infinite if None\n#   * - ``plan_to_hint``\n#     - ``Callable[[Plan | None], str | None] | None``\n#     - The function to generate hint message based on the current plan. If not provided, a default `DefaultPlanToHint` object will be used.\n#   * - ``storage``\n#     - ``PlanStorageBase | None``\n#     - The plan storage. If not provided, a default in-memory storage will be used.\n#\n# The ``plan_to_hint`` callable object is the most important part of the\n# `PlanNotebook` class, also serves as the interface for prompt engineering.\n# We have built a default `DefaultPlanToHint` class that can be used directly.\n# Developers are encouraged to providing their own ``plan_to_hint`` function\n# for better performance.\n#\n# The ``storage`` is to store historical plans, allowing agent to\n# retrieve and restore historical plans. Developers are encouraged to\n# implement their own plan storage by inheriting the ``PlanStorageBase`` class.\n# If not provided, a default in-memory storage will be used.\n#\n# .. tip:: The ``PlanStorageBase`` class inherits from the ``StateModule``\n#  class, so that the plan storage will also be saved and loaded by the\n#  session management.\n#\n# The core attributes and methods of the `PlanNotebook` class are summarized\n# as follows:\n#\n# .. list-table:: Core attributes and methods of the `PlanNotebook` class\n#    :header-rows: 1\n#\n#    * - Type\n#      - Name\n#      - Description\n#    * - attribute\n#      - ``current_plan``\n#      - The current plan that the agent is executing\n#    * -\n#      - ``storage``\n#      - The storage for historical plans, used for retrieving and restoring historical plans\n#    * -\n#      - ``plan_to_hint``\n#      - A callable object that takes the current plan as input and generates a hint message to guide the agent to finish the plan\n#    * - method\n#      - ``list_tools``\n#      - List all the tool functions provided by the `PlanNotebook` class\n#    * -\n#      - ``get_current_hint``\n#      - Get the hint message for the current plan, which will call the ``plan_to_hint`` function\n#    * -\n#      - | ``create_plan``,\n#        | ``view_subtasks``,\n#        | ``revise_current_plan``,\n#        | ``update_subtask_state``,\n#        | ``finish_subtask``,\n#        | ``finish_plan``,\n#        | ``view_historical_plans``,\n#        | ``recover_historical_plan``\n#      - The tool functions that allows the agent to manage the plan and subtasks\n#    * -\n#      - ``register_plan_change_hook``\n#      - Register a hook function that will be called when the plan is changed, used to plan visualization and monitoring\n#    * -\n#      - ``remove_plan_change_hook``\n#      - Remove a registered plan change hook function\n#\n# The ``list_tools`` method is a quick way to obtain all tool functions, so that you can register them to the agent's toolkit.\n\nplan_notebook = PlanNotebook()\n\n\nasync def list_tools() -> None:\n    \"\"\"List the tool functions provided by PlanNotebook.\"\"\"\n    print(\"The tools provided by PlanNotebook:\")\n    for tool in plan_notebook.list_tools():\n        print(tool.__name__)\n\n\nasyncio.run(list_tools())\n\n\n# %%\n# Working with ReActAgent\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# The `ReActAgent` in AgentScope has integrated the plan module by a ``plan_notebook`` parameter in its constructor.\n# Once provided, the agent will\n#\n# - be equipped with the plan management tool functions, and\n# - be inserted with the hint messages at the beginning of each reasoning step\n#\n# There are two ways to use the plan module with the `ReActAgent`:\n#\n# - Manual plan specification: Users can manually create a plan by calling the ``create_plan`` tool function, and initialize the `ReActAgent` with the plan notebook.\n# - Agent-managed plan execution: The agent will create and manage the plan by itself, by calling the plan management tool functions.\n#\n# Manual Plan Specification\n# ---------------------------------\n# Manually creating a plan is straightforward by calling the ``create_plan`` tool function.\n# The following is an example of manually creating a plan to conduct a comprehensive research on the LLM-empowered agent.\n#\nasync def manual_plan_specification() -> None:\n    \"\"\"Manual plan specification example.\"\"\"\n    await plan_notebook.create_plan(\n        name=\"Research on Agent\",\n        description=\"Conduct a comprehensive research on the LLM-empowered agent.\",\n        expected_outcome=\"A Markdown format report answer three questions: 1. What's agent? 2. What's the current state of the art of agent? 3. What's the future trend of agent?\",\n        subtasks=[\n            SubTask(\n                name=\"Search agent-related survey papers\",\n                description=(\n                    \"Search for survey parers on multiple sources, including \"\n                    \"Google Scholar, arXiv, and Semantic Scholar. Must be \"\n                    \"published after 2021 and have more than 50 citations.\"\n                ),\n                expected_outcome=\"A paper list in Markdown format\",\n            ),\n            SubTask(\n                name=\"Read and summarize the papers\",\n                description=(\n                    \"Read the papers found in the previous step, and \"\n                    \"summarize the key points, including the definition, \"\n                    \"taxonomy, challenges, and key directions.\"\n                ),\n                expected_outcome=\"A summary of the key points in Markdown format\",\n            ),\n            SubTask(\n                name=\"Research on recent advances of large company\",\n                description=(\n                    \"Research on the recent advances of large companies, \"\n                    \"including Google, Microsoft, OpenAI, Anthropic, Alibaba \"\n                    \"and Meta. Find the official blogs or news articles.\"\n                ),\n                expected_outcome=\"A recent advances of large company \",\n            ),\n            SubTask(\n                name=\"Write a report\",\n                description=(\n                    \"Write a report based on the previous steps, and answer \"\n                    \"the three questions in the expected outcome.\"\n                ),\n                expected_outcome=(\n                    \"A Markdown format report answer three questions: 1. \"\n                    \"What's agent? 2. What's the current state of the art of \"\n                    \"agent? 3. What's the future trend of agent?\"\n                ),\n            ),\n        ],\n    )\n\n    print(\"The current hint message:\\n\")\n    msg = await plan_notebook.get_current_hint()\n    print(f\"{msg.name}: {msg.content}\")\n\n\nasyncio.run(manual_plan_specification())\n\n# %%\n# After creating the plan, you can initialize the `ReActAgent` with the\n# plan notebook as follows:\n\nagent = ReActAgent(\n    name=\"Friday\",\n    sys_prompt=\"You are a helpful assistant.\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n    ),\n    formatter=DashScopeChatFormatter(),\n    plan_notebook=plan_notebook,\n)\n\n# %%\n# Agent-Managed Plan Execution\n# ---------------------------------\n# Agent can also create and manage the plan by itself, by calling the plan management tool functions.\n# We just need to initialize the `ReActAgent` with the plan notebook as follows:\n#\n\nagent = ReActAgent(\n    name=\"Friday\",\n    sys_prompt=\"You are a helpful assistant.\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n    ),\n    formatter=DashScopeChatFormatter(),\n    plan_notebook=PlanNotebook(),\n)\n\n# %%\n# After that, we can build a loop to interact with the agent as follows.\n# Once the task is complex, the agent will create a plan by itself and\n# execute the plan step by step.\n#\n# .. code-block:: python\n#     :caption: Build conversation with the plan agent\n#\n#     async def interact_with_agent() -> None:\n#         \"\"\"Interact with the plan agent.\"\"\"\n#         user = UserAgent(name=\"user\")\n#\n#         msg = None\n#         while True:\n#             msg = await user(msg)\n#             if msg.get_text_content() == \"exit\":\n#                 break\n#             msg = await agent(msg)\n#\n#     asyncio.run(interact_with_agent())\n#\n#\n# Plan Visualization and Monitoring\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# AgentScope supports real-time visualization and monitoring of the plan\n# execution by the plan change hook function.\n#\n# They will be triggered when the plan is changed by calling the tool\n# functions. A template of the plan change hook function is as follows:\n#\n\n\ndef plan_change_hook_template(self: PlanNotebook, plan: Plan) -> None:\n    \"\"\"A template of the plan change hook function.\n\n    Args:\n        self (`PlanNotebook`):\n            The PlanNotebook instance.\n        plan (`Plan`):\n            The current plan instance (after the change).\n    \"\"\"\n    # Forward the plan to the frontend for visualization or other processing\n"
  },
  {
    "path": "docs/tutorial/en/src/task_prompt.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _prompt:\n\nPrompt Formatter\n=========================\n\nThe formatter module in AgentScope is responsible for\n\n- converting messages into the expected format for different LLM APIs,\n- (optional) truncating messages to fit within token limits,\n- (optional) prompt engineering, e.g. summarizing long conversations.\n\nThe last two are optional and can also be handled by developers within the memory or at the agent level.\n\nIn AgentScope, there are two types of formatters, \"ChatFormatter\" and \"MultiAgentFormatter\", distinguished by the agent identities in their input messages.\n\n- **ChatFormatter**: Designed for standard user-assistant scenario (chatbot), using the ``role`` field to identify the user and assistant.\n- **MultiAgentFormatter**: Designed for multi-agent scenario, use the ``name`` field to identify different agents, which will combine conversation history into a single user message dictionary.\n\nThe built-in formatters are listed below\n\n.. list-table:: The built-in formatters in AgentScope\n    :header-rows: 1\n\n    * - API Provider\n      - User-assistant Scenario\n      - Multi-Agent Scenario\n    * - OpenAI\n      - ``OpenAIChatFormatter``\n      - ``OpenAIMultiAgentFormatter``\n    * - Anthropic\n      - ``AnthropicChatFormatter``\n      - ``AnthropicMultiAgentFormatter``\n    * - DashScope\n      - ``DashScopeChatFormatter``\n      - ``DashScopeMultiAgentFormatter``\n    * - Gemini\n      - ``GeminiChatFormatter``\n      - ``GeminiChatFormatter``\n    * - Ollama\n      - ``OllamaChatFormatter``\n      - ``OllamaMultiAgentFormatter``\n    * - DeepSeek\n      - ``DeepSeekChatFormatter``\n      - ``DeepSeekMultiAgentFormatter``\n    * - vLLM\n      - ``OpenAIChatFormatter``\n      - ``OpenAIMultiAgentFormatter``\n\n.. tip:: The OpenAI API supports the `name` field, so that `OpenAIChatFormatter` can also be used in multi-agent scenario. You can also use the `OpenAIMultiAgentFormatter` instead, which combine conversation history into a single user message.\n\nBesides, the built-in formatters support to convert different message blocks into the expected format for the target API, which are list below:\n\n.. list-table:: The supported message blocks in the built-in formatters\n    :header-rows: 1\n\n    * - Formatter\n      - tool_use/result\n      - image\n      - audio\n      - video\n      - thinking\n    * - ``OpenAIChatFormatter``\n      - ✅\n      - ✅\n      - ✅\n      - ❌\n      -\n    * - ``DashScopeChatFormatter``\n      - ✅\n      - ✅\n      - ✅\n      - ❌\n      -\n    * - ``DashScopeMultiAgentFormatter``\n      - ✅\n      - ✅\n      - ✅\n      - ❌\n      -\n    * - ``AnthropicChatFormatter``\n      - ✅\n      - ✅\n      - ❌\n      - ❌\n      - ✅\n    * - ``AnthropicMultiAgentFormatter``\n      - ✅\n      - ✅\n      - ❌\n      - ❌\n      - ✅\n    * - ``GeminiChatFormatter``\n      - ✅\n      - ✅\n      - ✅\n      - ✅\n      -\n    * - ``GeminiMultiAgentFormatter``\n      - ✅\n      - ✅\n      - ✅\n      - ✅\n      -\n    * - ``OllamaChatFormatter``\n      - ✅\n      - ✅\n      - ❌\n      - ❌\n      -\n    * - ``OllamaMultiAgentFormatter``\n      - ✅\n      - ✅\n      - ❌\n      - ❌\n      -\n    * - ``DeepSeekChatFormatter``\n      - ✅\n      - ❌\n      - ❌\n      - ❌\n      -\n    * - ``DeepSeekMultiAgentFormatter``\n      - ✅\n      - ❌\n      - ❌\n      - ❌\n      -\n\n.. note:: As stated in the `official documentation <https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#preserving-thinking-blocks>`_, only Anthropic suggests to preserve the thinking blocks in prompt formatting. For the others, we just ignore the thinking blocks in the input messages.\n\nReAct-Oriented Formatting\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nThe built-in formatters are all designed to support ReAct-style agents, where the input messages **consist of alternating conversation history and tool call sequences**.\n\nIn user-assistant scenario, the conversation history includes the user and assistant messages, we just convert them into the expected format directly.\nHowever, in multi-agent scenario, the conversation history is a list of messages from different agents as follows:\n\n.. figure:: ../../_static/images/multiagent_msgs.png\n    :alt: example of multiagent messages\n    :width: 85%\n    :align: center\n\n    *Example of multi-agent messages*\n\n\nTherefore, we have to merge the conversation history into a single user message with tags \"<history>\" and \"</history>\".\nTaking DashScope as an example, the formatted message will look like this:\n\"\"\"\n\nfrom agentscope.token import HuggingFaceTokenCounter\nfrom agentscope.formatter import DashScopeMultiAgentFormatter\nfrom agentscope.message import Msg, ToolResultBlock, ToolUseBlock, TextBlock\nimport asyncio, json\n\n\ninput_msgs = [\n    # System prompt\n    Msg(\"system\", \"You're a helpful assistant named Friday\", \"system\"),\n    # Conversation history\n    Msg(\"Bob\", \"Hi, Alice, do you know the nearest library?\", \"assistant\"),\n    Msg(\n        \"Alice\",\n        \"Sorry, I don't know. Do you have any idea, Charlie?\",\n        \"assistant\",\n    ),\n    Msg(\n        \"Charlie\",\n        \"No, let's ask Friday. Friday, get me the nearest library.\",\n        \"assistant\",\n    ),\n    # Tool sequence\n    Msg(\n        \"Friday\",\n        [\n            ToolUseBlock(\n                type=\"tool_use\",\n                name=\"get_current_location\",\n                id=\"1\",\n                input={},\n            ),\n        ],\n        \"assistant\",\n    ),\n    Msg(\n        \"system\",\n        [\n            ToolResultBlock(\n                type=\"tool_result\",\n                name=\"get_current_location\",\n                id=\"1\",\n                output=[TextBlock(type=\"text\", text=\"104.48, 36.30\")],\n            ),\n        ],\n        \"system\",\n    ),\n    Msg(\n        \"Friday\",\n        [\n            ToolUseBlock(\n                type=\"tool_use\",\n                name=\"search_around\",\n                id=\"2\",\n                input={\"location\": [104.48, 36.30], \"keyword\": \"library\"},\n            ),\n        ],\n        \"assistant\",\n    ),\n    Msg(\n        \"system\",\n        [\n            ToolResultBlock(\n                type=\"tool_result\",\n                name=\"search_around\",\n                id=\"2\",\n                output=[TextBlock(type=\"text\", text=\"[...]\")],\n            ),\n        ],\n        \"system\",\n    ),\n    # Conversation history continues\n    Msg(\"Friday\", \"The nearest library is ...\", \"assistant\"),\n    Msg(\"Bob\", \"Thanks, Friday!\", \"assistant\"),\n    Msg(\"Alice\", \"Let's go together.\", \"assistant\"),\n]\n\n\nasync def run_formatter_example() -> list[dict]:\n    \"\"\"Example of how to format multi-agent messages.\"\"\"\n    formatter = DashScopeMultiAgentFormatter()\n    formatted_message = await formatter.format(input_msgs)\n    print(\"The formatted message:\")\n    print(json.dumps(formatted_message, indent=4))\n    return formatted_message\n\n\nformatted_message = asyncio.run(run_formatter_example())\n\n# %%\n# Specifically, the conversation histories are formatted into:\n#\nprint(\"The first conversation history:\")\nprint(formatted_message[1][\"content\"])\n\nprint(\"\\nThe second conversation history:\")\nprint(formatted_message[-1][\"content\"])\n\n# %%\n# Truncation-based Formatting\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# With the token module in AgentScope, the built-in formatters support to truncate the input messages by **deleting the oldest messages** (except the system prompt message) when the token exceeds the limit.\n#\n# Taking OpenAIFormatter as an example, we first calculate the total number of tokens of the input messages.\n#\n\n\nasync def run_token_counter() -> int:\n    \"\"\"Compute the token number of the input messages.\"\"\"\n    # We use huggingface token counter for dashscope models.\n    token_counter = HuggingFaceTokenCounter(\n        \"Qwen/Qwen2.5-VL-3B-Instruct\",\n        use_mirror=False,\n    )\n\n    return await token_counter.count(formatted_message)\n\n\n# %%\n# Then we set the maximum token limit to 20 tokens less than the total number of tokens and run the formatter.\n#\n\n\nasync def run_truncated_formatter() -> None:\n    \"\"\"Example of how to format messages with truncation.\"\"\"\n    token_counter = HuggingFaceTokenCounter(\n        pretrained_model_name_or_path=\"Qwen/Qwen2.5-VL-3B-Instruct\",\n        use_mirror=False,\n    )\n    formatter = DashScopeMultiAgentFormatter(\n        token_counter=token_counter,\n        max_tokens=n_tokens - 20,\n    )\n    truncated_formatted_message = await formatter.format(input_msgs)\n    n_truncated_tokens = await token_counter.count(truncated_formatted_message)\n    print(\"The tokens after truncation: \", n_truncated_tokens)\n\n    print(\"\\nThe conversation history after truncation:\")\n    print(truncated_formatted_message[1][\"content\"])\n\n\n# %%\n# We can see the first two messages from Bob and Alice are removed to fit within the context length limits.\n#\n#\n# Customizing Formatter\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope provides two base classes ``FormatterBase`` and its child class ``TruncatedFormatterBase``.\n# The ``TruncatedFormatterBase`` class provides the FIFO truncation strategy, and all the built-in formatters are inherited from it.\n#\n# .. list-table:: The base classes of formatters in AgentScope\n#   :header-rows: 1\n#\n#   * - Class\n#     - Abstract Method\n#     - Description\n#   * - ``FormatterBase``\n#     - ``format``\n#     - Format the input ``Msg`` objects into the expected format for the target API\n#   * - ``TruncatedFormatterBase``\n#     - ``_format_agent_message``\n#     - Format the agent messages, which may contain multiple identities in multi-agent scenario\n#   * -\n#     - ``_format_tool_sequence``\n#     - Format the tool use and result sequence into the expected format\n#   * -\n#     - ``_format`` (optional)\n#     - Format the input ``Msg`` objects into the expected format for the target API\n#\n# .. tip:: - The ``_format`` in ``TruncatedFormatterBase`` groups input messages into agent messages and tool sequences, and then format them by calling ``_format_agent_message`` and ``_format_tool_sequence`` respectively. You can override it to implement your own formatting strategy.\n#  - Optionally, you can override the ``_truncate`` method in ``TruncatedFormatterBase`` to implement your own truncation strategy.\n#\n# Further Reading\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# - :ref:`token`\n# - :ref:`model`\n#\n"
  },
  {
    "path": "docs/tutorial/en/src/task_rag.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _rag:\n\nRAG\n===========================\n\nAgentScope provides built-in support for Retrieval-Augmented Generation (RAG)\ntasks. This tutorial demonstrates\n\n- how to use the RAG module in AgentScope,\n- how to use **multimodal** RAG,\n- how to integrate the RAG module with the ``ReActAgent`` in\n    - **agentic manner** and\n    - **generic manner**:\n\n.. list-table:: RAG module integration methods\n    :header-rows: 1\n\n    * - Integration Manner\n      - Description\n      - Advantages\n      - Disadvantages\n    * - Agentic Manner\n      - The RAG module is integrated with the agent as a tool, and the agent can decide when to retrieve knowledge and the queries to be retrieved.\n      - - The query rewriting and knowledge retrieval are integrated into the ReAct process, which is more flexible,\n        - the agent can rewrite the query based on all the available information,\n        - only retrieve knowledge when necessary.\n      - High requirements for the LLM's reasoning and tool-use capabilities.\n    * - Generic Manner\n      - Retrieve knowledge at the beginning of each reply, and attach the retrieved knowledge to the prompt in a user message.\n      - - Simple, easy to implement,\n        - does not require high reasoning and tool-use capabilities from the LLM.\n      - - Still retrieve knowledge even when not necessary, and\n        - if the retrieval is imperceptible to the user, the waiting time may be longer.\n\n.. note:: As an open-source project, AgentScope doesn't insist that developers\n use the built-in RAG module. Our target is make the development easier and\n more enjoyable, so integrating other RAG implementations, frameworks, or\n services are welcome and encouraged!\n\n\"\"\"\nimport asyncio\nimport json\nimport os\n\nfrom matplotlib import pyplot as plt\n\nimport agentscope\nfrom agentscope.agent import ReActAgent\nfrom agentscope.embedding import (\n    DashScopeTextEmbedding,\n    DashScopeMultiModalEmbedding,\n)\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.rag import (\n    TextReader,\n    SimpleKnowledge,\n    QdrantStore,\n    Document,\n    ImageReader,\n)\nfrom agentscope.tool import Toolkit\n\n# %%\n# Using RAG Module\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# The RAG module in AgentScope is composed of two core components:\n#\n# - **Reader**: responsible for reading and chunking the input documents.\n# - **Knowledge**: responsible for algorithm implementation of knowledge retrieval and updating.\n#\n# .. note:: We're integrating more vector databases and readers into AgentScope. Contributions are welcome!\n#\n# The currently built-in readers include:\n#\n\nfor _ in agentscope.rag.__all__:\n    if _.endswith(\"Reader\"):\n        print(f\"- {_}\")\n\n# %%\n# they are responsible for reading data and chunking them into ``Document`` objects.\n# The ``Document`` class has the following fields:\n#\n# - ``metadata``: the metadata of the document, including the content, doc_id, chunk_id, and total_chunks.\n# - ``embedding``: the embedding vector of the document, which will be filled when the document is added to or retrieved from the knowledge base.\n# - ``score``: the relevance score of the document, which will be filled when the document is retrieved from the knowledge base.\n#\n# Taking the ``TextReader`` as an example, it can read and chunk documents from text strings.\n#\n\n\nasync def example_text_reader(print_docs: bool) -> list[Document]:\n    \"\"\"The example of using TextReader.\"\"\"\n    # Create a text reader with chunk size of 512 characters, split by characters\n    reader = TextReader(chunk_size=512, split_by=\"paragraph\")\n\n    # Read documents from a text string\n    documents = await reader(\n        text=(\n            # Fake personal profile for demonstration\n            \"I'm John Doe, 28 years old.\\n\"\n            \"I live in San Francisco. I work at OpenAI as a \"\n            \"software engineer. I love hiking and photography.\\n\"\n            \"My father is Michael Doe, a doctor. I'm very proud of him. \"\n            \"My mother is Sarah Doe, a teacher. She is very kind and \"\n            \"always helps me with my studies.\\n\"\n            \"I'm now a PhD student at Stanford University, majoring in \"\n            \"Computer Science. My advisor is Prof. Jane Williams, who is \"\n            \"a leading expert in artificial intelligence. I have published \"\n            \"several papers in top conferences, such as NeurIPS and ICML.\\n\"\n            \"My best friend is James Smith.\\n\"\n        ),\n    )\n\n    if print_docs:\n        print(\"The length of the documents:\", len(documents))\n        for idx, doc in enumerate(documents):\n            print(\"Document #\", idx)\n            print(\"\\tScore: \", doc.score)\n            print(\"\\tMetadata: \", json.dumps(doc.metadata, indent=2), \"\\n\")\n\n    return documents\n\n\ndocs = asyncio.run(example_text_reader(print_docs=True))\n\n# %%\n# Note there doesn't exist a universally best chunk size and splitting method, especially for PDF files, we highly\n# encourage developers to implement or contribute their own readers according to their specific scenarios.\n# To create a custom reader, you only need to inherit the ``ReaderBase`` class and implement the ``__call__`` method.\n#\n# After chunking the documents, we can create a knowledge base to store the documents and perform retrieval.\n# Such a knowledge base is initialized by providing **an embedding model** and **an embedding store** (also known as a vector database).\n# Agentscope provides built-in support for `Qdrant <https://qdrant.tech/>`_ as the embedding store and a simple knowledge base implementation ``SimpleKnowledge``.\n# They can be used as follows:\n#\n# .. note::\n#\n#  - We're integrating more vector databases into AgentScope. Contributions are welcome!\n#  - The Qdrant store supports various storage backends by the ``location`` parameter, including in-memory, local file, and remote server. Refer to the `Qdrant documentation <https://qdrant.tech/>`_ for more details.\n#\n\n\nasync def build_knowledge_base() -> SimpleKnowledge:\n    \"\"\"Build a knowledge base with sample documents.\"\"\"\n    # Read documents using the text reader\n    documents = await example_text_reader(print_docs=False)\n\n    # Create an in-memory knowledge base instance\n    knowledge = SimpleKnowledge(\n        # Choose an embedding model to convert text to embedding vectors\n        embedding_model=DashScopeTextEmbedding(\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            model_name=\"text-embedding-v4\",\n            dimensions=1024,\n        ),\n        # Choose Qdrant as the embedding store\n        embedding_store=QdrantStore(\n            location=\":memory:\",  # Use in-memory storage for demonstration\n            collection_name=\"test_collection\",\n            dimensions=1024,  # The dimension of the embedding vectors\n        ),\n    )\n\n    # Insert documents into the knowledge base\n    await knowledge.add_documents(documents)\n\n    # Retrieve relevant documents based on a given query\n    docs = await knowledge.retrieve(\n        query=\"Who is John Doe's father?\",\n        limit=3,\n        score_threshold=0.5,\n    )\n\n    print(\"Retrieved Documents:\")\n    for doc in docs:\n        print(doc, \"\\n\")\n\n    return knowledge\n\n\nknowledge = asyncio.run(build_knowledge_base())\n\n# %%\n# The knowledge base class provides two main methods: ``add_documents`` and\n# ``retrieve``, which are used to add documents to the knowledge base and\n# retrieve relevant documents based on a given query, respectively.\n#\n# In addition, the knowledge base class also provides a convenient method\n# ``retrieve_knowledge``, which wraps the ``retrieve`` method into a tool\n# function that can be directly registered in the toolkit of an agent.\n#\n#\n# Customizing RAG Components\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope supports and encourages developers to customize their own RAG components, including readers, knowledge bases and embedding stores.\n# Specifically, we provide the following base classes for customization:\n#\n# .. list-table:: RAG Base Classes\n#     :header-rows: 1\n#\n#     * - Base Class\n#       - Description\n#       - Abstract Methods\n#     * - ``ReaderBase``\n#       - The base class for all readers.\n#       - ``__call__``\n#     * - ``VDBStoreBase``\n#       - The base class for embedding stores (vector databases).\n#       - | ``add``\n#         | ``search``\n#         | ``get_client`` (optional)\n#         | ``delete`` (optional)\n#     * - ``KnowledgeBase``\n#       - The base class for knowledge bases.\n#       - | ``retrieve``\n#         | ``add_documents``\n#\n#\n# The `get_client` method in the ``VDBStoreBase`` allows developers to access the full functionality of the underlying vector database.\n# So that they can implement more advanced features based on the vector database, e.g. index management, advanced search, etc.\n#\n# Integrating with ReActAgent\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# Next we demonstrate how to integrate the RAG module with the ``ReActAgent``\n# class in AgentScope in agentic and generic manners.\n#\n# Agentic Manner\n# --------------------------------\n# In agentic manner, the ReAct agent is empowered with the ability to decide when to retrieve knowledge and the queries to be retrieved.\n# It's very easy to integrate the RAG module with the ``ReActAgent`` class in AgentScope, just by registering the ``retrieve_knowledge`` method of the knowledge base as a tool,\n# and providing a proper description for the tool.\n\n\nasync def example_agentic_manner() -> None:\n    \"\"\"The example of integrating RAG module with ReActAgent in agentic manner.\"\"\"\n    # Create a ReAct agent\n    toolkit = Toolkit()\n\n    # Create the ReAct agent with DashScope as the model\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"You are a helpful assistant named Friday.\",\n        model=DashScopeChatModel(\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            model_name=\"qwen-max\",\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n    )\n\n    print(\"The first response: \")\n    # Ask some questions about Tony Stank\n    await agent(\n        Msg(\n            \"user\",\n            \"John Doe is my best friend.\",\n            \"user\",\n        ),\n    )\n\n    # Register the retrieve_knowledge method as a tool function in the toolkit\n    toolkit.register_tool_function(\n        knowledge.retrieve_knowledge,\n        func_description=(  # Provide a clear description for the tool\n            \"The tool used to retrieve documents relevant to the given query. \"\n            \"Use this tool when you need to find some information about John Doe.\"\n        ),\n    )\n\n    print(\"\\n\\nThe second response: \")\n    # We hope the agent can rewrite the query to be more specific, e.g.\n    # \"Who is Tony Stank's father?\" or \"Tony Stank's father\"\n    await agent(\n        Msg(\n            \"user\",\n            \"Do you know who his father is?\",\n            \"user\",\n        ),\n    )\n\n\nasyncio.run(example_agentic_manner())\n\n# %%\n# In the above example, our question is \"Do you know who his father is?\".\n# We hope the agent can rewrite the query with the historical information, and\n# rewrite it to be more specific, e.g. \"Who is John Doe's father?\" or \"John Doe's father\".\n#\n#\n# Generic Manner\n# --------------------------------\n# The ``ReActAgent`` also integrates the RAG module in a generic manner, which\n# retrieves knowledge at the beginning of each reply, and attaches the\n# retrieved knowledge to the prompt in a user message.\n#\n# Just set the ``knowledge`` parameter of the ``ReActAgent``, and the agent\n# will automatically retrieve knowledge at the beginning of each reply.\n#\n\n\nasync def example_generic_manner() -> None:\n    \"\"\"The example of integrating RAG module with ReActAgent in generic manner.\"\"\"\n    # Create a ReAct agent\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"You are a helpful assistant named Friday.\",\n        model=DashScopeChatModel(\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            model_name=\"qwen-max\",\n        ),\n        formatter=DashScopeChatFormatter(),\n        #\n        knowledge=knowledge,\n    )\n\n    await agent(\n        Msg(\n            \"user\",\n            \"Do you know who John Doe's father is?\",\n            \"user\",\n        ),\n    )\n\n    print(\"Take a look at the agent's memory:\")\n    content = (await agent.memory.get_memory())[1].content\n    print(json.dumps(content, indent=2, ensure_ascii=False))\n\n\nasyncio.run(example_generic_manner())\n\n\n# %%\n# Multimodal RAG\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# The RAG module in AgentScope supports multimodal RAG natively, as\n#\n# - AgentScope supports multimodal embedding API, e.g. ``DashScopeMultimodalEmbedding``.\n# - The ``Document`` class supports text, image, and other modalities in its ``metadata`` field.\n#\n# Thus, we can directly use the multimodal reader and embedding model to build\n# a multimodal knowledge base as follows.\n#\n# First we prepare an image with some text about my name.\n\n# Prepare an image with the text \"John Doe's father is Michael Doe.\"\npath_image = \"./example.jpg\"\nplt.figure(figsize=(8, 3))\nplt.text(\n    0.5,\n    0.5,\n    \"My name is Tony Stank\",\n    ha=\"center\",\n    va=\"center\",\n    fontsize=30,\n)\nplt.axis(\"off\")\nplt.savefig(path_image, bbox_inches=\"tight\", pad_inches=0.1)\nplt.close()\n\n# %%\n# Then we can build a multimodal knowledge base with the image document.\n# The example is the same as before, just using the ``ImageReader`` and\n# ``DashScopeMultiModalEmbedding`` instead of the text counterparts.\n#\n\n\nasync def example_multimodal_rag() -> None:\n    \"\"\"The example of using multimodal RAG.\"\"\"\n    # Read the image using the ImageReader\n    reader = ImageReader()\n    docs = await reader(image_url=path_image)\n\n    # Create a knowledge base with the new image document\n    knowledge = SimpleKnowledge(\n        embedding_model=DashScopeMultiModalEmbedding(\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            model_name=\"multimodal-embedding-v1\",\n            dimensions=1024,\n        ),\n        embedding_store=QdrantStore(\n            location=\":memory:\",\n            collection_name=\"test_collection\",\n            dimensions=1024,\n        ),\n    )\n\n    await knowledge.add_documents(docs)\n\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"You are a helpful assistant named Friday.\",\n        model=DashScopeChatModel(\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            model_name=\"qwen-vl-max\",\n        ),\n        formatter=DashScopeChatFormatter(),\n        knowledge=knowledge,\n    )\n\n    await agent(\n        Msg(\n            \"user\",\n            \"What's my name?\",\n            \"user\",\n        ),\n    )\n\n    # Let's see the last message from the agent\n    print(\"\\nThe image is attached in the agent's memory:\")\n    print((await agent.memory.get_memory())[1])\n\n\nasyncio.run(example_multimodal_rag())\n\n# %%\n# We can see that the agent can answer the question based on the retrieved\n# image.\n"
  },
  {
    "path": "docs/tutorial/en/src/task_realtime.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _realtime:\n\nRealtime Agent\n====================\n\nThe **realtime** agent is designed to handle real-time interactions, such as\nvoice conversations or live chat sessions.\nThe realtime agent in AgentScope features:\n\n- Integration with OpenAI, DashScope, Gemini, and other realtime model APIs\n- Unified event interface to simplify interactions with different realtime models\n- Support for tool calling capabilities\n- Support for multi-agent interactions\n\n.. note:: The realtime agent is currently under active development. We welcome\n    community contributions, discussions, and feedback! If you're interested in\n    realtime agents, please join our discussion and development.\n\n\"\"\"\n\nimport asyncio\nimport os\nfrom agentscope.agent import RealtimeAgent\nfrom agentscope.realtime import (\n    DashScopeRealtimeModel,\n    OpenAIRealtimeModel,\n    GeminiRealtimeModel,\n)\n\n# %%\n# Creating Realtime Models\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# AgentScope currently supports the following realtime model APIs:\n#\n# .. list-table::\n#    :header-rows: 1\n#    :widths: 15 25 25 15 20\n#\n#    * - Provider\n#      - Class\n#      - Supported Models\n#      - Input Modalities\n#      - Tool Support\n#    * - DashScope\n#      - ``DashScopeRealtimeModel``\n#      - ``qwen3-omni-flash-realtime``\n#      - Text, Audio, Image\n#      - No\n#    * - OpenAI\n#      - ``OpenAIRealtimeModel``\n#      - ``gpt-4o-realtime-preview``\n#      - Text, Audio\n#      - Yes\n#    * - Gemini\n#      - ``GeminiRealtimeModel``\n#      - ``gemini-2.5-flash-native-audio-preview-09-2025``\n#      - Text, Audio, Image\n#      - Yes\n#\n#\n# Here are examples of initializing different realtime models:\n#\n# .. code-block:: python\n#     :caption: Example of initializing different realtime models\n#     # DashScope realtime model\n#     dashscope_model = DashScopeRealtimeModel(\n#         model_name=\"qwen3-omni-flash-realtime\",\n#         api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n#         voice=\"Cherry\",  # Options: \"Cherry\", \"Serena\", \"Ethan\", \"Chelsie\"\n#         enable_input_audio_transcription=True,\n#     )\n#\n#     # OpenAI realtime model\n#     openai_model = OpenAIRealtimeModel(\n#         model_name=\"gpt-4o-realtime-preview\",\n#         api_key=os.getenv(\"OPENAI_API_KEY\"),\n#         voice=\"alloy\",  # Options: \"alloy\", \"echo\", \"marin\", \"cedar\"\n#         enable_input_audio_transcription=True,\n#     )\n#\n#     # Gemini realtime model\n#     gemini_model = GeminiRealtimeModel(\n#         model_name=\"gemini-2.5-flash-native-audio-preview-09-2025\",\n#         api_key=os.getenv(\"GEMINI_API_KEY\"),\n#         voice=\"Puck\",  # Options: \"Puck\", \"Charon\", \"Kore\", \"Fenrir\"\n#         enable_input_audio_transcription=True,\n#     )\n#\n# The realtime model provides the following key methods:\n#\n# .. list-table::\n#    :header-rows: 1\n#    :widths: 30 70\n#\n#    * - Method\n#      - Description\n#    * - ``connect(outgoing_queue, instructions, tools)``\n#      - Establish WebSocket connection to the realtime model API\n#    * - ``disconnect()``\n#      - Close the WebSocket connection\n#    * - ``send(data)``\n#      - Send audio/text/image data to the realtime model for processing\n#\n# The ``outgoing_queue`` parameter in ``connect()`` is an asyncio queue used to\n# forward events from the realtime model to the outside (e.g., the agent or frontend).\n#\n#\n# Model Events Interface\n# -----------------------\n#\n# AgentScope provides a unified ``agentscope.realtime.ModelEvents`` interface to simplify\n# interactions with different realtime models. The following events are\n# supported:\n#\n# .. note:: The \"session\" in ModelEvents refers to the WebSocket connection\n#     session between the realtime model and the model API, not the session\n#     between the frontend and backend.\n#\n# .. list-table::\n#    :header-rows: 1\n#    :widths: 40 60\n#\n#    * - Event\n#      - Description\n#    * - ``ModelEvents.ModelSessionCreatedEvent``\n#      - Session is successfully created\n#    * - ``ModelEvents.ModelSessionEndedEvent``\n#      - Session has ended\n#    * - ``ModelEvents.ModelResponseCreatedEvent``\n#      - Model begins generating a response\n#    * - ``ModelEvents.ModelResponseDoneEvent``\n#      - Model finished generating a response\n#    * - ``ModelEvents.ModelResponseAudioDeltaEvent``\n#      - Streaming audio data chunk from the model\n#    * - ``ModelEvents.ModelResponseAudioDoneEvent``\n#      - Audio response is complete\n#    * - ``ModelEvents.ModelResponseAudioTranscriptDeltaEvent``\n#      - Streaming transcription chunk of audio response\n#    * - ``ModelEvents.ModelResponseAudioTranscriptDoneEvent``\n#      - Audio transcription is complete\n#    * - ``ModelEvents.ModelResponseToolUseDeltaEvent``\n#      - Streaming tool call parameters\n#    * - ``ModelEvents.ModelResponseToolUseDoneEvent``\n#      - Tool call parameters are complete\n#    * - ``ModelEvents.ModelInputTranscriptionDeltaEvent``\n#      - Streaming transcription chunk of user input\n#    * - ``ModelEvents.ModelInputTranscriptionDoneEvent``\n#      - User input transcription is complete\n#    * - ``ModelEvents.ModelInputStartedEvent``\n#      - Detected start of user audio input (VAD)\n#    * - ``ModelEvents.ModelInputDoneEvent``\n#      - Detected end of user audio input (VAD)\n#    * - ``ModelEvents.ModelErrorEvent``\n#      - An error occurred\n#\n#\n#\n# Creating a Realtime Agent\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# The ``RealtimeAgent`` serves as a bridge layer that:\n#\n# - Converts ``ModelEvents`` from realtime models into ``ServerEvents`` for\n#   frontend and other agents\n# - Receives ``ClientEvents`` from frontend or other agents and forwards them\n#   to the realtime model API\n# - Manages the agent's lifecycle and event queues\n#\n# Server and Client Events\n# -------------------------\n#\n# AgentScope provides unified ``ServerEvents`` and ``ClientEvents`` for\n# communication between backend and frontend:\n#\n# **ServerEvents** (Backend → Frontend):\n#\n# .. list-table::\n#    :header-rows: 1\n#    :widths: 40 60\n#\n#    * - Event\n#      - Description\n#    * - ``ServerEvents.ServerSessionCreatedEvent``\n#      - Session created in backend\n#    * - ``ServerEvents.ServerSessionUpdatedEvent``\n#      - Session updated in backend\n#    * - ``ServerEvents.ServerSessionEndedEvent``\n#      - Session ended in backend\n#    * - ``ServerEvents.AgentReadyEvent``\n#      - Agent is ready to receive inputs\n#    * - ``ServerEvents.AgentEndedEvent``\n#      - Agent has ended\n#    * - ``ServerEvents.AgentResponseCreatedEvent``\n#      - Agent starts generating response\n#    * - ``ServerEvents.AgentResponseDoneEvent``\n#      - Agent finished generating response\n#    * - ``ServerEvents.AgentResponseAudioDeltaEvent``\n#      - Streaming audio chunk from agent\n#    * - ``ServerEvents.AgentResponseAudioDoneEvent``\n#      - Audio response complete\n#    * - ``ServerEvents.AgentResponseAudioTranscriptDeltaEvent``\n#      - Streaming transcription of agent response\n#    * - ``ServerEvents.AgentResponseAudioTranscriptDoneEvent``\n#      - Transcription complete\n#    * - ``ServerEvents.AgentResponseToolUseDeltaEvent``\n#      - Streaming tool call data\n#    * - ``ServerEvents.AgentResponseToolUseDoneEvent``\n#      - Tool call complete\n#    * - ``ServerEvents.AgentResponseToolResultEvent``\n#      - Tool execution result\n#    * - ``ServerEvents.AgentInputTranscriptionDeltaEvent``\n#      - Streaming transcription of user input\n#    * - ``ServerEvents.AgentInputTranscriptionDoneEvent``\n#      - Input transcription complete\n#    * - ``ServerEvents.AgentInputStartedEvent``\n#      - User audio input started\n#    * - ``ServerEvents.AgentInputDoneEvent``\n#      - User audio input ended\n#    * - ``ServerEvents.AgentErrorEvent``\n#      - An error occurred\n#\n# **ClientEvents** (Frontend → Backend):\n#\n# .. list-table::\n#    :header-rows: 1\n#    :widths: 40 60\n#\n#    * - Event\n#      - Description\n#    * - ``ClientEvents.ClientSessionCreateEvent``\n#      - Create a new session with specified configuration\n#    * - ``ClientEvents.ClientSessionEndEvent``\n#      - End current session\n#    * - ``ClientEvents.ClientResponseCreateEvent``\n#      - Request agent to generate response immediately\n#    * - ``ClientEvents.ClientResponseCancelEvent``\n#      - Interrupt agent's current response\n#    * - ``ClientEvents.ClientTextAppendEvent``\n#      - Append text input\n#    * - ``ClientEvents.ClientAudioAppendEvent``\n#      - Append audio input\n#    * - ``ClientEvents.ClientAudioCommitEvent``\n#      - Commit audio input (signal end of input)\n#    * - ``ClientEvents.ClientImageAppendEvent``\n#      - Append image input\n#    * - ``ClientEvents.ClientToolResultEvent``\n#      - Send tool execution result\n#\n# Initializing a Realtime Agent\n# ------------------------------\n#\n# Here's how to create and use a realtime agent:\n\n\nasync def example_realtime_agent() -> None:\n    \"\"\"Example of creating and using a realtime agent.\"\"\"\n    agent = RealtimeAgent(\n        name=\"Friday\",\n        sys_prompt=\"You are a helpful assistant named Friday.\",\n        model=DashScopeRealtimeModel(\n            model_name=\"qwen3-omni-flash-realtime\",\n            api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n        ),\n    )\n\n    # Create a queue to receive messages from the agent\n    outgoing_queue = asyncio.Queue()\n\n    # The agent is now ready to handle inputs\n    # Handle outgoing messages in a separate task\n    async def handle_agent_messages():\n        while True:\n            event = await outgoing_queue.get()\n            # Process the event (e.g., send to frontend via WebSocket)\n            print(f\"Agent event: {event.type}\")\n\n    # Start the message handling task\n    asyncio.create_task(handle_agent_messages())\n\n    # Start the agent (establishes connection)\n    await agent.start(outgoing_queue)\n\n    # Stop the agent when done\n    await agent.stop()\n\n\n# %%\n# Starting Realtime Conversation\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# Now we can set up a realtime conversation between a user and a realtime agent.\n#\n# Here we take FastAPI as an example backend framework to demonstrate how to set up\n# a realtime conversation.\n#\n# **Backend Setup (Server-side):**\n#\n# The backend needs to:\n#\n# 1. Create a WebSocket endpoint to accept frontend connections\n# 2. Create a ``RealtimeAgent`` when the session starts\n# 3. Forward ``ClientEvents`` from frontend to the agent\n# 4. Forward ``ServerEvents`` from agent to the frontend\n#\n# .. code-block:: python\n#\n#     from fastapi import FastAPI, WebSocket\n#     from agentscope.agent import RealtimeAgent\n#     from agentscope.realtime import (\n#         DashScopeRealtimeModel,\n#         ClientEvents,\n#         ServerEvents,\n#     )\n#\n#     app = FastAPI()\n#\n#     @app.websocket(\"/ws/{user_id}/{session_id}\")\n#     async def websocket_endpoint(\n#         websocket: WebSocket,\n#         user_id: str,\n#         session_id: str,\n#     ):\n#         await websocket.accept()\n#\n#         # Create queue for agent messages\n#         frontend_queue = asyncio.Queue()\n#\n#         # Create agent\n#         agent = RealtimeAgent(\n#             name=\"Assistant\",\n#             sys_prompt=\"You are a helpful assistant.\",\n#             model=DashScopeRealtimeModel(\n#                 model_name=\"qwen3-omni-flash-realtime\",\n#                 api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n#             ),\n#         )\n#\n#         # Start agent\n#         await agent.start(frontend_queue)\n#\n#         # Forward messages from agent to frontend\n#         async def send_to_frontend():\n#             while True:\n#                 msg = await frontend_queue.get()\n#                 await websocket.send_json(msg.model_dump())\n#\n#         asyncio.create_task(send_to_frontend())\n#\n#         # Receive messages from frontend and forward to agent\n#         while True:\n#             data = await websocket.receive_json()\n#             client_event = ClientEvents.from_json(data)\n#             await agent.handle_input(client_event)\n#\n# **Frontend Setup (Client-side):**\n#\n# The frontend needs to:\n#\n# 1. Establish WebSocket connection to the backend\n# 2. Send ``CLIENT_SESSION_CREATE`` event to initialize the session\n# 3. Capture audio from microphone and send via ``CLIENT_AUDIO_APPEND`` events\n# 4. Receive and handle ``ServerEvents`` (e.g., play audio, display transcripts)\n#\n# .. code-block:: javascript\n#\n#     // Connect to WebSocket\n#     const ws = new WebSocket('ws://localhost:8000/ws/user1/session1');\n#\n#     ws.onopen = () => {\n#         // Create session\n#         ws.send(JSON.stringify({\n#             type: 'client_session_create',\n#             config: {\n#                 instructions: 'You are a helpful assistant.',\n#                 user_name: 'User1'\n#             }\n#         }));\n#     };\n#\n#     // Handle messages from backend\n#     ws.onmessage = (event) => {\n#         const data = JSON.parse(event.data);\n#         if (data.type === 'response_audio_delta') {\n#             // Play audio chunk\n#             playAudio(data.delta);\n#         }\n#     };\n#\n#     // Send audio data\n#     function sendAudioChunk(audioData) {\n#         ws.send(JSON.stringify({\n#             type: 'client_audio_append',\n#             session_id: 'session1',\n#             audio: audioData,  // base64 encoded\n#             format: { encoding: 'pcm16', sample_rate: 16000 }\n#         }));\n#     }\n#\n# For a complete working example, see\n# ``examples/agent/realtime_voice_agent/`` in the AgentScope repository.\n\n# %%\n# Multi-Agent Realtime Conversation\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# AgentScope supports multi-agent realtime interactions through the ``ChatRoom``\n# class.\n#\n# Note currently most realtime model APIs only support single-user interactions,\n# but AgentScope's architecture is designed to support multiple agents and users\n# when API capabilities expand.\n#\n# The Realtime ChatRoom\n# ----------------------------\n#\n# AgentScope introduces the ``ChatRoom`` class to manage multiple realtime\n# agents in a shared conversation space. The ChatRoom provides:\n#\n# - Centralized management of multiple ``RealtimeAgent`` instances\n# - Automatic message broadcasting between agents\n# - Unified message queue for frontend communication\n# - Lifecycle management for all agents in the room\n#\n# Using ChatRoom\n# --------------\n#\n# The usage of ``ChatRoom`` is similar to ``RealtimeAgent``:\n#\n\n\nasync def example_chat_room() -> None:\n    \"\"\"Example of using ChatRoom with multiple realtime agents.\"\"\"\n    from agentscope.pipeline import ChatRoom\n    from agentscope.agent import RealtimeAgent\n    from agentscope.realtime import DashScopeRealtimeModel\n\n    # Create multiple agents\n    agent1 = RealtimeAgent(\n        name=\"Agent1\",\n        sys_prompt=\"You are Agent1, a helpful assistant.\",\n        model=DashScopeRealtimeModel(\n            model_name=\"qwen3-omni-flash-realtime\",\n            api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n        ),\n    )\n\n    agent2 = RealtimeAgent(\n        name=\"Agent2\",\n        sys_prompt=\"You are Agent2, a helpful assistant.\",\n        model=DashScopeRealtimeModel(\n            model_name=\"qwen3-omni-flash-realtime\",\n            api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n        ),\n    )\n\n    # Create a chat room with multiple agents\n    chat_room = ChatRoom(agents=[agent1, agent2])\n\n    # Create queue to receive messages from all agents\n    outgoing_queue = asyncio.Queue()\n\n    # Start the chat room\n    await chat_room.start(outgoing_queue)\n\n    # Handle input from frontend\n    # The chat room will broadcast to all agents\n    from agentscope.realtime import ClientEvents\n\n    client_event = ClientEvents.ClientTextAppendEvent(\n        session_id=\"session1\",\n        text=\"Hello everyone!\",\n    )\n    await chat_room.handle_input(client_event)\n\n    # Stop the chat room when done\n    await chat_room.stop()\n\n\n# %%\n# Roadmap\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# The realtime agent feature is currently experimental and under active\n# development. The future plans include:\n#\n# - Support for more realtime model APIs\n# - Enhanced memory management for conversation history\n# - Comprehensive tool calling support across all providers\n# - Multi-user voice interaction support\n# - Improved VAD (Voice Activity Detection) configuration\n# - Better error handling and recovery mechanisms\n#\n# We welcome contributions and feedback from the community to help shape the\n# future of realtime agents in AgentScope!\n"
  },
  {
    "path": "docs/tutorial/en/src/task_state.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _state:\n\nState/Session Management\n=================================\n\nIn AgentScope, the **\"state\"** refers to the agent status in the running application, including its current system prompt, memory, context, equipped tools, and other information that **change over time**.\n\nTo manage the state of an application, AgentScope designs an automatic state registration system and session-level state management, which features:\n\n- Support **automatic state registration** for all variables inherited from ``StateModule``\n- Support **manual state registration** with custom serialization/deserialization methods\n- Support **session/application-level management**\n\"\"\"\nimport asyncio\nimport json\nimport os\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.module import StateModule\nfrom agentscope.session import JSONSession\nfrom agentscope.tool import Toolkit\n\n# %%\n# State Module\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# The ``StateModule`` class is the foundation for state management in AgentScope and provides three basic functions:\n#\n# .. list-table:: Methods of ``StateModule``\n#     :header-rows: 1\n#\n#     * - Method\n#       - Arguments\n#       - Description\n#     * - ``register_state``\n#       - | ``attr_name``,\n#         | ``custom_to_json`` (optional),\n#         | ``custom_from_json`` (optional)\n#       - Register an attribute as its state, with optional serialization/deserialization function.\n#     * - ``state_dict``\n#       - \\-\n#       - Get the state dictionary of current object\n#     * - ``load_state_dict``\n#       - | ``state_dict``,\n#         | ``strict`` (optional)\n#       - Load the state dictionary to current object\n#\n# Within an object of ``StateModule``, all the following attributes will be treated as parts of its state:\n#\n# - the **attributes** that inherit from ``StateModule``\n# - the **attributes** registered by the ``register_state`` method\n#\n# Note the ``StateModule`` supports **NESTED** serialization and deserialization:\n#\n\n\nclass ClassA(StateModule):\n    def __init__(self) -> None:\n        super().__init__()\n        self.cnt = 123\n        # register cnt attribute as state\n        self.register_state(\"cnt\")\n\n\nclass ClassB(StateModule):\n    def __init__(self) -> None:\n        super().__init__()\n\n        # attribute \"a\" inherits from StateModule\n        self.a = ClassA()\n\n        # register attribute \"c\" as state manually\n        self.c = \"Hello, world!\"\n        self.register_state(\"c\")\n\n\nobj_b = ClassB()\n\nprint(\"State of obj_b.a:\")\nprint(obj_b.a.state_dict())\n\nprint(\"\\nState of obj_b:\")\nprint(json.dumps(obj_b.state_dict(), indent=4))\n\n# %%\n# We can observe the state of ``obj_b`` contains the state of its attribute ``a`` automatically.\n#\n# In AgentScope, the ``AgentBase``, ``MemoryBase``, ``LongTermMemoryBase`` and ``Toolkit`` classes all inherit from ``StateModule``, thus supporting automatic and nested state management.\n#\n\n# Creating an agent\nagent = ReActAgent(\n    name=\"Friday\",\n    sys_prompt=\"You're a assistant named Friday.\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n    ),\n    formatter=DashScopeChatFormatter(),\n    memory=InMemoryMemory(),\n    toolkit=Toolkit(),\n)\n\ninitial_state = agent.state_dict()\n\nprint(\"Initial state of the agent:\")\nprint(json.dumps(initial_state, indent=4))\n\n# %%\n# Then we change its state by generating a reply message:\n#\n\n\nasync def example_agent_state() -> None:\n    \"\"\"Example of agent state management\"\"\"\n    await agent(Msg(\"user\", \"Hello, agent!\", \"user\"))\n\n    print(\"State of the agent after generating a reply:\")\n    print(json.dumps(agent.state_dict(), indent=4))\n\n\nasyncio.run(example_agent_state())\n\n# %%\n# Now we recover the state of the agent to its initial state:\n#\n\nagent.load_state_dict(initial_state)\n\nprint(\"State after loading the initial state:\")\nprint(json.dumps(agent.state_dict(), indent=4))\n\n# %%\n# Session Management\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# In AgentScope, a session refers to a collection of ``StateModule`` in an application, e.g. multiple agents.\n#\n# AgentScope provides a ``SessionBase`` class with two abstract methods for session\n# management: ``save_session_state`` and ``load_session_state``.\n# Developers can implement these methods with their own storage solution.\n#\n# In AgentScope, we provide a JSON based session class ``JSONSession`` that\n# stores/loads the session state in/from a JSON file named with the session ID.\n#\n# Here we show how to use the JSON based session management in AgentScope.\n#\n# Saving Session State\n# -----------------------------------------\n#\n\n# change the agent state by generating a reply message\nasyncio.run(example_agent_state())\n\nprint(\"\\nState of agent:\")\nprint(json.dumps(agent.state_dict(), indent=4))\n\n# %%\n# Then we save it to a session file:\n\n\nsession = JSONSession(\n    save_dir=\"./\",  # The dir used to save the session files\n)\n\n\nasync def example_session() -> None:\n    \"\"\"Example of session management.\"\"\"\n    await session.save_session_state(\n        session_id=\"user_1\",  # Use the name as the session id\n        agent=agent,\n    )\n\n    print(\"The saved state:\")\n    with open(\"./user_1.json\", \"r\", encoding=\"utf-8\") as f:\n        print(json.dumps(json.load(f), indent=4))\n\n\nasyncio.run(example_session())\n\n# %%\n# Loading Session State\n# -----------------------------------------\n# Now we load the session state from the saved file:\n\n\nasync def example_load_session() -> None:\n    \"\"\"Example of loading session state.\"\"\"\n\n    # we first clear the memory of the agent\n    await agent.memory.clear()\n\n    print(\"Current state of the agent:\")\n    print(json.dumps(agent.state_dict(), indent=4))\n\n    # then we load the session state\n    await session.load_session_state(\n        session_id=\"user_1\",\n        # The keyword argument must be the same as the one used in `save_session_state`\n        agent=agent,\n    )\n    print(\"After loading the session state:\")\n    print(json.dumps(agent.state_dict(), indent=4))\n\n\nasyncio.run(example_load_session())\n\n# %%\n# Now we can see the agent state is restored to the saved state.\n#\n"
  },
  {
    "path": "docs/tutorial/en/src/task_studio.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _studio:\n\nAgentScope Studio\n=========================\n\nAgentScope Studio is a local-deployed web application that\n\n- provides **project management** for the development of agent applications\n- provides native **visualization** for running applications and tracing\n- provides a **built-in agent** named \"Friday\" that supports secondary development\n\n.. note:: The Studio is under fast development, more features are coming soon!\n\n.. figure:: ../../_static/images/studio_home.webp\n    :width: 100%\n    :alt: AgentScope Studio Home Page\n    :class: bordered-image\n    :align: center\n\n    AgentScope Studio Home Page\n\nQuick Start\n~~~~~~~~~~~~~~~~~~~~~~~~\n\nAgentScope Studio is installed via ``npm``:\n\n.. code-block:: bash\n\n    npm install -g @agentscope/studio\n\n\nStart the Studio with the following command:\n\n.. code-block:: bash\n\n    as_studio\n\nTo connect your application to the Studio, use the ``agentscope.init`` function with the ``studio_url`` parameter:\n\n.. code-block:: python\n\n    import agentscope\n\n    agentscope.init(studio_url=\"http://localhost:3000\")\n\n    # your application code\n    ...\n\nThen, you can see your application in the Studio as follows:\n\n.. figure:: ../../_static/images/studio_project.webp\n    :width: 100%\n    :alt: Project management\n    :class: bordered-image\n    :align: center\n\n    Project management in AgentScope Studio\n\nThe details about your running application, e.g. token usage, model invocations, and tracing information, can all be viewed in the Studio.\n\n.. figure:: ../../_static/images/studio_run.webp\n    :width: 100%\n    :alt: AgentScope Studio run Page\n    :class: bordered-image\n    :align: center\n\n    Application visualization in AgentScope Studio\n\n\nFriday Agent\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nFriday is an experimental local-deployed agent built by AgentScope, aims at\n\n- answering questions about the AgentScope,\n- providing a quick secondary development environment for developers,\n- integrating all available features in AgentScope to build a more powerful agent, and\n- testing and integrating the advanced features in AgentScope.\n\n.. note:: We highly greet contributions from the community to improve Friday! Feel free to open issues or pull requests on our `GitHub repository <https://github.com/agentscope-ai/agentscope>`_.\n\nWe are keeping improving Friday, and currently it integrates the following features in AgentScope:\n\n.. list-table::\n    :header-rows: 1\n\n    * - Feature\n      - Status\n      - Further Reading\n      - Description\n    * - Meta tool\n      - ✅\n      - :ref:`tool`\n      - Group-wise tool management, and allow agent to change equipped tools by itself.\n    * - Agent Hook\n      - ✅\n      - :ref:`hook`\n      - Use hook to forward the printing messages to the frontend.\n    * - Agent Interruption\n      - ✅\n      - :ref:`agent`\n      - Allow use to interrupt the agent's reply process with post-processing.\n    * - Truncated Prompt\n      - ✅\n      - :ref:`prompt`\n      - Support to truncate the prompt with the preset max token limit.\n    * - State & Session Management\n      - ✅\n      - :ref:`state`\n      - Auto state management and session management for agents, maintaining the state between different runs.\n    * - Long-term Memory\n      - 🚧\n      - :ref:`memory`\n      - Support long-term memory management.\n\n\n\"\"\"\n"
  },
  {
    "path": "docs/tutorial/en/src/task_token.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _token:\n\nToken\n=========================\n\nAgentScope provides a token counter module under ``agentscope.token`` to\ncalculate the number of tokens in the given messages, allowing developers\nto estimate the number of tokens in a prompt before sending it to an API.\n\nSpecifically, the following token counters are available:\n\n.. list-table::\n    :header-rows: 1\n\n    * - Provider\n      - Class\n      - Support Image Data\n      - Support Tools\n    * - Anthropic\n      - ``AnthropicTokenCounter``\n      - ✅\n      - ✅\n    * - OpenAI\n      - ``OpenAITokenCounter``\n      - ✅\n      - ✅\n    * - Gemini\n      - ``GeminiTokenCounter``\n      - ✅\n      - ✅\n    * - HuggingFace\n      - ``HuggingFaceTokenCounter``\n      - Depends on the model\n      - Depends on the model\n\n.. tip:: The formatter module has integrated the token counters to support prompt truncation. Refer to the :ref:`prompt` section for more details.\n\n.. note:: For DashScope models, the dashscope library doesn't provide a token counting API. So we recommend using the HuggingFace token counter instead.\n\nWe show an example of using the OpenAI token counter to count the number of tokens:\n\"\"\"\n\nimport asyncio\nfrom agentscope.token import OpenAITokenCounter\n\n\nasync def example_token_counting():\n    # Example messages\n    messages = [\n        {\"role\": \"user\", \"content\": \"Hello!\"},\n        {\"role\": \"assistant\", \"content\": \"Hi, how can I help you?\"},\n    ]\n\n    # OpenAI token counting\n    openai_counter = OpenAITokenCounter(model_name=\"gpt-4.1\")\n    n_tokens = await openai_counter.count(messages)\n\n    print(f\"Number of tokens: {n_tokens}\")\n\n\nasyncio.run(example_token_counting())\n\n\n# %%\n# Further Reading\n# ------------------------------\n#\n# - :ref:`prompt`\n#\n"
  },
  {
    "path": "docs/tutorial/en/src/task_tool.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _tool:\n\nTool\n=========================\n\nTo ensure accurate and reliable tool parsing, AgentScope fully embraces the use of tools API with the following features:\n\n- Support **automatic** tool parsing from Python functions with their docstrings\n- Support both **synchronous and asynchronous** tool functions\n- Support **streaming** tool responses (either synchronous or asynchronous generators)\n- Support **dynamic extension** to the tool JSON Schema\n- Support **interrupting** the tool execution with proper signal handling\n- Support **autonomous tool management** by agents\n\nAll above features are implemented by the ``Toolkit`` class in AgentScope, which is responsible for managing tool functions and their execution.\n\n.. tip:: The support of MCP (Model Context Protocol) refers to the :ref:`mcp` section.\n\"\"\"\nimport asyncio\nimport inspect\nimport json\nfrom typing import Any, AsyncGenerator\n\nfrom pydantic import BaseModel, Field\n\nimport agentscope\nfrom agentscope.message import TextBlock, ToolUseBlock\nfrom agentscope.tool import ToolResponse, Toolkit, execute_python_code\n\n\n# %%\n# Tool Function\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# In AgentScope, a tool function is a Python function that\n#\n# - returns a ``ToolResponse`` object or a generator that yields ``ToolResponse`` objects\n# - has a docstring that describes the tool's functionality and parameters\n#\n# A template of a tool function is as follows:\n\n\ndef tool_function(a: int, b: str) -> ToolResponse:\n    \"\"\"{function description}\n\n    Args:\n        a (int):\n            {description of the first parameter}\n        b (str):\n            {description of the second parameter}\n    \"\"\"\n\n\n# %%\n# .. tip:: Instance method and class method can also be used as tool functions, and the ``self`` and ``cls`` parameters will be ignored.\n#\n# AgentScope provides several built-in tool functions under the ``agentscope.tool`` module, such as ``execute_python_code``, ``execute_shell_command`` and text file write/read functions.\n#\n\nprint(\"Built-in Tool Functions:\")\nfor _ in agentscope.tool.__all__:\n    if _ not in [\"Toolkit\", \"ToolResponse\"]:\n        print(_)\n\n# %%\n# Toolkit\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# The ``Toolkit`` class is designed to manage tool functions, extracting their JSON Schema from docstrings and providing a unified interface for tool execution.\n#\n# Basic Usage\n# ------------------------------\n# The basic functionality of the ``Toolkit`` class is to register tool functions and execute them.\n#\n\n\n# Prepare a custom tool function\nasync def my_search(query: str, api_key: str) -> ToolResponse:\n    \"\"\"A simple example tool function.\n\n    Args:\n        query (str):\n            The search query.\n        api_key (str):\n            The API key for authentication.\n    \"\"\"\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=f\"Searching for '{query}' with API key '{api_key}'\",\n            ),\n        ],\n    )\n\n\n# Register the tool function in a toolkit\ntoolkit = Toolkit()\ntoolkit.register_tool_function(my_search)\n\n# %%\n# When registering a tool function, you can get its JSON Schema by calling the ``get_json_schemas`` method.\n#\n\nprint(\"Tool JSON Schemas:\")\nprint(json.dumps(toolkit.get_json_schemas(), indent=4, ensure_ascii=False))\n\n# %%\n# ``Toolkit`` also allows developers to preset the arguments for tool functions, especially useful for API keys or other sensitive information.\n#\n\n# Clear the toolkit first\ntoolkit.clear()\n\n# Register tool function with preset keyword arguments\ntoolkit.register_tool_function(my_search, preset_kwargs={\"api_key\": \"xxx\"})\n\nprint(\"Tool JSON Schemas with Preset Arguments:\")\nprint(json.dumps(toolkit.get_json_schemas(), indent=4, ensure_ascii=False))\n\n# %%\n# In ``Toolkit``, the ``call_tool_function`` method takes a tool use block as input and executes the corresponding tool function, returning **a unified asynchronous generator** that yields ``ToolResponse`` objects.\n#\n\n\nasync def example_tool_execution() -> None:\n    \"\"\"Example of executing a tool call.\"\"\"\n    res = await toolkit.call_tool_function(\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"123\",\n            name=\"my_search\",\n            input={\"query\": \"AgentScope\"},\n        ),\n    )\n\n    # Only one tool response is expected in this case\n    print(\"Tool Response:\")\n    async for tool_response in res:\n        print(tool_response)\n\n\nasyncio.run(example_tool_execution())\n\n# %%\n# Extending JSON Schema Dynamically\n# --------------------------------------\n#\n# Toolkit allows to extend the JSON schemas of tool functions dynamically by calling the ``set_extended_model`` method.\n# Such feature allows to add more parameters to the tool function without modifying its original definition.\n#\n# .. tip:: Related scenarios include dynamic :ref:`structured-output` and CoT (Chain of Thought) reasoning\n#\n# .. note:: The function to be extended should accept variable keyword arguments (``**kwargs``), so that the additional fields can be passed to it.\n#\n# Taking the CoT reasoning as an example, we can extend all tool functions with a ``thinking`` field, allowing the agent to summarize the current state and then decide what to do next.\n#\n\n\n# Example tool function\ndef tool_function(**kwargs: Any) -> ToolResponse:\n    \"\"\"A tool function\"\"\"\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=f\"Received parameters: {kwargs}\",\n            ),\n        ],\n    )\n\n\n# Add a thinking field so that the agent could think before giving the other parameters.\nclass ThinkingModel(BaseModel):\n    \"\"\"A Pydantic model for additional fields.\"\"\"\n\n    thinking: str = Field(\n        description=\"Summarize the current state and decide what to do next.\",\n    )\n\n\n# Register\ntoolkit.set_extended_model(\"my_search\", ThinkingModel)\n\nprint(\"The extended JSON Schema:\")\nprint(json.dumps(toolkit.get_json_schemas(), indent=4, ensure_ascii=False))\n\n# %%\n# Interrupting Tool Execution\n# ------------------------------\n# The ``Toolkit`` class supports **execution interruption** of **async tool functions** and provides a comprehensive **agent-oriented post-processing mechanism**.\n# Such interruption is implemented based on the asyncio cancellation mechanism, and the post-processing varies depending on the return type of tool function.\n#\n# .. note:: For synchronous tool functions, their execution cannot be interrupted by asyncio cancellation. So the interruption is handled within the agent rather than the toolkit.\n#  Refer to the :ref:`agent` section for more information.\n#\n# Specifically, if the tool function returns a ``ToolResponse`` object, a predefined ``ToolResponse`` object with an interrupted message will be yielded.\n# So that the agent can observe the interruption and handle it accordingly.\n# Besides, a flag ``is_interrupted`` will be set to ``True`` in the response, and the external caller can decide whether to throw the ``CancelledError`` exception to the outer layer.\n#\n# An example of async tool function that can be interrupted is as follows:\n#\n\n\nasync def non_streaming_function() -> ToolResponse:\n    \"\"\"A non-streaming tool function that can be interrupted.\"\"\"\n    await asyncio.sleep(1)  # Simulate a long-running task\n\n    # Fake interruption for demonstration\n    raise asyncio.CancelledError()\n\n    # The following code won't be executed due to the cancellation\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=\"Run successfully!\",\n            ),\n        ],\n    )\n\n\nasync def example_tool_interruption() -> None:\n    \"\"\"Example of tool interruption.\"\"\"\n    toolkit = Toolkit()\n    toolkit.register_tool_function(non_streaming_function)\n    res = await toolkit.call_tool_function(\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"123\",\n            name=\"non_streaming_function\",\n            input={},\n        ),\n    )\n\n    async for tool_response in res:\n        print(\"Tool Response:\")\n        print(tool_response)\n        print(\"The interrupted flag:\")\n        print(tool_response.is_interrupted)\n\n\nasyncio.run(example_tool_interruption())\n\n# %%\n# For streaming tool functions, which returns an asynchronous generator, the ``Toolkit`` will attach the interrupted message to the previous chunk of the response.\n# By this way, the agent can observe what the tool has returned before the interruption.\n#\n# The example of interrupting a streaming tool function is as follows:\n#\n\n\nasync def streaming_function() -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"A streaming tool function that can be interrupted.\"\"\"\n    # Simulate a chunk of response\n    yield ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=\"1234\",\n            ),\n        ],\n        stream=True,\n    )\n\n    # Simulate interruption\n    raise asyncio.CancelledError()\n\n    # The following code won't be executed due to the cancellation\n    yield ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=\"123456789\",\n            ),\n        ],\n    )\n\n\nasync def example_streaming_tool_interruption() -> None:\n    \"\"\"Example of streaming tool interruption.\"\"\"\n    toolkit = Toolkit()\n    toolkit.register_tool_function(streaming_function)\n\n    res = await toolkit.call_tool_function(\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"xxx\",\n            name=\"streaming_function\",\n            input={},\n        ),\n    )\n\n    i = 0\n    async for tool_response in res:\n        print(f\"Chunk {i}:\")\n        print(tool_response)\n        print(\"The interrupted flag: \", tool_response.is_interrupted, \"\\n\")\n        i += 1\n\n\nasyncio.run(example_streaming_tool_interruption())\n\n# %%\n# Automatic Tool Management\n# -------------------------------------\n# .. image:: https://img.alicdn.com/imgextra/i3/O1CN013cvRpO27MfesMsTeh_!!6000000007783-2-tps-840-521.png\n#     :width: 100%\n#     :align: center\n#     :alt: Automatic Tool Management\n#\n#\n# The ``Toolkit`` class supports **automatic tool management** by introducing the concept of **tool group**, as well as a **meta tool function** named ``reset_equipped_tools``.\n#\n# The tool group is a set of related tool functions, e.g. browser-use tools, map services tools, etc., which will be managed together.\n# Only the tools in the activated groups will be visible to agents, i.e. accessible by the ``toolkit.get_json_schemas()`` method.\n#\n# Note there is a special group called ``basic``, which is always activated and the tools registered without specifying the group name will be added to this group by default.\n#\n# .. tip:: The ``basic`` group ensures that the basic usage of tools won't be affected by the group features if you don't need them.\n#\n# Now we try to create a tool group named ``browser_use``, which contains some web browsing tools.\n#\n\n\ndef navigate(url: str) -> ToolResponse:\n    \"\"\"Navigate to a web page.\n\n    Args:\n        url (str):\n            The URL of the web page to navigate to.\n    \"\"\"\n    pass\n\n\ndef click_element(element_id: str) -> ToolResponse:\n    \"\"\"Click an element on the web page.\n\n    Args:\n        element_id (str):\n            The ID of the element to click.\n    \"\"\"\n    pass\n\n\ntoolkit = Toolkit()\n\n# Create a tool group named browser_use\ntoolkit.create_tool_group(\n    group_name=\"browser_use\",\n    description=\"The tool functions for web browsing.\",\n    active=False,\n    # The notes when using these tools\n    notes=\"\"\"1. Use ``navigate`` to open a web page.\n2. When requiring user authentication, ask the user for the credentials\n3. ...\"\"\",\n)\n\ntoolkit.register_tool_function(navigate, group_name=\"browser_use\")\ntoolkit.register_tool_function(click_element, group_name=\"browser_use\")\n\n# We can also register some basic tools\ntoolkit.register_tool_function(execute_python_code)\n\n# %%\n# If we check the tools JSON schema, we can only see the ``execute_python_code`` tool, because the ``browser_use`` group is not activated yet:\n\nprint(\"Tool JSON Schemas with Group:\")\nprint(json.dumps(toolkit.get_json_schemas(), indent=4, ensure_ascii=False))\n\n# %%\n# Use the ``update_tool_groups`` method to activate or deactivate tool groups:\n\ntoolkit.update_tool_groups(group_names=[\"browser_use\"], active=True)\n\nprint(\"Tool JSON Schemas with Group:\")\nprint(json.dumps(toolkit.get_json_schemas(), indent=4, ensure_ascii=False))\n\n# %%\n# Additionally, ``Toolkit`` provides a meta tool function named ``reset_equipped_tools``, taking the current group names as the argument to indicate which groups to activate:\n#\n# .. note:: In ``ReActAgent`` class, you can enable the meta tool function by setting ``enable_meta_tool=True`` in the constructor.\n#\n\n# Register the meta tool function\ntoolkit.register_tool_function(toolkit.reset_equipped_tools)\n\nreset_equipped = next(\n    tool\n    for tool in toolkit.get_json_schemas()\n    if tool[\"function\"][\"name\"] == \"reset_equipped_tools\"\n)\nprint(\"JSON schema of the ``reset_equipped_tools`` function:\")\nprint(\n    json.dumps(\n        reset_equipped,\n        indent=4,\n        ensure_ascii=False,\n    ),\n)\n\n# %%\n# When agent calls the ``reset_equipped_tools`` function, the corresponding tool groups will be activated, and the tool response will\n# contain the notes of the activated tool groups.\n#\n\n\nasync def mock_agent_reset_tools() -> None:\n    \"\"\"Mock agent to reset tool groups.\"\"\"\n    # Call the meta tool function\n    res = await toolkit.call_tool_function(\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"154\",\n            name=\"reset_equipped_tools\",\n            input={\n                \"browser_user\": True,\n            },\n        ),\n    )\n\n    async for tool_response in res:\n        print(\"Text content in tool Response:\")\n        print(tool_response)\n\n\nasyncio.run(mock_agent_reset_tools())\n\n# %%\n# The toolkit also provides a method to gather the notes of the activated tool groups, and you can assemble it into your agent's system prompt.\n#\n# .. tip:: The automatic tool management feature is already implemented in the ``ReActAgent`` class, refer to the :ref:`agent` section for more details.\n#\n\n# Create one more tool group\ntoolkit.create_tool_group(\n    group_name=\"map_service\",\n    description=\"The google map service tools.\",\n    active=True,\n    notes=\"\"\"1. Use ``get_location`` to get the location of a place.\n2. ...\"\"\",\n)\n\nprint(\"The gathered notes of the activated tool groups:\")\nprint(toolkit.get_activated_notes())\n\n# %%\n# Further Reading\n# ---------------------\n# - :ref:`agent`\n# - :ref:`state`\n# - :ref:`mcp`\n#\n"
  },
  {
    "path": "docs/tutorial/en/src/task_tracing.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _tracing:\n\nTracing\n==============================\n\nAgentScope implements OpenTelemetry-based tracing to monitor and debug the\nexecution of agent applications, which features\n\n- Provide built-in tracing for LLM, tool, agent, formatter, etc.\n- Support error and exception tracking\n- Provide native tracing **visualization** in AgentScope Studio\n- Support connecting to **third-party platforms** like Alibaba Cloud CloudMonitor, `Arize-Phoenix <https://github.com/Arize-ai/phoenix>`_, `Langfuse <https://langfuse.com/>`_, etc.\n\nSetting Up\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n.. note:: Connecting to the :ref:`studio` or third-party tracing endpoint should be done at the beginning of your application by the ``agentscope.init`` function.\n\nAgentScope Studio\n---------------------------------------\n\n.. figure:: ../../_static/images/studio_tracing.webp\n    :width: 100%\n    :alt: AgentScope Studio tracing Page\n    :class: bordered-image\n    :align: center\n\n    *Tracing in AgentScope Studio*\n\nWhen connecting to AgentScope Studio, just provide ``studio_url`` parameter in ``agentscope.init`` function.\n\n.. code-block:: python\n\n    import agentscope\n\n    agentscope.init(studio_url=\"http://xxx:port\")\n\n\nThird-party Platforms\n---------------------------------------\n\nTo connect to third-party tracing platforms, set the ``tracing_url`` parameter in the ``agentscope.init`` function.\nThe ``tracing_url`` is the URL of your OpenTelemetry collector or any compatible backend that supports OTLP (OpenTelemetry Protocol).\n\n.. code-block:: python\n\n    import agentscope\n\n    # Connect to OpenTelemetry-compatible backends\n    agentscope.init(tracing_url=\"https://your-tracing-backend:port/traces\")\n\nTaking Alibaba Cloud CloudMonitor, Arize-Phoenix, and Langfuse as examples:\n\n**Alibaba Cloud CloudMonitor**: A fully-managed observability platform.\n\n.. code-block:: python\n    :caption: Connect to Alibaba Cloud CloudMonitor\n\n    agentscope.init(tracing_url=\"https://tracing-cn-hangzhou.arms.aliyuncs.com/adapt_xxx/api/otlp/traces\")\n\n.. tip::\n    **Get your Endpoint:** In the `ARMS Console <https://arms.console.aliyun.com/>`_ under **Access Center** > **OpenTelemetry**,\n    select the **Public Endpoint** matching your deployment region. Customize your app name via the ``OTEL_SERVICE_NAME`` environment variable.\n    Alibaba Cloud CloudMonitor provides zero-code instrumentation through `LoongSuite <https://github.com/alibaba/loongsuite-python-agent>`_ agent.\n    Learn more in the `CloudMonitor Documentation <https://www.alibabacloud.com/help/en/cms/cloudmonitor-2-0/user-guide/model-application>`_.\n\n**Arize-Phoenix**: You need to set the ``PHOENIX_API_KEY`` in your environment variables.\n\n.. code-block:: python\n    :caption: Connect to Arize Phoenix\n\n    # Arize Phoenix Integration\n    import os\n\n    PHOENIX_API_KEY = os.environ.get(\"PHOENIX_API_KEY\")\n    os.environ[\"OTEL_EXPORTER_OTLP_HEADERS\"] = f\"api_key={PHOENIX_API_KEY}\"\n\n    agentscope.init(tracing_url=\"https://app.phoenix.arize.com/v1/traces\")\n\n**LangFuse**: You need to set the ``LANGFUSE_PUBLIC_KEY`` and\n``LANGFUSE_SECRET_KEY`` in your environment variables. The authorization\nheader is constructed using these keys.\n\n.. code-block:: python\n    :caption: Connect to LangFuse\n\n    import os, base64\n\n    LANGFUSE_PUBLIC_KEY = os.environ[\"LANGFUSE_PUBLIC_KEY\"]\n    LANGFUSE_SECRET_KEY = os.environ[\"LANGFUSE_SECRET_KEY\"]\n    LANGFUSE_AUTH_STRING = f\"{LANGFUSE_PUBLIC_KEY}:{LANGFUSE_SECRET_KEY}\"\n\n    LANGFUSE_AUTH = base64.b64encode(LANGFUSE_AUTH_STRING.encode(\"utf-8\")).decode(\"ascii\")\n    os.environ[\"OTEL_EXPORTER_OTLP_HEADERS\"] = f\"Authorization=Basic {LANGFUSE_AUTH}\"\n\n    # EU data region\n    agentscope.init(tracing_url=\"https://cloud.langfuse.com/api/public/otel/v1/traces\")\n    # US data region\n    # agentscope.init(tracing_url=\"https://us.cloud.langfuse.com/api/public/otel/v1/traces\")\n\n\nCustomizing Tracing\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nAs stated above, the tracing in AgentScope is implemented based on OpenTelemetry.\nThat means your own tracing code implemented by OpenTelemetry sdk is compatible with\nAgentScope natively.\n\nBesides, AgentScope has built-in the following decorators to trace the corresponding modules:\n\n- ``@trace_llm``: Trace the ``__call__`` function of classes inherit from ``ChatModelBase``\n- ``@trace_reply``: Trace the ``reply`` function of classes inherit from ``AgentBase``\n- ``@trace_format``: Trace the ``format`` function of classes inherit from ``FormatterBase``\n- ``@trace``: Trace general functions\n\n\nTracing LLMs\n----------------------------------------\n\n\nThe ``@trace_llm`` decorator is to trace the ``__call__`` function of ``ChatModelBase`` classes.\n\n.. note:: Your LLM class must inherit from ``ChatModelBase``\n\n.. code-block:: python\n    :caption: Tracing new ChatModel class\n\n    class ExampleChatModel(ChatModelBase):\n        \\\"\\\"\\\"An example Model\\\"\\\"\\\"\n\n        ...\n\n        @trace_llm\n        async def __call__(\n            self,\n            *args: Any,\n            **kwargs: Any,\n        ) -> AsyncGenerator[ChatResponse, None] | ChatResponse:\n            \\\"\\\"\\\"LLM call\\\"\\\"\\\"\n            ...\n\n\nTracing Agent\n----------------------------------------\n\nThe ``@trace_reply`` decorator is for agent implementations and tracing the `reply` function.\n\n.. note:: Your agent class must inherit from ``AgentBase``\n\n.. code-block:: python\n    :caption: Tracing new Agent class\n\n    class ExampleAgent(AgentBase):\n        \\\"\\\"\\\"An example agent class\\\"\\\"\\\"\n\n        @tracer_reply\n        async def reply(self, *args: Any, **kwargs: Any) -> Msg:\n            \\\"\\\"\\\"Reply to the message.\\\"\\\"\\\"\n            ...\n\n\nTracing Formatter\n----------------------------------------\nThe ``@trace_format`` decorator is for formatters implementations and tracing the `format` function.\n\n.. note:: Your formatter class must inherit from ``FormatterBase``\n\n.. code-block:: python\n    :caption: Tracing new Formatter class\n\n    class ExampleFormatter(FormatterBase):\n            \\\"\\\"\\\"A simple example formatter class\\\"\\\"\\\"\n\n            @trace_format\n            async def format(self, *args: Any, **kwargs: Any) -> list[dict]:\n                \\\"\\\"\\\"Example formatting\\\"\\\"\\\"\n\n\nGeneral Tracing\n----------------------------------------\n\nThe ``@trace`` decorator is different from the above decorators, as it is a general-purpose tracing decorator that can be applied to any function.\nIt requires a `name` parameter to identify the traced function, and can trace various types of functions, including:\n\n- synchronous functions\n- synchronous generator functions\n- asynchronous functions\n- asynchronous generator functions\n\n.. code-block:: python\n    :caption: General tracing example\n\n    # 1. Synchronous function\n    @trace(name='simple_function')\n    def simple_function(name: str, age: int) -> str:\n        \\\"\\\"\\\"A simple function with automatic tracing.\\\"\\\"\\\"\n        return f\"Hello, {name}! You are {age} years old.\"\n\n    # 2. Synchronous generator function\n    @trace(name='number_generator')\n    def number_generator(n: int) -> Generator[int, None, None]:\n        \\\"\\\"\\\"Generate numbers from 0 to n-1.\\\"\\\"\\\"\n        for i in range(n):\n            yield i\n\n    # 3. Asynchronous function\n    @trace(name='async_function')\n    async def async_function(data: dict) -> dict:\n        \\\"\\\"\\\"Process data asynchronously.\\\"\\\"\\\"\n        return {\"processed\": data}\n\n    # 4. Asynchronous generator function\n    @trace(name='async_stream')\n    async def async_stream(n: int) -> AsyncGenerator[str, None]:\n        \\\"\\\"\\\"Generate stream of data asynchronously.\\\"\\\"\\\"\n        for i in range(n):\n            yield f\"data_{i}\"\n\n\"\"\"\n"
  },
  {
    "path": "docs/tutorial/en/src/task_tts.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _tts:\n\nTTS\n====================\n\nAgentScope provides a unified interface for Text-to-Speech (TTS) models across multiple API providers.\nThis tutorial demonstrates how to use TTS models in AgentScope.\n\nAgentScope supports the following TTS APIs:\n\n.. list-table:: Built-in TTS Models\n    :header-rows: 1\n\n    * - API\n      - Class\n      - Streaming Input\n      - Non-Streaming Input\n      - Streaming Output\n      - Non-Streaming Output\n    * - DashScope Realtime API\n      - ``DashScopeRealtimeTTSModel``\n      - ✅\n      - ✅\n      - ✅\n      - ✅\n    * - DashScope CosyVoice Realtime API\n      - ``DashScopeCosyVoiceRealtimeTTSModel``\n      - ✅\n      - ✅\n      - ✅\n      - ✅\n    * - DashScope API\n      - ``DashScopeTTSModel``\n      - ❌\n      - ✅\n      - ✅\n      - ✅\n    * - DashScope CosyVoice API\n      - ``DashScopeCosyVoiceTTSModel``\n      - ❌\n      - ✅\n      - ✅\n      - ✅\n    * - OpenAI API\n      - ``OpenAITTSModel``\n      - ❌\n      - ✅\n      - ✅\n      - ✅\n    * - Gemini API\n      - ``GeminiTTSModel``\n      - ❌\n      - ✅\n      - ✅\n      - ✅\n\n.. note:: The streaming input and output in AgentScope TTS models are all accumulative.\n\n**Choosing the Right Model:**\n\n- **Use Non-Realtime TTS** when you have complete text ready (e.g., pre-written\n  responses, complete LLM outputs)\n- **Use Realtime TTS** when text is generated progressively (e.g., streaming\n  LLM responses) for lower latency\n\n\"\"\"\n\nimport asyncio\nimport os\n\nfrom agentscope.agent import ReActAgent, UserAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tts import (\n    DashScopeRealtimeTTSModel,\n    DashScopeTTSModel,\n)\n\n# %%\n# Non-Realtime TTS\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# Non-realtime TTS models process complete text inputs and are the simplest\n# to use. You can directly call their ``synthesize()`` method.\n#\n# Taking DashScope TTS model as an example:\n\n\nasync def example_non_realtime_tts() -> None:\n    \"\"\"A basic example of using non-realtime TTS models.\"\"\"\n    # Example with DashScope TTS\n    tts_model = DashScopeTTSModel(\n        api_key=os.environ.get(\"DASHSCOPE_API_KEY\", \"\"),\n        model_name=\"qwen3-tts-flash\",\n        voice=\"Cherry\",\n        stream=False,  # Non-streaming output\n    )\n\n    msg = Msg(\n        name=\"assistant\",\n        content=\"Hello, this is DashScope TTS.\",\n        role=\"assistant\",\n    )\n\n    # Directly synthesize without connecting\n    tts_response = await tts_model.synthesize(msg)\n\n    # tts_response.content contains an audio block with base64-encoded audio data\n    print(\n        \"The length of audio data:\",\n        len(tts_response.content[\"source\"][\"data\"]),\n    )\n\n\nasyncio.run(example_non_realtime_tts())\n\n# %%\n# **Streaming Output for Lower Latency:**\n#\n# When ``stream=True``, the model returns audio chunks progressively, allowing\n# you to start playback before synthesis completes. This reduces perceived latency.\n#\n\n\nasync def example_non_realtime_tts_streaming() -> None:\n    \"\"\"An example of using non-realtime TTS models with streaming output.\"\"\"\n    # Example with DashScope TTS with streaming output\n    tts_model = DashScopeTTSModel(\n        api_key=os.environ.get(\"DASHSCOPE_API_KEY\", \"\"),\n        model_name=\"qwen3-tts-flash\",\n        voice=\"Cherry\",\n        stream=True,  # Enable streaming output\n    )\n\n    msg = Msg(\n        name=\"assistant\",\n        content=\"Hello, this is DashScope TTS with streaming output.\",\n        role=\"assistant\",\n    )\n\n    # Synthesize and receive an async generator for streaming output\n    async for tts_response in await tts_model.synthesize(msg):\n        # Process each audio chunk as it arrives\n        print(\n            \"Received audio chunk of length:\",\n            len(tts_response.content[\"source\"][\"data\"]),\n        )\n\n\nasyncio.run(example_non_realtime_tts_streaming())\n\n\n# %%\n# Realtime TTS\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# Realtime TTS models are designed for scenarios where text is generated\n# incrementally, such as streaming LLM responses. This enables the lowest\n# possible latency by starting audio synthesis before the complete text is ready.\n#\n# **Key Concepts:**\n#\n# - **Stateful Processing**: Realtime TTS maintains state for a single streaming\n#   session, identified by ``msg.id``. Only one streaming session can be active\n#   at a time.\n# - **Two Methods**:\n#\n#   - ``push(msg)``: Non-blocking method that submits text chunks and returns\n#     immediately. May return partial audio if available.\n#   - ``synthesize(msg)``: Blocking method that finalizes the session and returns\n#     all remaining audio. When ``stream=True``, it returns an async generator.\n#\n# .. code-block:: python\n#\n#     async def example_realtime_tts_streaming():\n#         tts_model = DashScopeRealtimeTTSModel(\n#             api_key=os.environ.get(\"DASHSCOPE_API_KEY\", \"\"),\n#             model_name=\"qwen3-tts-flash-realtime\",\n#             voice=\"Cherry\",\n#             stream=False,\n#         )\n#\n#         # realtime tts model received accumulative text chunks\n#         res = await tts_model.push(msg_chunk_1)  # non-blocking\n#         res = await tts_model.push(msg_chunk_2)  # non-blocking\n#         ...\n#         res = await tts_model.synthesize(final_msg)  # blocking, get all remaining audio\n#\n# When setting ``stream=True`` during initialization, the ``synthesize()`` method returns an async generator of ``TTSResponse`` objects, allowing you to process audio chunks as they arrive.\n#\n#\n# Integrating with ReActAgent\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope agents can automatically synthesize their responses to speech\n# when provided with a TTS model. This works seamlessly with both realtime\n# and non-realtime TTS models.\n#\n# **How It Works:**\n#\n# 1. The agent generates a text response (potentially streamed from an LLM)\n# 2. The TTS model synthesizes the text to audio automatically\n# 3. The synthesized audio is attached to the ``speech`` field of the ``Msg`` object\n# 4. The audio is played during the agent's ``self.print()`` method\n#\n\n\nasync def example_agent_with_tts() -> None:\n    \"\"\"An example of using TTS with ReActAgent.\"\"\"\n    # Create an agent with TTS enabled\n    agent = ReActAgent(\n        name=\"Assistant\",\n        sys_prompt=\"You are a helpful assistant.\",\n        model=DashScopeChatModel(\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\", \"\"),\n            model_name=\"qwen-max\",\n            stream=True,\n        ),\n        formatter=DashScopeChatFormatter(),\n        # Enable TTS\n        tts_model=DashScopeRealtimeTTSModel(\n            api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n            model_name=\"qwen3-tts-flash-realtime\",\n            voice=\"Cherry\",\n        ),\n    )\n    user = UserAgent(\"User\")\n\n    # Build a conversation just like normal\n    msg = None\n    while True:\n        msg = await agent(msg)\n        msg = await user(msg)\n        if msg.get_text_content() == \"exit\":\n            break\n\n\n# %%\n# Customizing TTS Model\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# You can create custom TTS implementations by inheriting from ``TTSModelBase``.\n# The base class provides a flexible interface for both realtime and non-realtime\n# TTS models.\n# We use an attribute ``supports_streaming_input`` to indicate if the TTS model is realtime or not.\n#\n# For realtime TTS models, you need to implement the ``connect``, ``close``, ``push`` and ``synthesize`` methods to handle the lifecycle and streaming input.\n#\n# While for non-realtime TTS models, you only need to implement the ``synthesize`` method.\n#\n# Further Reading\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# - :ref:`agent` - Learn more about agents in AgentScope\n# - :ref:`message` - Understand message format in AgentScope\n# - API Reference: :class:`agentscope.tts.TTSModelBase`\n#\n"
  },
  {
    "path": "docs/tutorial/en/src/task_tuner.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _tuner:\n\nTuner\n=================\n\nAgentScope provides the ``tuner`` module for training agent applications using reinforcement learning (RL).\nThis tutorial will guide you through how to leverage the ``tuner`` module to improve agent performance on specific tasks, including:\n\n- Introducing the core components of the ``tuner`` module\n- Demonstrating the key code required for the tuning workflow\n- Showing how to configure and run the tuning process\n\nMain Components\n~~~~~~~~~~~~~~~~~~~\nThe ``tuner`` module introduces three core components essential for RL-based agent training:\n\n- **Task Dataset**: A collection of tasks for training and evaluating the agent.\n- **Workflow Function**: Encapsulates the agent's logic to be tuned.\n- **Judge Function**: Evaluates the agent's performance on tasks and provides reward signals for tuning.\n\nIn addition, ``tuner`` provides several configuration classes for customizing the tuning process, including:\n\n- **TunerModelConfig**: Model configurations for tuning purposes.\n- **AlgorithmConfig**: Specifies the RL algorithm (e.g., GRPO, PPO) and its parameters.\n\nImplementation\n~~~~~~~~~~~~~~~~~~~\nThis section demonstrates how to use ``tuner`` to train a simple math agent.\n\nTask Dataset\n--------------------\nThe task dataset contains tasks for training and evaluating your agent.\n\nYou dataset should follow the Huggingface `datasets <https://huggingface.co/docs/datasets/quickstart>`_ format, which can be loaded with ``datasets.load_dataset``. For example:\n\n.. code-block:: text\n\n    my_dataset/\n        ├── train.jsonl  # training samples\n        └── test.jsonl   # evaluation samples\n\nSuppose your `train.jsonl` contains:\n\n.. code-block:: json\n\n    {\"question\": \"What is 2 + 2?\", \"answer\": \"4\"}\n    {\"question\": \"What is 4 + 4?\", \"answer\": \"8\"}\n\nBefore starting tuning, you can verify that your dataset is loaded correctly with:\n\n.. code-block:: python\n\n    from agentscope.tuner import DatasetConfig\n\n    dataset = DatasetConfig(path=\"my_dataset\", split=\"train\")\n    dataset.preview(n=2)\n    # Output the first two samples to verify correct loading\n    # [\n    #   {\n    #     \"question\": \"What is 2 + 2?\",\n    #     \"answer\": \"4\"\n    #   },\n    #   {\n    #     \"question\": \"What is 4 + 4?\",\n    #     \"answer\": \"8\"\n    #   }\n    # ]\n\nWorkflow Function\n--------------------\nThe workflow function defines how the agent interacts with the environment and makes decisions. All workflow functions should follow the input/output signature defined in ``agentscope.tuner.WorkflowType``.\n\nBelow is an example workflow function using a ReAct agent to answer math questions:\n\"\"\"\n\nfrom typing import Dict, Optional\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import OpenAIChatFormatter\nfrom agentscope.message import Msg\nfrom agentscope.model import ChatModelBase\nfrom agentscope.tuner import WorkflowOutput\n\n\nasync def example_workflow_function(\n    task: Dict,\n    model: ChatModelBase,\n    auxiliary_models: Optional[Dict[str, ChatModelBase]] = None,\n) -> WorkflowOutput:\n    \"\"\"An example workflow function for tuning.\n\n    Args:\n        task (`Dict`): The task information.\n        model (`ChatModelBase`): The chat model used by the agent.\n        auxiliary_models (`Optional[Dict[str, ChatModelBase]]`): Additional\n            chat models, generally used to simulate the behavior of other\n            non-training agents in multi-agent scenarios.\n\n    Returns:\n        `WorkflowOutput`: The output generated by the workflow.\n    \"\"\"\n    agent = ReActAgent(\n        name=\"react_agent\",\n        sys_prompt=\"You are a helpful math problem solving agent.\",\n        model=model,\n        formatter=OpenAIChatFormatter(),\n    )\n\n    response = await agent.reply(\n        msg=Msg(\n            \"user\",\n            task[\"question\"],\n            role=\"user\",\n        ),  # extract question from task\n    )\n\n    return WorkflowOutput(  # return the response\n        response=response,\n    )\n\n\n# %%\n# You can directly run this workflow function with a task dictionary and a ``DashScopeChatModel`` / ``OpenAIChatModel`` to test its correctness before formal training. For example:\n\nimport asyncio\nimport os\nfrom agentscope.model import DashScopeChatModel\n\ntask = {\"question\": \"What is 123 plus 456?\", \"answer\": \"579\"}\nmodel = DashScopeChatModel(\n    model_name=\"qwen-max\",\n    api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n)\nworkflow_output = asyncio.run(example_workflow_function(task, model))\nassert isinstance(\n    workflow_output.response,\n    Msg,\n), \"In this example, the response should be a Msg instance.\"\nprint(\"\\nWorkflow response:\", workflow_output.response.get_text_content())\n\n# %%\n#\n# Judge Function\n# --------------------\n# The judge function evaluates the agent's performance on a given task and provides a reward signal for tuning.\n# All judge functions should follow the input/output signature defined in ``agentscope.tuner.JudgeType``.\n# Below is a simple judge function that compares the agent's response with the ground truth answer:\n\nfrom typing import Any\nfrom agentscope.tuner import JudgeOutput\n\n\nasync def example_judge_function(\n    task: Dict,\n    response: Any,\n    auxiliary_models: Optional[Dict[str, ChatModelBase]] = None,\n) -> JudgeOutput:\n    \"\"\"A very simple judge function only for demonstration.\n\n    Args:\n        task (`Dict`): The task information.\n        response (`Any`): The response field from the WorkflowOutput.\n        auxiliary_models (`Optional[Dict[str, ChatModelBase]]`): Additional\n            chat models for LLM-as-a-Judge purpose.\n    Returns:\n        `JudgeOutput`: The reward assigned by the judge.\n    \"\"\"\n    ground_truth = task[\"answer\"]\n    reward = 1.0 if ground_truth in response.get_text_content() else 0.0\n    return JudgeOutput(reward=reward)\n\n\njudge_output = asyncio.run(\n    example_judge_function(\n        task,\n        workflow_output.response,\n    ),\n)\nprint(f\"Judge reward: {judge_output.reward}\")\n\n# %%\n# The judge function can also be locally tested in the same way as shown above before formal training to ensure its logic is correct.\n#\n# .. tip::\n#    You can leverage existing `MetricBase <https://github.com/agentscope-ai/agentscope/blob/main/src/agentscope/evaluate/_metric_base.py>`_ implementations in your judge function to compute more sophisticated metrics and combine them into a composite reward.\n#\n# Configuration and Running\n# ~~~~~~~~~~~~~~~\n# Finally, you can configure and run the tuning process using the ``tuner`` module.\n# Before starting, ensure that `Trinity-RFT <https://github.com/agentscope-ai/Trinity-RFT>`_ is installed in your environment, as it is required for tuning.\n#\n# Below is an example of configuring and starting the tuning process:\n#\n# .. note::\n#    This example is for demonstration only. For a complete runnable example, see `Tune ReActAgent <https://github.com/agentscope-ai/agentscope/tree/main/examples/tuner/react_agent>`_\n#\n# .. code-block:: python\n#\n#        from agentscope.tuner import tune, AlgorithmConfig, DatasetConfig, TunerModelConfig\n#        # your workflow / judge function here...\n#\n#        if __name__ == \"__main__\":\n#            dataset = DatasetConfig(path=\"my_dataset\", split=\"train\")\n#            model = TunerModelConfig(model_path=\"Qwen/Qwen3-0.6B\", max_model_len=16384)\n#            algorithm = AlgorithmConfig(\n#                algorithm_type=\"multi_step_grpo\",\n#                group_size=8,\n#                batch_size=32,\n#                learning_rate=1e-6,\n#            )\n#            tune(\n#                workflow_func=example_workflow_function,\n#                judge_func=example_judge_function,\n#                model=model,\n#                train_dataset=dataset,\n#                algorithm=algorithm,\n#            )\n#\n# Here, ``DatasetConfig`` configures the training dataset, ``TunerModelConfig`` sets the parameters for the trainable model, and ``AlgorithmConfig`` specifies the reinforcement learning algorithm and its hyperparameters.\n#\n# .. tip::\n#    The ``tune`` function is based on `Trinity-RFT <https://github.com/agentscope-ai/Trinity-RFT>`_ and internally converts input parameters to a YAML configuration.\n#    Advanced users can skip the ``model``, ``train_dataset``, and ``algorithm`` arguments and instead provide a YAML config file path via the ``config_path`` argument.\n#    Using a configuration file is recommended for fine-grained control and to leverage advanced Trinity-RFT features. See the Trinity-RFT `Configuration Guide <https://agentscope-ai.github.io/Trinity-RFT/en/main/tutorial/trinity_configs.html>`_ for more options.\n#\n# Save the above code as ``main.py`` and run it with:\n#\n# .. code-block:: bash\n#\n#        ray start --head\n#        python main.py\n#\n# Checkpoints and logs are automatically saved to the ``checkpoints/AgentScope`` directory under your workspace, with each run in a timestamped sub-directory. Tensorboard logs can be found in ``monitor/tensorboard`` within the checkpoint directory.\n#\n# .. code-block:: text\n#\n#        your_workspace/\n#            └── checkpoints/\n#                └──AgentScope/\n#                    └── Experiment-20260104185355/  # each run saved in a sub-directory with timestamp\n#                        ├── monitor/\n#                        │   └── tensorboard/  # tensorboard logs\n#                        └── global_step_x/    # saved model checkpoints at step x\n#\n# .. tip::\n#    For more tuning examples, refer to the `tuner directory <https://github.com/agentscope-ai/agentscope-samples/tree/main/tuner>`_ of the AgentScope-Samples repository.\n"
  },
  {
    "path": "docs/tutorial/en/src/workflow_concurrent_agents.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nConcurrent Agents\n===================================\nWith the help of asynchronous programming, the concurrent agents can be executed by ``asyncio.gather`` in Python.\n\nA simple example is shown below, where two agents are created and executed concurrently.\n\"\"\"\nimport asyncio\nfrom datetime import datetime\nfrom typing import Any\n\nfrom agentscope.agent import AgentBase\n\n\nclass ExampleAgent(AgentBase):\n    \"\"\"The example agent for concurrent execution.\"\"\"\n\n    def __init__(self, name: str) -> None:\n        \"\"\"Initialize the agent with its name.\"\"\"\n        super().__init__()\n        self.name = name\n\n    async def reply(self, *args: Any, **kwargs: Any) -> None:\n        \"\"\"Reply to the message.\"\"\"\n        start_time = datetime.now().strftime(\"%H:%M:%S.%f\")[:-3]\n        print(f\"{self.name} started at {start_time}\")\n        await asyncio.sleep(3)  # Simulate a long-running task\n        end_time = datetime.now().strftime(\"%H:%M:%S.%f\")[:-3]\n        print(f\"{self.name} finished at {end_time}\")\n\n\nasync def run_concurrent_agents() -> None:\n    \"\"\"Run the concurrent agents.\"\"\"\n    agent1 = ExampleAgent(\"Agent 1\")\n    agent2 = ExampleAgent(\"Agent 2\")\n\n    await asyncio.gather(agent1(), agent2())\n\n\nasyncio.run(run_concurrent_agents())\n"
  },
  {
    "path": "docs/tutorial/en/src/workflow_conversation.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _conversation:\n\nConversation\n======================\n\nConversation is a design pattern that agents exchange and share information\nbetween each other, most commonly in game playing, chatbot, and multi-agent\ndiscussion scenarios.\n\nIn AgentScope, the conversation is built upon the **explicit message\nexchange**. In this tutorial, we will demonstrate how to build a conversation\n\n- between a user and an agent (chatbot)\n- between multiple agents (game playing, discussion, etc.)\n\nTheir main difference lies in\n\n- how the **prompt is constructed**, and\n- how the information is **propagated/shared** among agents.\n\"\"\"\nimport asyncio\nimport json\nimport os\n\nfrom agentscope.agent import ReActAgent, UserAgent\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.formatter import (\n    DashScopeChatFormatter,\n    DashScopeMultiAgentFormatter,\n)\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.message import Msg\nfrom agentscope.pipeline import MsgHub\nfrom agentscope.tool import Toolkit\n\n# %%\n# User-Agent Conversation\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# User-agent conversation, also known as chatbot, is the most common usage\n# scenario of LLM-empowered agents, and the design target of most LLM APIs.\n# Such conversation features only two participants: a user and an agent.\n#\n# In AgentScope, the formatters with **\"Chat\"** in its name are designed for\n# user-agent conversation, such as ``DashScopeChatFormatter``,\n# ``AnthropicChatFormatter``, etc.\n# They use the ``role`` field in the message to distinguish the user and the\n# agent, and format the messages accordingly.\n#\n# Here we build a simple conversation between agent ``Friday`` and user.\n#\n# .. tip:: AgentScope provides a built-in ``UserAgent`` class for human-in-the-loop (HITL) interaction. Refer to :ref:`user-agent` for more details.\n#\n\nfriday = ReActAgent(\n    name=\"Friday\",\n    sys_prompt=\"You're a helpful assistant named Friday\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n    ),\n    formatter=DashScopeChatFormatter(),  # The formatter for user-agent conversation\n    memory=InMemoryMemory(),\n    toolkit=Toolkit(),\n)\n\n# Create a user agent\nuser = UserAgent(name=\"User\")\n\n# %%\n# Now, we can program the conversation by exchanging messages between these two agents until the user types \"exit\" to end the conversation.\n#\n# .. code-block:: python\n#\n#     async def run_conversation() -> None:\n#         \"\"\"Run a simple conversation between Friday and User.\"\"\"\n#         msg = None\n#         while True:\n#             msg = await friday(msg)\n#             msg = await user(msg)\n#             if msg.get_text_content() == \"exit\":\n#                 break\n#\n#     asyncio.run(run_conversation())\n#\n\n# %%\n# More than Two Agents\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# As stated in the beginning, we demonstrate how to build conversation with multiple agents in terms of **prompt construction** and **information sharing**.\n#\n# Prompt Construction\n# -------------------------------\n# In AgentScope, we provide built-in formatters for multi-agent conversation, featuring **\"MultiAgent\"** in their names, such as ``DashScopeMultiAgentFormatter``, ``AnthropicMultiAgentFormatter``, etc.\n#\n# Specifically, they use the ``name`` field in the message to distinguish different agents, and format the conversation history into a single user message.\n# Taking ``DashScopeMultiAgentFormatter`` as an example:\n#\n# .. tip:: More details about the formatter can be found in :ref:`prompt`.\n#\n\n\nasync def example_multi_agent_prompt() -> None:\n    msgs = [\n        Msg(\"system\", \"You're a helpful assistant named Bob.\", \"system\"),\n        Msg(\"Alice\", \"Hi!\", \"user\"),\n        Msg(\"Bob\", \"Hi! Nice to meet you guys.\", \"assistant\"),\n        Msg(\"Charlie\", \"Me too! I'm Charlie, by the way.\", \"assistant\"),\n    ]\n\n    formatter = DashScopeMultiAgentFormatter()\n    prompt = await formatter.format(msgs)\n\n    print(\"Formatted prompt:\")\n    print(json.dumps(prompt, indent=4, ensure_ascii=False))\n\n    # We print the content of the combined user message here for better\n    # understanding:\n    print(\"-------------\")\n    print(\"Combined message\")\n    print(prompt[1][\"content\"])\n\n\nasyncio.run(example_multi_agent_prompt())\n\n\n# %%\n# Message Sharing\n# -------------------------------\n# In multi-agent conversation, exchanging messages explicitly may not be efficient and convenient, especially when broadcasting messages among multiple agents.\n#\n# Therefore, AgentScope provides an async context manager named ``MsgHub`` to simplify the operation of broadcasting messages.\n# Specifically, the agents within the same ``MsgHub`` will receive messages from other participants in the same ``MsgHub`` automatically.\n#\n\nmodel = DashScopeChatModel(\n    model_name=\"qwen-max\",\n    api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n)\nformatter = DashScopeMultiAgentFormatter()\n\nalice = ReActAgent(\n    name=\"Alice\",\n    sys_prompt=\"You're a student named Alice.\",\n    model=model,\n    formatter=formatter,\n    toolkit=Toolkit(),\n    memory=InMemoryMemory(),\n)\n\nbob = ReActAgent(\n    name=\"Bob\",\n    sys_prompt=\"You're a student named Bob.\",\n    model=model,\n    formatter=formatter,\n    toolkit=Toolkit(),\n    memory=InMemoryMemory(),\n)\n\ncharlie = ReActAgent(\n    name=\"Charlie\",\n    sys_prompt=\"You're a student named Charlie.\",\n    model=model,\n    formatter=formatter,\n    toolkit=Toolkit(),\n    memory=InMemoryMemory(),\n)\n\n\nasync def example_msghub() -> None:\n    \"\"\"Example of using MsgHub for multi-agent conversation.\"\"\"\n    async with MsgHub(\n        [alice, bob, charlie],\n        announcement=Msg(\n            \"system\",\n            \"Now you meet each other with a brief self-introduction.\",\n            \"system\",\n        ),\n    ):\n        await alice()\n        await bob()\n        await charlie()\n\n\nasyncio.run(example_msghub())\n\n# %%\n# Now we print the memory of Alice to check if her memory is updated correctly.\n#\n\n\nasync def example_memory() -> None:\n    \"\"\"Print the memory of Alice.\"\"\"\n    print(\"Memory of Alice:\")\n    for msg in await alice.memory.get_memory():\n        print(f\"{msg.name}: {msg.get_text_content()}\")\n\n\nasyncio.run(example_memory())\n\n# %%\n# Further Reading\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# - :ref:`prompt`\n# - :ref:`pipeline`\n#\n"
  },
  {
    "path": "docs/tutorial/en/src/workflow_handoffs.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nHandoffs\n========================================\n\n.. figure:: ../../_static/images/handoffs.png\n   :width: 80%\n   :align: center\n   :alt: Orchestrator-Workers Workflow\n\n   *Handoffs example*\n\nIt's very simple to implement the Orchestrator-Workers workflow with tool calls in AgentScope.\nFirst, we create a function to allow the orchestrator to create workers dynamically.\n\n\"\"\"\n\nimport asyncio\nimport os\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import (\n    ToolResponse,\n    Toolkit,\n    execute_python_code,\n)\n\n\n# The tool function to create a worker\nasync def create_worker(\n    task_description: str,\n) -> ToolResponse:\n    \"\"\"Create a worker to finish the given task. The worker is equipped with python execution tool.\n\n    Args:\n        task_description (``str``):\n            The description of the task to be finished by the worker.\n    \"\"\"\n    # Equip the worker agent with some tools\n    toolkit = Toolkit()\n    toolkit.register_tool_function(execute_python_code)\n\n    # Create a worker agent\n    worker = ReActAgent(\n        name=\"Worker\",\n        sys_prompt=\"You're a worker agent. Your target is to finish the given task.\",\n        model=DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            stream=False,\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n    )\n    # Let the worker finish the task\n    res = await worker(Msg(\"user\", task_description, \"user\"))\n    return ToolResponse(\n        content=res.get_content_blocks(\"text\"),\n    )\n\n\nasync def run_handoffs() -> None:\n    \"\"\"Example of handoffs workflow.\"\"\"\n    # Initialize the orchestrator agent\n    toolkit = Toolkit()\n    toolkit.register_tool_function(create_worker)\n\n    orchestrator = ReActAgent(\n        name=\"Orchestrator\",\n        sys_prompt=\"You're an orchestrator agent. Your target is to finish the given task by decomposing it into smaller tasks and creating workers to finish them.\",\n        model=DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            stream=False,\n        ),\n        memory=InMemoryMemory(),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n    )\n\n    # The task description\n    task_description = \"Execute hello world in Python\"\n\n    # Create a worker to finish the task\n    await orchestrator(Msg(\"user\", task_description, \"user\"))\n\n\nasyncio.run(run_handoffs())\n"
  },
  {
    "path": "docs/tutorial/en/src/workflow_multiagent_debate.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _multiagent-debate:\n\nMulti-Agent Debate\n========================\n\nDebate workflow simulates a multi-turn discussion between different agents, mostly several solvers and an aggregator.\nTypically, the solvers generate and exchange their answers, while the aggregator collects and summarizes the answers.\n\nWe implement the examples in `EMNLP 2024`_, where two debater agents will discuss a topic in a fixed order, and express their\narguments based on the previous debate history.\nAt each round a moderator agent will decide whether the correct answer can be obtained in the current iteration.\n\"\"\"\nimport asyncio\nimport os\n\nfrom pydantic import Field, BaseModel\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import (\n    DashScopeMultiAgentFormatter,\n    DashScopeChatFormatter,\n)\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.pipeline import MsgHub\n\n# Prepare a topic\ntopic = (\n    \"The two circles are externally tangent and there is no relative sliding. \"\n    \"The radius of circle A is 1/3 the radius of circle B. Circle A rolls \"\n    \"around circle B one trip back to its starting point. How many times will \"\n    \"circle A revolve in total?\"\n)\n\n\n# Create two debater agents, Alice and Bob, who will discuss the topic.\ndef create_solver_agent(name: str) -> ReActAgent:\n    \"\"\"Get a solver agent.\"\"\"\n    return ReActAgent(\n        name=name,\n        sys_prompt=f\"You're a debater named {name}. Hello and welcome to the \"\n        \"debate competition. It's unnecessary to fully agree with \"\n        \"each other's perspectives, as our objective is to find \"\n        \"the correct answer. The debate topic is stated as \"\n        f\"follows: {topic}.\",\n        model=DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            stream=False,\n        ),\n        formatter=DashScopeMultiAgentFormatter(),\n    )\n\n\nalice, bob = [create_solver_agent(name) for name in [\"Alice\", \"Bob\"]]\n\n# Create a moderator agent\nmoderator = ReActAgent(\n    name=\"Aggregator\",\n    sys_prompt=f\"\"\"You're a moderator. There will be two debaters involved in a debate competition. They will present their answer and discuss their perspectives on the topic:\n``````\n{topic}\n``````\nAt the end of each round, you will evaluate both sides' answers and decide which one is correct.\"\"\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        stream=False,\n    ),\n    # Use multiagent formatter because the moderator will receive messages from more than a user and an assistant\n    formatter=DashScopeMultiAgentFormatter(),\n)\n\n\n# A structured output model for the moderator\nclass JudgeModel(BaseModel):\n    \"\"\"The structured output model for the moderator.\"\"\"\n\n    finished: bool = Field(\n        description=\"Whether the debate is finished.\",\n    )\n    correct_answer: str | None = Field(\n        description=\"The correct answer to the debate topic, only if the debate is finished. Otherwise, leave it as None.\",\n        default=None,\n    )\n\n\nasync def run_multiagent_debate() -> None:\n    \"\"\"Run the multi-agent debate workflow.\"\"\"\n    while True:\n        # The reply messages in MsgHub from the participants will be broadcasted to all participants.\n        async with MsgHub(participants=[alice, bob, moderator]):\n            await alice(\n                Msg(\n                    \"user\",\n                    \"You are affirmative side, Please express your viewpoints.\",\n                    \"user\",\n                ),\n            )\n            await bob(\n                Msg(\n                    \"user\",\n                    \"You are negative side. You disagree with the affirmative side. Provide your reason and answer.\",\n                    \"user\",\n                ),\n            )\n\n        # Alice and Bob doesn't need to know the moderator's message, so moderator is called outside the MsgHub.\n        msg_judge = await moderator(\n            Msg(\n                \"user\",\n                \"Now you have heard the answers from the others, have the debate finished, and can you get the correct answer?\",\n                \"user\",\n            ),\n            structured_model=JudgeModel,\n        )\n\n        if msg_judge.metadata.get(\"finished\"):\n            print(\n                \"\\nThe debate is finished, and the correct answer is: \",\n                msg_judge.metadata.get(\"correct_answer\"),\n            )\n            break\n\n\nasyncio.run(run_multiagent_debate())\n\n\n# %%\n# Further Reading\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# - :ref:`pipeline`\n#\n# .. _EMNLP 2024:\n# Encouraging Divergent Thinking in Large Language Models through Multi-Agent Debate. EMNLP 2024.\n#\n"
  },
  {
    "path": "docs/tutorial/en/src/workflow_routing.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _routing:\n\nRouting\n==========================\nThere are two ways to implement routing in AgentScope, both simple and easy to implement:\n\n- Routing by structured output\n- Routing by tool calls\n\n.. tip:: Considering there is no unified standard/definition for agent routing, we follow the setting in `Building effective agents <https://www.anthropic.com/engineering/building-effective-agents>`_\n\nRouting by Structured Output\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nBy this way, we can directly use the structured output of the agent to determine which agent to route the message to.\n\nInitialize a routing agent\n\"\"\"\nimport asyncio\nimport json\nimport os\nfrom typing import Literal\n\nfrom pydantic import BaseModel, Field\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import Toolkit, ToolResponse\n\nrouter = ReActAgent(\n    name=\"Router\",\n    sys_prompt=\"You're a routing agent. Your target is to route the user query to the right follow-up task.\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        stream=False,\n    ),\n    formatter=DashScopeChatFormatter(),\n)\n\n\n# Use structured output to specify the routing task\nclass RoutingChoice(BaseModel):\n    your_choice: Literal[\n        \"Content Generation\",\n        \"Programming\",\n        \"Information Retrieval\",\n        None,\n    ] = Field(\n        description=\"Choose the right follow-up task, and choose ``None`` if the task is too simple or no suitable task\",\n    )\n    task_description: str | None = Field(\n        description=\"The task description\",\n        default=None,\n    )\n\n\nasync def example_router_explicit() -> None:\n    \"\"\"Example of explicit routing with structured output.\"\"\"\n    msg_user = Msg(\n        \"user\",\n        \"Help me to write a poem\",\n        \"user\",\n    )\n\n    # Route the query\n    msg_res = await router(\n        msg_user,\n        structured_model=RoutingChoice,\n    )\n\n    # The structured output is stored in the metadata field\n    print(\"The structured output:\")\n    print(json.dumps(msg_res.metadata, indent=4, ensure_ascii=False))\n\n\nasyncio.run(example_router_explicit())\n\n# %%\n# Routing by Tool Calls\n# ~~~~~~~~~~~~~~~~~~~~~~~~~\n# Another way is to wrap the downstream agents into a tool function, so that the routing agent decides which tool to call based on the user query.\n#\n# We first define several tool functions:\n#\n\n\nasync def generate_python(demand: str) -> ToolResponse:\n    \"\"\"Generate Python code based on the demand.\n\n    Args:\n        demand (``str``):\n            The demand for the Python code.\n    \"\"\"\n    # An example demand agent\n    python_agent = ReActAgent(\n        name=\"PythonAgent\",\n        sys_prompt=\"You're a Python expert, your target is to generate Python code based on the demand.\",\n        model=DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            stream=False,\n        ),\n        memory=InMemoryMemory(),\n        formatter=DashScopeChatFormatter(),\n        toolkit=Toolkit(),\n    )\n    msg_res = await python_agent(Msg(\"user\", demand, \"user\"))\n\n    return ToolResponse(\n        content=msg_res.get_content_blocks(\"text\"),\n    )\n\n\n# Fake some other tool functions for demonstration purposes\nasync def generate_poem(demand: str) -> ToolResponse:\n    \"\"\"Generate a poem based on the demand.\n\n    Args:\n        demand (``str``):\n            The demand for the poem.\n    \"\"\"\n    pass\n\n\nasync def web_search(query: str) -> ToolResponse:\n    \"\"\"Search the web for the query.\n\n    Args:\n        query (``str``):\n            The query to search.\n    \"\"\"\n    pass\n\n\n# %%\n# After that, we define a routing agent and equip it with the above tool functions.\n#\n\ntoolkit = Toolkit()\ntoolkit.register_tool_function(generate_python)\ntoolkit.register_tool_function(generate_poem)\ntoolkit.register_tool_function(web_search)\n\n# Initialize the routing agent with the toolkit\nrouter_implicit = ReActAgent(\n    name=\"Router\",\n    sys_prompt=\"You're a routing agent. Your target is to route the user query to the right follow-up task.\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        stream=False,\n    ),\n    formatter=DashScopeChatFormatter(),\n    toolkit=toolkit,\n    memory=InMemoryMemory(),\n)\n\n\nasync def example_router_implicit() -> None:\n    \"\"\"Example of implicit routing with tool calls.\"\"\"\n    msg_user = Msg(\n        \"user\",\n        \"Help me to generate a quick sort function in Python\",\n        \"user\",\n    )\n\n    # Route the query\n    await router_implicit(msg_user)\n\n\nasyncio.run(example_router_implicit())\n"
  },
  {
    "path": "docs/tutorial/zh_CN/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?=\nSPHINXBUILD   ?= sphinx-build\nSOURCEDIR     = source\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "docs/tutorial/zh_CN/build.sh",
    "content": "#!/bin/bash\n\nset -e\n\n# Clean old build files\nrm -rf build/ doctrees/\n\n# Build the html\nsphinx-build -M html ./ build\n\n# Remove temporary files (double insurance)\nrm -rf build/html/.doctrees\nrm -f build/html/.buildinfo\nfind build/html -name \"*.pickle\" -delete\nfind build/html -name \"__pycache__\" -delete\nfind build/html -name \"*.pyc\" -delete\n\necho \"✅ Chinese docs built successfully, temporary files cleaned\""
  },
  {
    "path": "docs/tutorial/zh_CN/conf.py",
    "content": "# -*- coding: utf-8 -*-\n# Configuration file for the Sphinx documentation builder.\n#\n# For the full list of built-in configuration values, see the documentation:\n# https://www.sphinx-doc.org/en/master/usage/configuration.html\n\n# -- Project information -----------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information\n\nproject = \"AgentScope\"\ncopyright = \"2025, Alibaba\"\nauthor = \"Alibaba Tongyi Lab\"\n\n# -- General configuration ---------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration\n\nextensions = [\n    \"myst_parser\",\n    \"sphinx_gallery.gen_gallery\",\n    \"sphinx.ext.autodoc\",\n    \"sphinx.ext.viewcode\",\n    \"sphinx.ext.napoleon\",\n]\n\nmyst_enable_extensions = [\n    \"colon_fence\",\n]\n\nsphinx_gallery_conf = {\n    \"download_all_examples\": False,\n    \"examples_dirs\": [\n        \"src\",\n    ],\n    \"gallery_dirs\": [\n        \"tutorial\",\n    ],\n    \"filename_pattern\": \"src/.*\\.py\",\n    \"example_extensions\": [\".py\"],\n}\n\ntemplates_path = [\"../_templates\"]\nexclude_patterns = [\"_build\", \"Thumbs.db\", \".DS_Store\"]\n\nlanguages = [\"en\", \"zh_CN\"]\nlanguage = \"zh_CN\"\n\n# -- Options for HTML output -------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output\n\nhtml_theme = \"furo\"\nhtml_title = (\n    \"<span style='font-weight: 700; color: #2196f3;'>AgentScope</span>\"\n)\nhtml_logo = \"../_static/images/logo.svg\"\nhtml_favicon = \"../_static/images/logo.svg\"\nhtml_static_path = [\"../_static\"]\nhtml_css_files = [\n    \"css/gallery.css\",\n]\n\nhtml_js_files = [\n    \"language_switch.js\",\n]\n\nhtml_theme_options = {\n    \"footer_icons\": [\n        {\n            \"name\": \"GitHub\",\n            \"url\": \"https://github.com/agentscope-ai/agentscope\",\n            \"html\": \"\"\"\n                <svg stroke=\"currentColor\" fill=\"currentColor\" stroke-width=\"0\" viewBox=\"0 0 16 16\">\n                    <path fill-rule=\"evenodd\" d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z\"></path>\n                </svg>\n            \"\"\",\n            \"class\": \"\",\n        },\n        {\n            \"name\": \"Discord\",\n            \"url\": \"https://discord.gg/eYMpfnkG8h\",\n            \"html\": \"\"\"\n                <svg stroke=\"currentColor\" fill=\"currentColor\" stroke-width=\"0\" t=\"1753331148815\" class=\"icon\" viewBox=\"0 0 1024 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" p-id=\"5721\" width=\"200\" height=\"200\">\n                    <path d=\"M723.903423 359.138018c-69.65045-52.952793-136.256577-51.476757-136.256576-51.476757l-6.088649 7.564685c83.027027 25.738378 121.127207 62.085766 121.127207 62.085766a387.459459 387.459459 0 0 0-145.297297-46.956397 418.179459 418.179459 0 0 0-98.340901 1.752793 73.801802 73.801802 0 0 1-7.564684 1.476036 357.385225 357.385225 0 0 0-110.702703 30.258739 278.786306 278.786306 0 0 0-28.782703 13.653333S353.049369 339.488288 440.873514 313.657658l-4.612613-6.088649s-66.513874-1.476036-136.164324 51.476757A654.252973 654.252973 0 0 0 230.630631 642.167928s40.867748 71.126486 148.341621 73.801802c0 0 16.697658-22.694054 31.827027-40.867748-62.085766-18.45045-84.77982-57.565405-84.77982-57.565405a130.998198 130.998198 0 0 0 13.653334 7.564684s0 1.568288 1.476036 1.568289c1.476036 1.476036 3.044324 1.476036 4.52036 3.044324a238.748829 238.748829 0 0 0 34.779099 16.605405 513.199279 513.199279 0 0 0 71.218739 21.218018 350.558559 350.558559 0 0 0 125.555315 0 329.894054 329.894054 0 0 0 69.650451-21.218018A247.328288 247.328288 0 0 0 702.685405 618.09009s-24.262342 39.391712-87.824144 57.565405c13.653333 18.45045 31.827027 39.299459 31.827027 39.29946 107.473874-2.952072 148.341622-73.801802 146.773334-72.602523a654.990991 654.990991 0 0 0-69.558199-283.214414zM421.131532 596.77982a54.705586 54.705586 0 0 1 0-109.042162 54.705586 54.705586 0 0 1 0 109.042162z m177.124324 0a54.705586 54.705586 0 1 1 49.908468-54.521081 52.491532 52.491532 0 0 1-49.908468 54.521081z\" p-id=\"5722\"></path><path d=\"M512 1024A512 512 0 1 1 1024 512 512.645766 512.645766 0 0 1 512 1024z m0-972.892252a461.261261 461.261261 0 1 0 461.261261 461.261261 461.261261 461.261261 0 0 0-461.261261-461.261261z\" p-id=\"5723\"></path>\n                </svg>\n            \"\"\",\n            \"class\": \"\",\n        },\n        {\n            \"name\": \"DingTalk\",\n            \"url\": \"https://qr.dingtalk.com/action/joingroup?code=v1,k1,OmDlBXpjW+I2vWjKDsjvI9dhcXjGZi3bQiojOq3dlDw=&_dt_no_comment=1&origin=11\",\n            \"html\": \"\"\"\n                <svg stroke=\"currentColor\" fill=\"currentColor\" stroke-width=\"0\" viewBox=\"0 0 1024 1024\">\n                    <path d=\"M512 0C229.205333 0 0 229.205333 0 512s229.205333 512 512 512 512-229.205333 512-512S794.794667 0 512 0z m237.312 480.810667c-1.109333 4.48-3.712 11.093333-7.424 18.986666h0.128l-0.426667 0.682667c-21.504 46.037333-77.610667 136.106667-77.610666 136.106667l-0.298667-0.597334-16.384 28.501334h79.018667l-150.912 200.917333 34.304-136.533333h-62.208l21.589333-90.282667c-17.493333 4.224-38.101333 10.026667-62.592 17.92 0 0-33.109333 19.370667-95.317333-37.333333 0 0-41.984-36.992-17.578667-46.165334 10.410667-3.925333 50.304-8.917333 81.706667-13.226666 42.410667-5.674667 68.48-8.789333 68.48-8.789334s-130.773333 2.005333-161.792-2.901333c-30.976-4.906667-70.4-56.704-78.805334-102.186667 0 0-12.970667-25.002667 27.904-13.226666 40.917333 11.818667 210.005333 46.08 210.005334 46.08S321.109333 411.434667 306.517333 394.922667c-14.634667-16.469333-43.093333-89.770667-39.424-134.869334 0 0 1.621333-11.221333 13.098667-8.192 0 0 162.602667 74.282667 273.792 114.986667 111.104 40.704 207.786667 61.397333 195.328 114.005333z\" opacity=\".65\" p-id=\"6077\"></path>\n                </svg>\n            \"\"\",\n            \"class\": \"\",\n        },\n    ],\n    \"light_css_variables\": {\n        \"color-brand-primary\": \"#2196f3\",\n        \"color-brand-content\": \"#2196f3\",\n        \"color-admonition-background\": \"#f8f9fa\",\n    },\n    \"dark_css_variables\": {\n        \"color-link\": \"#2196f3\",\n        \"color-link--hover\": \"#2196f3\",\n        \"color-brand-primary\": \"#64b5f6\",\n        \"color-brand-content\": \"#64b5f6\",\n    },\n}\n\nsource_suffix = [\".md\", \".rst\"]\n\n\n# -- Options for API documentation -------------------------------------------\n\nautodoc_member_order = \"bysource\"\nautodoc_typehints = \"description\"\nautodoc_class_signature = \"separated\"\nautodoc_default_options = {\n    \"special-members\": \"__call__\",\n}\n\nadd_module_names = False\npython_display_short_literal_types = True\n\n\ndef skip_member(app, what, name, obj, skip, options):\n    if name in [\n        \"__call__\",\n        \"_format\",\n        \"_format_agent_message\",\n        \"_format_tool_sequence\",\n    ]:\n        return False\n\n    return skip\n\n\ndef setup(app):\n    app.connect(\"autodoc-skip-member\", skip_member)\n"
  },
  {
    "path": "docs/tutorial/zh_CN/index.rst",
    "content": ".. AgentScope Doc documentation master file, created by\n   sphinx-quickstart on Thu Aug  8 15:07:21 2024.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to AgentScope's documentation!\n==========================================\n\n.. toctree::\n   :maxdepth: 1\n   :caption: Tutorial\n\n   tutorial/quickstart_installation\n   tutorial/quickstart_key_concept\n   tutorial/quickstart_message\n   tutorial/quickstart_agent\n\n.. toctree::\n   :maxdepth: 1\n   :caption: Workflow\n\n   tutorial/workflow_conversation\n   tutorial/workflow_multiagent_debate\n   tutorial/workflow_concurrent_agents\n   tutorial/workflow_routing\n   tutorial/workflow_handoffs\n\n.. toctree::\n   :maxdepth: 1\n   :caption: FAQ\n\n   tutorial/faq\n\n\n.. toctree::\n   :maxdepth: 1\n   :caption: Model and Context\n\n   tutorial/task_model\n   tutorial/task_prompt\n   tutorial/task_token\n   tutorial/task_memory\n   tutorial/task_long_term_memory\n\n.. toctree::\n   :maxdepth: 1\n   :caption: Tool\n\n   tutorial/task_tool\n   tutorial/task_mcp\n   tutorial/task_agent_skill\n\n.. toctree::\n   :maxdepth: 1\n   :caption: Agent\n\n   tutorial/task_agent\n   tutorial/task_state\n   tutorial/task_hook\n   tutorial/task_middleware\n   tutorial/task_a2a\n   tutorial/task_realtime\n\n.. toctree::\n   :maxdepth: 1\n   :caption: Features\n\n   tutorial/task_pipeline\n   tutorial/task_plan\n   tutorial/task_rag\n   tutorial/task_studio\n   tutorial/task_tracing\n   tutorial/task_eval\n   tutorial/task_eval_openjudge\n   tutorial/task_embedding\n   tutorial/task_tts\n   tutorial/task_tuner\n"
  },
  {
    "path": "docs/tutorial/zh_CN/make.bat",
    "content": "@ECHO OFF\n\npushd %~dp0\n\nREM Command file for Sphinx documentation\n\nif \"%SPHINXBUILD%\" == \"\" (\n\tset SPHINXBUILD=sphinx-build\n)\nset SOURCEDIR=source\nset BUILDDIR=build\n\n%SPHINXBUILD% >NUL 2>NUL\nif errorlevel 9009 (\n\techo.\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\n\techo.installed, then set the SPHINXBUILD environment variable to point\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\n\techo.may add the Sphinx directory to PATH.\n\techo.\n\techo.If you don't have Sphinx installed, grab it from\n\techo.https://www.sphinx-doc.org/\n\texit /b 1\n)\n\nif \"%1\" == \"\" goto help\n\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\ngoto end\n\n:help\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\n\n:end\npopd\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/README.md",
    "content": ""
  },
  {
    "path": "docs/tutorial/zh_CN/src/faq.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _faq:\n\n常见问题\n========================================\n\n关于 AgentScope\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n*什么是AgentScope？*\n    AgentScope 是一个多智能体框架，旨在提供一种简单高效的方式来构建基于大语言模型的智能体应用程序。\n\n*AgentScope v1.0 与 v0.x 版本有什么区别？*\n    AgentScope v1.0是对框架的完全重写，配备了新功能和改进。详细变更请参考相关文档。\n\n\n关于模型\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n*如何在 AgentScope 中集成我自己的模型？*\n    通过继承 ``agentscope.model.ChatModelBase`` 并实现 ``__call__`` 方法来创建您自己的模型。\n\n*AgentScope 支持哪些模型？*\n    目前，AgentScope 内置支持 DashScope、Gemini、OpenAI、Anthropic 和 Ollama API，以及与 DeepSeek 和 vLLMs 模型兼容的 ``OpenAIChatModel``。\n\n*如何在 AgentScope 中监控token 使用情况？*\n    在 AgentScope Studio中，我们提供了 token 使用情况的可视化和追踪功能。详情请参考:ref:`studio` 部分。\n\n\n关于智能体\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n*如何创建我自己的智能体？*\n    您可以选择直接使用 ``ReActAgent`` 类，或者通过继承 ``AgentBase`` 或 ``ReActAgentBase`` 类来创建您自己的智能体。详情请参考 :ref:`agent` 部分。\n\n\n*如何将智能体的（流式）输出转发到我自己的前端或应用程序？*\n    使用 ``print`` 函数的前置钩子来转发打印消息。请参考 :ref:`hook` 部分。\n\n\n关于工具\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n*AgentScope 提供了多少工具？*\n    AgentScope 提供了一套内置工具，包括 ``execute_python_code``、``execute_shell_command``、``write_text_file`` 等。您可以在 ``agentscope.tool`` 模块下找到它们。\n\n\n关于错误报告\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n*如何报告 AgentScope中的错误？*\n    如果您在使用 AgentScope 时遇到错误，请通过在我们的 GitHub仓库中开启一个issue来报告。\n\n*如何报告AgentScope 中的安全漏洞？*\n    如果您在AgentScope 中发现安全问题，请通过 `阿里巴巴安全响应中心(ASRC) <https://security.alibaba.com/>`_ 向我们报告。\n\n\"\"\"\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/quickstart_agent.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _react-agent:\n\n创建 ReAct 智能体\n====================\n\nAgentScope 在 ``agentscope.agent`` 模块下提供了开箱即用的 ReAct 智能体 ``ReActAgent`` 供开发者使用。\n\n它同时支持以下功能：\n\n- ✨ 基础功能\n    - 支持围绕 ``reply``、``observe``、``print``、``_reasoning`` 和 ``_acting`` 的 **钩子函数（hooks）**\n    - 支持结构化输出\n- ✋ 实时介入（Realtime Steering）\n    - 支持用户 **中断**\n    - 支持自定义 **中断处理**\n- 🛠️ 工具\n    - 支持 **同步/异步** 工具函数\n    - 支持 **流式** 工具响应\n    - 支持 **状态化** 的工具管理\n    - 支持 **并行** 工具调用\n    - 支持 **MCP** 服务器\n- 💾 记忆\n    - 支持智能体 **自主管理** 长期记忆\n    - 支持“静态”的长期记忆管理\n\n.. tip:: 有关这些功能的更多详细信息，请参考 :ref:`agent` 部分。本章节中，我们重点介绍如何创建 ReAct 智能体并运行。\n\n\"\"\"\n\nfrom agentscope.agent import ReActAgent, AgentBase\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nimport asyncio\nimport os\n\nfrom agentscope.tool import Toolkit, execute_python_code\n\n\n# %%\n# 创建 ReAct 智能体\n# ------------------------------\n# 为了提高灵活性，``ReActAgent`` 类在其构造函数中暴露了以下参数：\n#\n# .. list-table:: ``ReActAgent`` 类的初始化参数\n#   :header-rows: 1\n#\n#   * - 参数\n#     - 进一步阅读\n#     - 描述\n#   * - ``name`` (必需)\n#     -\n#     - 智能体的名称\n#   * - ``sys_prompt`` (必需)\n#     -\n#     - 智能体的系统提示\n#   * - ``model`` (必需)\n#     - :ref:`model`\n#     - 智能体用于生成响应的模型\n#   * - ``formatter`` (必需)\n#     - :ref:`prompt`\n#     - 提示构建策略，应与使用的模型保持一致\n#   * - ``toolkit``\n#     - :ref:`tool`\n#     - 用于注册/调用工具函数的工具模块\n#   * - ``memory``\n#     - :ref:`memory`\n#     - 用于存储对话历史的短期记忆\n#   * - ``long_term_memory``\n#     - :ref:`long-term-memory`\n#     - 长期记忆\n#   * - ``long_term_memory_mode``\n#     - :ref:`long-term-memory`\n#     - 长期记忆的管理模式：\n#\n#       - ``agent_control``: 允许智能体通过工具函数自己控制长期记忆\n#       - ``static_control``: 在每次回复（reply）的开始/结束时，会自动从长期记忆中检索/记录信息\n#       - ``both``: 同时激活上述两种模式\n#   * - ``enable_meta_tool``\n#     - :ref:`tool`\n#     - 是否启用元工具（Meta tool），即允许智能体自主管理工具函数\n#   * - ``parallel_tool_calls``\n#     - :ref:`agent`\n#     - 是否允许并行工具调用\n#   * - ``max_iters``\n#     -\n#     - 智能体生成响应的最大迭代次数\n#   * - ``plan_notebook``\n#     - :ref:`plan`\n#     - 计划模块，允许智能体制定和管理计划与子任务\n#   * - ``print_hint_msg``\n#     -\n#     - 是否在终端打印 ``plan_notebook`` 生成的提示消息\n#\n# 以 DashScope API 为例，我们创建一个智能体对象如下：\n\n\nasync def creating_react_agent() -> None:\n    \"\"\"创建一个 ReAct 智能体并运行一个简单任务。\"\"\"\n    # 准备工具\n    toolkit = Toolkit()\n    toolkit.register_tool_function(execute_python_code)\n\n    jarvis = ReActAgent(\n        name=\"Jarvis\",\n        sys_prompt=\"你是一个名为 Jarvis 的助手\",\n        model=DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            stream=True,\n            enable_thinking=False,\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n        memory=InMemoryMemory(),\n    )\n\n    msg = Msg(\n        name=\"user\",\n        content=\"你好！Jarvis，用 Python 运行 Hello World。\",\n        role=\"user\",\n    )\n\n    await jarvis(msg)\n\n\nasyncio.run(creating_react_agent())\n\n# %%\n# 从零开始创建\n# --------------------------------\n# 为了支持开发者从零开始创建智能体，AgentScope 提供了两个基类：\n#\n# .. list-table::\n#   :header-rows: 1\n#\n#   * - 类\n#     - 抽象方法\n#     - 描述\n#   * - ``AgentBase``\n#     - | ``reply``\n#       | ``observe``\n#       | ``handle_interrupt``\n#     - - 所有智能体的基类，支持 ``reply``、``observe`` 和 ``print`` 函数的前置和后置钩子函数。\n#       - 在 ``__call__`` 函数内实现了基础的实时介入（Realtime Steering）逻辑。\n#   * - ``ReActAgentBase``\n#     - | ``reply``\n#       | ``observe``\n#       | ``handle_interrupt``\n#       | ``_reasoning``\n#       | ``_acting``\n#     - 在 ``AgentBase`` 的基础上添加了两个抽象函数 ``_reasoning`` 和 ``_acting``，以及它们的钩子函数。\n#\n# 有关智能体类的更多详细信息，请参考 :ref:`agent` 部分。\n#\n# 以 ``AgentBase`` 类为例，我们可以通过继承它并实现 ``reply`` 方法来创建自定义智能体类。\n\n\nclass MyAgent(AgentBase):\n    \"\"\"自定义智能体类\"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"初始化智能体\"\"\"\n        super().__init__()\n\n        self.name = \"Friday\"\n        self.sys_prompt = \"你是一个名为 Friday 的助手。\"\n        self.model = DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            stream=False,\n        )\n        self.formatter = DashScopeChatFormatter()\n        self.memory = InMemoryMemory()\n\n    async def reply(self, msg: Msg | list[Msg] | None) -> Msg:\n        \"\"\"直接调用大模型，产生回复消息。\"\"\"\n        await self.memory.add(msg)\n\n        # 准备提示\n        prompt = await self.formatter.format(\n            [\n                Msg(\"system\", self.sys_prompt, \"system\"),\n                *await self.memory.get_memory(),\n            ],\n        )\n\n        # 调用模型\n        response = await self.model(prompt)\n\n        msg = Msg(\n            name=self.name,\n            content=response.content,\n            role=\"assistant\",\n        )\n\n        # 在记忆中记录响应\n        await self.memory.add(msg)\n\n        # 打印消息\n        await self.print(msg)\n        return msg\n\n    async def observe(self, msg: Msg | list[Msg] | None) -> None:\n        \"\"\"观察消息。\"\"\"\n        # 将消息存储在记忆中\n        await self.memory.add(msg)\n\n    async def handle_interrupt(self) -> Msg:\n        \"\"\"处理中断。\"\"\"\n        # 以固定响应为例\n        return Msg(\n            name=self.name,\n            content=\"我注意到您打断了我的回复，我能为你做些什么？\",\n            role=\"assistant\",\n        )\n\n\nasync def run_custom_agent() -> None:\n    \"\"\"运行自定义智能体。\"\"\"\n    agent = MyAgent()\n    msg = Msg(\n        name=\"user\",\n        content=\"你是谁？\",\n        role=\"user\",\n    )\n    await agent(msg)\n\n\nasyncio.run(run_custom_agent())\n\n# %%\n#\n# 进一步阅读\n# ---------------------\n# - :ref:`agent`\n# - :ref:`model`\n# - :ref:`prompt`\n# - :ref:`tool`\n#\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/quickstart_installation.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _installation:\n\n安装\n============================\n\nAgentScope 需要 Python 3.10 或更高版本。您可以从源代码或 PyPI 安装。\n\n从 PyPI 安装\n----------------\n.. code-block:: bash\n\n    pip install agentscope\n\n从源代码安装\n----------------\n从源代码安装 AgentScope，需要从 GitHub 克隆仓库，并通过以下命令安装\n\n.. code-block:: bash\n\n    git clone -b main https://github.com/agentscope-ai/agentscope\n    cd agentscope\n    pip install -e .\n\n执行以下代码确保 AgentScope 正常安装：\n\"\"\"\n\nimport agentscope\n\nprint(agentscope.__version__)\n\n# %%\n# 额外依赖\n# ----------------------------\n#\n# 为了满足不同功能的需求，AgentScope 提供了额外依赖项。\n#\n# - full: 包含模型 API 和工具函数的额外依赖项\n# - dev: 开发依赖项，包括测试和文档工具\n#\n# 以 full 模式为例，安装命令根据您的操作系统而有所不同。\n#\n# 对于 Windows 用户：\n#\n# .. code-block:: bash\n#\n#       pip install agentscope[full]\n#\n# 对于 Mac 和 Linux 用户：\n#\n# .. code-block:: bash\n#\n#       pip install agentscope\\[full\\]\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/quickstart_key_concept.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. key-concepts:\n\n核心概念\n====================================\n\n本章从工程实践的角度介绍 AgentScope 中的核心概念，从而阐明 AgentScope 的设计理念。\n\n.. note:: 介绍核心概念的目标是为了更好的阐明 AgentScope 在工程实践中解决的问题，以及为开发者提供的帮助，而非给出严谨的定义。\n\n状态\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n状态（State）管理是 AgentScope 框架构建的基础。其中，状态表示对象运行时某一时刻数据的快照。\n\nAgentScope 将对象的“初始化”与“状态管理”分离，对象在初始化后通过 ``load_state_dict`` 和 ``state_dict`` 方法恢复到不同的状态，或导出当前的状态。\n\n在 AgentScope 中，智能体（Agent）、记忆（memory）、长期记忆（Long-term memory）和工具模块（toolkit）都是有状态的对象。\nAgentScope 通过支持嵌套式的状态管理，将这些对象的状态管理联系起来。\n\n消息\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n消息（message）是 AgentScope 最核心的数据结构，用于\n\n- 在智能体之间交换信息，\n- 在用户交互界面显示信息，\n- 在记忆中存储信息，\n- 作为 AgentScope 与不同 LLM API 之间的统一媒介。\n\n工具\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nAgentScope 中的“工具”指的是可调用的 Python 对象，包括\n\n- 函数，\n- 偏函数（Partial function），\n- 实例方法，\n- 类方法，\n- 静态方法，以及\n- 带有 ``__call__`` 方法的可调用实例。\n\n此外，可调用对象可以是\n\n- 异步或同步调用的，\n- 流式或非流式返回结果的。\n\n因此，请放心在 AgentScope 中使用任何调用对象作为智能体的工具。\n\n智能体\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n在 AgentScope 中，智能体（Agent）行为被抽象为 ``AgentBase`` 类中的三个核心函数：\n\n- ``reply``：处理传入的消息并生成响应消息。\n- ``observe``：接收来自环境或其它智能体的消息，但不返回响应。\n- ``print``：将消息显示到目标输出（例如终端、Web 界面）。\n\n为了支持用户实时介入（Realtime Steering），AgentScope 提供了额外\n的 ``handle_interrupt`` 函数来处理智能体回复过程中的用户中断。\n\n此外，ReAct 智能体是 AgentScope 中最重要的智能体，该智能体的回复过程分为两个阶段：\n\n- 推理（Reasoning）：通过调用 LLM 进行推理并生成工具调用\n- 行动（Acting）：执行工具函数。\n\n因此，我们在 ``ReActAgentBase`` 类中提供了两个额外的核心函数，``_reasoning`` 和 ``_acting``。\n\n提示词格式化\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n提示词格式化器（Prompt Formatter）是 AgentScope 中保证 LLM 兼容性的核心组件，负责将消息对象转换为 LLM API 所需的格式。\n\n此外，诸如提示工程、截断和消息验证等附加功能也可以在格式化器中实现。\n\n在格式化器中，\"多智能体\"（或\"多实体\"）概念与常见的多智能体编排概念不同。\n它专注于给定消息中包含多个身份实体的场景，因此 LLM API 中常用的 ``role`` 字段（通常取值为 \"user\"、\"assistant\" 或 \"system\"）无法区分它们。\n\n因此，AgentScope 提供 MultiAgentFormatter 来处理这种场景，通常用于游戏、多人聊天和社交仿真。\n\n.. note:: 多智能体工作流 **!=** 格式化器中的多智能体。例如，即使以下代码片段可能涉及多个\n 智能体（``tool_agent`` 和 ``tool_function`` 的调用者），但是输入的 ``query`` 参数\n 被包装成了 ``role`` 为 **“user”** 消息，因此 ``role`` 字段仍然可以区分它们。\n\n .. code-block:: python\n\n    async def tool_function(query: str) -> str:\n        \\\"\\\"\\\"调用另一个智能体的工具函数\\\"\\\"\\\"\n        msg = Msg(\"user\", query, role=\"user\")\n        tool_agent = Agent(name=\"Programmer\")\n        return await tool_agent(msg)\n\n 理解这种区别有助于开发者了解格式化器部分中单智能体和多智能体的区别。\n\n\n长期记忆\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n虽然 AgentScope 为短期记忆和长期记忆提供了不同的基类，但是 AgentScope 中并没有严格区分它们的作用。\n\n在我们看来，一切都应该是 **需求驱动的**。只要开发者的需求得到了很好的满足，完全可以只使用一个强大的记忆系统。\n\n这里为了确保 AgentScope 的灵活性，我们为长期记忆提供了两种运行和管理方式，开发者可以根据自己的需要进行选择。\n其中“agent_control”模式允许智能体自己主动管理长期记忆，而“static_control”则是传统的由开发者管理的长期记忆\n模式。\n\"\"\"\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/quickstart_message.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _message:\n\n创建消息\n====================\n\n消息是 AgentScope 中的核心概念，用于支持多模态数据、工具 API、信息存储/交换和提示构建。\n\n一个消息由四个字段组成：\n\n- ``name``，\n- ``role``，\n- ``content``，以及\n- ``metadata``\n\n这些字段的类型和含义如下：\n\n.. list-table:: 消息对象中的字段\n    :header-rows: 1\n\n    * - 字段\n      - 类型\n      - 描述\n    * - name\n      - ``str``\n      - 消息发送者的名称/身份\n    * - role\n      - | ``Literal[``\n        |     ``\"system\",``\n        |     ``\"assistant\",``\n        |     ``\"user\"``\n        | ``]``\n      - 消息发送者的角色，必须是 \"system\"、\"assistant\" 或 \"user\" 之一。\n    * - content\n      - ``str | list[ContentBlock]``\n      - 消息包含的数据，可以是字符串或 block 的列表。\n    * - metadata\n      - ``dict[str, JSONSerializableObject] | None``\n      - 包含额外元数据的字典，通常用于结构化输出。\n\n.. tip:: - 在具有多个身份实体的应用程序中，``name`` 字段用于区分不同的身份。\n - 建议将 ``metadata`` 字段用于结构化输出，在 AgentScope 内置的模块中，``metadata`` 不会参与 LLM 的提示构建。\n\n接下来，我们根据不同的场景分别介绍 ``content`` 字段中支持的不同数据结构（block）。\n\"\"\"\n\nfrom agentscope.message import (\n    Msg,\n    Base64Source,\n    TextBlock,\n    ThinkingBlock,\n    ImageBlock,\n    AudioBlock,\n    VideoBlock,\n    ToolUseBlock,\n    ToolResultBlock,\n)\nimport json\n\n# %%\n# 创建文本消息\n# -----------------------------\n# 通过提供 ``name``、``role`` 和 ``content`` 字段来创建消息对象。\n#\n\nmsg = Msg(\n    name=\"Jarvis\",\n    role=\"assistant\",\n    content=\"你好！我能怎么帮助你？\",\n)\n\nprint(f\"发送者的名称: {msg.name}\")\nprint(f\"发送者的角色: {msg.role}\")\nprint(f\"消息的内容: {msg.content}\")\n\n# %%\n# 创建多模态消息\n# --------------------------------------\n# Message 类通过提供不同的 block 结构来支持多模态内容：\n#\n# .. list-table:: AgentScope 中的多模态 block\n#     :header-rows: 1\n#\n#     * - 类\n#       - 描述\n#       - 示例\n#     * - TextBlock\n#       - 纯文本数据\n#       - .. code-block:: python\n#\n#             TextBlock(\n#                type=\"text\",\n#                text=\"Hello, world!\"\n#             )\n#     * - ImageBlock\n#       - 图像数据\n#       - .. code-block:: python\n#\n#             ImageBlock(\n#                type=\"image\",\n#                source=URLSource(\n#                    type=\"url\",\n#                    url=\"https://example.com/image.jpg\"\n#                )\n#             )\n#     * - AudioBlock\n#       - 音频数据\n#       - .. code-block:: python\n#\n#             AudioBlock(\n#                type=\"audio\",\n#                source=URLSource(\n#                    type=\"url\",\n#                    url=\"https://example.com/audio.mp3\"\n#                )\n#             )\n#     * - VideoBlock\n#       - 视频数据\n#       - .. code-block:: python\n#\n#             VideoBlock(\n#                type=\"video\",\n#                source=URLSource(\n#                    type=\"url\",\n#                    url=\"https://example.com/video.mp4\"\n#                )\n#             )\n#\n# 对于 ``ImageBlock``、``AudioBlock`` 和 ``VideoBlock``，还可以使用 base64 编码的字符串作为数据源（Source）：\n#\n\nmsg = Msg(\n    name=\"Jarvis\",\n    role=\"assistant\",\n    content=[\n        TextBlock(\n            type=\"text\",\n            text=\"这是一个包含 base64 编码数据的多模态消息。\",\n        ),\n        ImageBlock(\n            type=\"image\",\n            source=Base64Source(\n                type=\"base64\",\n                media_type=\"image/jpeg\",\n                data=\"/9j/4AAQSkZ...\",\n            ),\n        ),\n        AudioBlock(\n            type=\"audio\",\n            source=Base64Source(\n                type=\"base64\",\n                media_type=\"audio/mpeg\",\n                data=\"SUQzBAAAAA...\",\n            ),\n        ),\n        VideoBlock(\n            type=\"video\",\n            source=Base64Source(\n                type=\"base64\",\n                media_type=\"video/mp4\",\n                data=\"AAAAIGZ0eX...\",\n            ),\n        ),\n    ],\n)\n\n# %%\n# 创建推理消息\n# --------------------------------------\n# ``ThinkingBlock`` 用于支持推理模型，包含模型的思考过程。\n#\n\nmsg_thinking = Msg(\n    name=\"Jarvis\",\n    role=\"assistant\",\n    content=[\n        ThinkingBlock(\n            type=\"thinking\",\n            thinking=\"我正在为 AgentScope 构建一个思考块的示例。\",\n        ),\n        TextBlock(\n            type=\"text\",\n            text=\"这是一个思考块的示例。\",\n        ),\n    ],\n)\n\n# %%\n# .. _tool-block:\n#\n# 创建工具使用/结果消息\n# --------------------------------------\n# ``ToolUseBlock`` 和 ``ToolResultBlock`` 用于支持工具 API：\n#\n\nmsg_tool_call = Msg(\n    name=\"Jarvis\",\n    role=\"assistant\",\n    content=[\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"343\",\n            name=\"get_weather\",\n            input={\n                \"location\": \"Beijing\",\n            },\n        ),\n    ],\n)\n\nmsg_tool_res = Msg(\n    name=\"system\",\n    role=\"system\",\n    content=[\n        ToolResultBlock(\n            type=\"tool_result\",\n            id=\"343\",\n            name=\"get_weather\",\n            output=\"北京的天气是晴天，温度为 25°C。\",\n        ),\n    ],\n)\n\n\n# %%\n# .. tip:: AgentScope 中，通常使用 ``role`` 为“system”的消息来记录工具函数的执行结果。有关 AgentScope 中工具的更多信息，请参考 :ref:`tool` 部分。\n#\n# 序列化和反序列化\n# ------------------------------------------------\n# 消息对象可以分别通过 ``to_dict`` 和 ``from_dict`` 方法进行序列化和反序列化。\n\nserialized_msg = msg.to_dict()\n\nprint(type(serialized_msg))\nprint(json.dumps(serialized_msg, indent=4, ensure_ascii=False))\n\n# %%\n# 从 JSON 格式的数据反序列化消息。\n\nnew_msg = Msg.from_dict(serialized_msg)\n\nprint(type(new_msg))\nprint(f'消息的发送者: \"{new_msg.name}\"')\nprint(f'发送者的角色: \"{new_msg.role}\"')\nprint(f'消息的内容: \"{json.dumps(new_msg.content, indent=4, ensure_ascii=False)}\"')\n\n# %%\n# 属性函数\n# ------------------------------------------------\n# 为了便于使用 Msg 对象，AgentScope 提供了以下函数：\n#\n# .. list-table:: Msg 对象的函数\n#   :header-rows: 1\n#\n#   * - 函数\n#     - 参数\n#     - 描述\n#   * - ``get_text_content``\n#     -\n#     - 将所有 ``TextBlock`` 中的内容收集到单个字符串中（用 \"\\\\n\" 分隔）。\n#   * - ``get_content_blocks``\n#     - ``block_type``\n#     - 返回指定类型的内容块列表。如果未提供 ``block_type``，则以块格式返回全部内容。\n#   * - ``has_content_blocks``\n#     - ``block_type``\n#     - 检查消息是否具有指定类型的内容块。``str`` 内容会被视为 ``TextBlock`` 类型。\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_a2a.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _a2a:\n\nA2A 智能体\n============================\n\nA2A（Agent-to-Agent）是一种开放标准协议，用于实现不同 AI 智能体之间的互操作通信。\n\nAgentScope 从获取 Agent Card 信息和连接远程智能体两个层面提供对 A2A 协议的支持，涉及到的相关 API 如下：\n\n.. list-table:: A2A 相关类\n    :header-rows: 1\n\n    * - 类\n      - 描述\n    * - ``A2AAgent``\n      - 用于与远程 A2A 智能体通信的智能体类\n    * - ``A2AChatFormatter``\n      - 用于在 AgentScope 消息和 A2A 消息/任务格式之间进行转换的格式化器\n    * - ``AgentCardResolverBase``\n      - Agent Card 解析器基类\n    * - ``FileAgentCardResolver``\n      - 从本地 JSON 文件加载 Agent Card 的解析器\n    * - ``WellKnownAgentCardResolver``\n      - 从 URL 的 well-known 路径获取 Agent Card 的解析器\n    * - ``NacosAgentCardResolver``\n      - 从 Nacos Agent 注册中心获取 Agent Card 的解析器\n\n本节将演示如何创建 ``A2aAgent`` 并与远程 A2A 智能体进行通信。\n\n.. note:: 注意 A2A 的支持为**实验性功能**，可能会在未来版本中发生变化。同时由于 A2A 协议自身\n 的局限性，因此功能上 ``A2AAgent`` 无法完全对齐 ``ReActAgent`` 等本地智能体，包括：\n\n - 仅支持 chatbot 场景，即仅支持一个用户与一个智能体之间的对话（不影响 handsoff/router 等使用方式）\n - 不支持在对话过程中实时打断\n - 不支持 agentic 结构化输出\n - 目前实现中，``observe`` 方法收到的消息会被存储在本地，并在调用 ``reply`` 方法时一并发送给远程智能体，因此如果最后若干 ``observe`` 调用后未发生 ``reply`` 调用，则这些消息不会被远程智能体看到\n\n\n\"\"\"\n\n# %%\n# 获取 Agent Card\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# 首先，我们需要获得一个 Agent Card 来连接对应的智能体。Agent Card 中包含了智能体的名称，描述，能力以及连接方式等信息。\n#\n# 手动创建 Agent Card 对象\n# --------------------------------\n#\n# 在已知 Agent Card 各项信息的情况下，可以直接从 `a2a.types.AgentCard` 手动创建 Agent Card 对象。\n#\n\nfrom a2a.types import AgentCard, AgentCapabilities\nfrom v2.nacos import ClientConfig\n\nfrom agentscope.a2a import WellKnownAgentCardResolver, NacosAgentCardResolver\nfrom agentscope.agent import A2AAgent, UserAgent\nfrom agentscope.message import Msg, TextBlock\nfrom agentscope.tool import ToolResponse\n\n# 创建 Agent Card 对象\nagent_card = AgentCard(\n    name=\"Friday\",  # 智能体名称\n    description=\"一个有趣的聊天伙伴\",  # 智能体描述\n    url=\"http://localhost:8000\",  # 智能体的 RPC 服务地址\n    version=\"1.0.0\",  # 智能体版本\n    capabilities=AgentCapabilities(  # 智能体能力配置\n        push_notifications=False,\n        state_transition_history=True,\n        streaming=True,\n    ),\n    default_input_modes=[\"text/plain\"],  # 支持的输入格式\n    default_output_modes=[\"text/plain\"],  # 支持的输出格式\n    skills=[],  # 智能体技能列表\n)\n\n# %%\n#\n# 从远程服务获取 Agent Card\n# --------------------------------\n# 同时，AgentScope 也支持通过多种方式动态获取 Agent Card，包括从本地文件加载、从远程服务 (well-known server) 的标准路径获取以及从 Nacos 注册中心获取等。\n# 这里以 ``WellKnownAgentCardResolver`` 为例，从远程服务的标准路径获取 Agent Card：\n#\n\n\nasync def agent_card_from_well_known_website() -> AgentCard:\n    \"\"\"从远程服务的 well-known 路径获取 Agent Card 的示例。\"\"\"\n    # 创建 Agent Card 解析器\n    resolver = WellKnownAgentCardResolver(\n        base_url=\"http://localhost:8000\",\n    )\n    # 获取并返回 Agent Card\n    return await resolver.get_agent_card()\n\n\n# %%\n# 从本地文件加载 Agent Card\n# --------------------------------\n#\n# ``FileAgentCardResolver`` 类支持从本地 JSON 文件加载 Agent Card，适用于配置文件管理的场景。\n# 一个 JSON 格式的 Agent Card 样例如下所示：\n#\n# .. code-block:: json\n#     :caption: 示例 Agent Card JSON 文件内容\n#\n#     {\n#         \"name\": \"RemoteAgent\",\n#         \"url\": \"http://localhost:8000\",\n#         \"description\": \"远程 A2A 智能体\",\n#         \"version\": \"1.0.0\",\n#         \"capabilities\": {},\n#         \"default_input_modes\": [\"text/plain\"],\n#         \"default_output_modes\": [\"text/plain\"],\n#         \"skills\": []\n#     }\n#\n# 通过 ``FileAgentCardResolver`` 可以方便地加载该文件：\n#\n\n\nasync def agent_card_from_file() -> AgentCard:\n    \"\"\"从本地 JSON 文件加载 Agent Card 的示例。\"\"\"\n    from agentscope.a2a import FileAgentCardResolver\n\n    # 从 JSON 文件加载 Agent Card\n    resolver = FileAgentCardResolver(\n        file_path=\"./agent_card.json\",  # JSON 文件路径\n    )\n    # 获取并返回 Agent Card\n    return await resolver.get_agent_card()\n\n\n# %%\n# 从 Nacos 注册中心获取 Agent Card\n# --------------------------------\n#\n# Nacos 是一款开源的动态服务发现、配置管理和服务管理平台，在 3.1.0 版本中引入了 Agent 注册中心功能，支持 A2A 智能体的分布式注册、发现和版本管理。\n#\n# .. important:: 使用 ``NacosAgentCardResolver`` 的前提是用户已经部署了 3.1.0 版本以上的 Nacos 服务端，部署与注册流程请参考`官方文档 <https://nacos.io/docs/latest/quickstart/quick-start>`_。\n#\n\n\nasync def agent_card_from_nacos() -> AgentCard:\n    \"\"\"从 Nacos 注册中心获取 Agent Card 的示例。\"\"\"\n\n    # 创建 Nacos Agent Card 解析器\n    resolver = NacosAgentCardResolver(\n        remote_agent_name=\"my-remote-agent\",  # Nacos 中注册的智能体名称\n        nacos_client_config=ClientConfig(\n            server_addresses=\"http://localhost:8848\",  # Nacos 服务器地址\n            # 其他可选配置项\n        ),\n    )\n    # 获取并返回 Agent Card\n    return await resolver.get_agent_card()\n\n\n# %%\n# 构建 A2A 智能体\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# AgentScope 提供的 ``A2AAgent`` 类用于与远程 A2A 智能体进行通信，其使用方式与普通智能体类似。\n\nagent = A2AAgent(agent_card=agent_card)\n\n# %%\n# 利用 ``A2AAgent``，开发者可以构建 chatbot 场景的聊天，或是封装成工具函数，从而构建 handsoff/router 等更复杂的应用场景。\n# 目前 ``A2AAgent`` 支持的格式协议转换由 ``agentscope.formatter.A2AChatFormatter`` 负责，支持\n#\n# - 将 AgentScope 的 ``Msg`` 消息转换为 A2A 协议的 ``Message`` 格式\n# - 将 A2A 协议的响应转换回 AgentScope 的 ``Msg`` 格式\n# - 将 A2A 协议的 ``Task`` 相应转换成 AgentScope 的 ``Msg`` 格式\n# - 支持文本、图像、音频、视频等多种内容类型\n#\n\n\nasync def a2a_in_chatbot() -> None:\n    \"\"\"使用 A2AAgent 进行聊天的示例。\"\"\"\n\n    user = UserAgent(\"user\")\n\n    msg = None\n    while True:\n        msg = await user(msg)\n        if msg.get_text_content() == \"exit\":\n            break\n        msg = await agent(msg)\n\n\n# %%\n# 或是如下封装成工具函数用于调用：\n\n\nasync def create_worker(query: str) -> ToolResponse:\n    \"\"\"通过子智能体完成给定的任务\n\n    Args:\n        query (`str`):\n            需要子智能体完成的任务描述\n    \"\"\"\n    res = await agent(\n        Msg(\"user\", query, \"user\"),\n    )\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=res.get_text_content(),\n            ),\n        ],\n    )\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_agent.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _agent:\n\n智能体\n=========================\n\n在章我们首先重点介绍 AgentScope 中的 ReAct 智能体，然后简要介绍如何从零开始自定义智能体。\n\nReAct 智能体\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n在 AgentScope 中，``ReActAgent`` 类将各种功能集成到最终实现中，具体包括\n\n.. list-table:: ``ReActAgent`` 的功能特性\n    :header-rows: 1\n\n    * - 功能特性\n      - 参考文档\n    * - 支持实时介入（Realtime Steering）\n      -\n    * - 支持记忆压缩\n      -\n    * - 支持并行工具调用\n      -\n    * - 支持结构化输出\n      -\n    * - 支持智能体自主管理工具（Meta tool）\n      - :ref:`tool`\n    * - 支持函数粒度的 MCP 控制\n      - :ref:`mcp`\n    * - 支持智能体自主控制长期记忆\n      - :ref:`long-term-memory`\n    * - 支持自动状态管理\n      - :ref:`state`\n\n\n由于篇幅限制，本章我们仅演示 ``ReActAgent`` 类的前三个功能特性，其它功能我们在对应的章节进行介绍。\n\n\"\"\"\n\nimport asyncio\nimport json\nimport os\nfrom datetime import datetime\nimport time\n\nfrom pydantic import BaseModel, Field\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.message import TextBlock, Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import Toolkit, ToolResponse\n\n\n# %%\n# 实时控制\n# ---------------------------------------\n#\n# 实时控制指 **允许用户随时中断智能体的回复，介入智能体的执行过程**，AgentScope 基于 asyncio 取消机制实现了该功能。\n#\n# 具体来说，AgentScope 中智能体提供了 ``interrupt`` 方法，当该函数被调用时，它将取消当前正在执行的 `reply` 函数，并执行 ``handle_interrupt`` 方法进行后处理。\n#\n# .. hint:: 结合 :ref:`tool` 中提到的 AgentScope 支持工具函数流式返回结果的功能，工具执行过程中如果执行时间过长或偏离用户期望，用户可以通过在终端中按 Ctrl+C 或在代码中调用智能体的\n#  ``interrupt`` 方法来中断工具执行。\n#\n# .. hint:: ``ReActAgent`` 中提供了完善的中断逻辑，智能体的记忆和状态会在中断发生时被正确的保存。\n#\n# 中断逻辑已在 ``AgentBase`` 类中作为基本功能实现，并提供 ``handle_interrupt`` 抽象方法供用户自定义\n# 中断的后处理，如下所示：\n#\n# .. code-block:: python\n#\n#     # AgentBase 的代码片段\n#     class AgentBase:\n#         ...\n#         async def __call__(self, *args: Any, **kwargs: Any) -> Msg:\n#             ...\n#             reply_msg: Msg | None = None\n#             try:\n#                 self._reply_task = asyncio.current_task()\n#                 reply_msg = await self.reply(*args, **kwargs)\n#\n#             except asyncio.CancelledError:\n#                 # 捕获中断并通过 handle_interrupt 方法处理\n#                 reply_msg = await self.handle_interrupt(*args, **kwargs)\n#\n#             ...\n#\n#         @abstractmethod\n#         async def handle_interrupt(self, *args: Any, **kwargs: Any) -> Msg:\n#             pass\n#\n#\n# 在 ``ReActAgent`` 类的实现中，我们返回一个固定消息\"I noticed that you have interrupted me. What can I do for you?\"，如下所示：\n#\n# .. figure:: ../../_static/images/interruption_zh.gif\n#     :width: 100%\n#     :align: center\n#     :class: bordered-image\n#     :alt: 中断示例\n#\n#     中断智能体 ``reply`` 的执行过程\n#\n# 开发者可以通过覆盖 ``handle_interrupt`` 函数实现自定义的中断后处理逻辑，例如，调用 LLM 生成对中断的简单响应。\n#\n#\n# 记忆压缩\n# ----------------------------------------\n# 随着对话的不断增长，记忆中的 token 数量可能会超过模型的上下文限制或导致推理速度变慢。\n# ``ReActAgent`` 提供了自动记忆压缩功能来解决这个问题。\n#\n# **基础用法**\n#\n# 要启用记忆压缩，在初始化 ``ReActAgent`` 时提供一个 ``CompressionConfig`` 实例：\n#\n# .. code-block:: python\n#\n#     from agentscope.agent import ReActAgent\n#     from agentscope.token import CharTokenCounter\n#\n#     agent = ReActAgent(\n#         name=\"助手\",\n#         sys_prompt=\"你是一个有用的助手。\",\n#         model=model,\n#         formatter=formatter,\n#         compression_config=ReActAgent.CompressionConfig(\n#             enable=True,\n#             agent_token_counter=CharTokenCounter(),  # 智能体的 token 计数器\n#             trigger_threshold=10000,  # 超过 10000 个 token 时触发压缩\n#             keep_recent=3,            # 保持最近 3 条消息不被压缩\n#         ),\n#     )\n#\n# 启用记忆压缩后，智能体会监控其记忆中的 token 数量。\n# 一旦超过 ``trigger_threshold``，智能体会自动：\n#\n# 1. 识别尚未被压缩的消息（通过 ``exclude_mark``）\n# 2. 保持最近 ``keep_recent`` 条消息不被压缩（以保留最近的上下文）\n# 3. 将较早的消息发送给 LLM 生成结构化摘要\n# 4. 使用 ``MemoryMark.COMPRESSED`` 标记已压缩的消息（通过 ``update_messages_mark``）\n# 5. 将摘要存储在记忆中（通过 ``update_compressed_summary``）\n#\n# .. important:: 压缩采用**标记机制**而非替换消息。旧消息被标记为已压缩，并通过 ``exclude_mark=MemoryMark.COMPRESSED`` 在后续检索中被排除，而生成的摘要则单独存储，在需要时检索。这种方式保留了原始消息，允许灵活的记忆管理。关于标记功能的更多详情，请参考 :ref:`memory`。\n#\n# 默认情况下，压缩摘要被结构化为五个关键字段：\n#\n# - **task_overview**：用户的核心请求和成功标准\n# - **current_state**：到目前为止已完成的工作，包括文件和输出\n# - **important_discoveries**：技术约束、决策、错误和失败的尝试\n# - **next_steps**：完成任务所需的具体操作\n# - **context_to_preserve**：用户偏好、领域细节和做出的承诺\n#\n# **自定义压缩**\n#\n# 可以通过指定 ``summary_schema``、``summary_template`` 和 ``compression_prompt`` 参数来自定义压缩的工作方式。\n#\n# - **summary_schema**：使用 Pydantic 模型定义压缩摘要的结构\n# - **compression_prompt**：指导 LLM 如何生成摘要\n# - **summary_template**：格式化压缩摘要如何呈现给智能体\n#\n# 下面是一个自定义压缩的示例：\n#\n# .. code-block:: python\n#\n#     from pydantic import BaseModel, Field\n#\n#     # 定义自定义摘要结构\n#     class CustomSummary(BaseModel):\n#         main_topic: str = Field(\n#             max_length=200,\n#             description=\"对话的主题\"\n#         )\n#         key_points: str = Field(\n#             max_length=400,\n#             description=\"讨论的重要观点\"\n#         )\n#         pending_tasks: str = Field(\n#             max_length=200,\n#             description=\"待完成的任务\"\n#         )\n#\n#     # 使用自定义压缩配置创建智能体\n#     agent = ReActAgent(\n#         name=\"助手\",\n#         sys_prompt=\"你是一个有用的助手。\",\n#         model=model,\n#         formatter=formatter,\n#         compression_config=ReActAgent.CompressionConfig(\n#             enable=True,\n#             agent_token_counter=CharTokenCounter(),\n#             trigger_threshold=10000,\n#             keep_recent=3,\n#             # 结构化摘要的自定义 schema\n#             summary_schema=CustomSummary,\n#             # 指导压缩的自定义提示\n#             compression_prompt=(\n#                 \"<system-hint>请总结上述对话，\"\n#                 \"重点关注主题、关键讨论点和待完成任务。</system-hint>\"\n#             ),\n#             # 格式化摘要的自定义模板\n#             summary_template=(\n#                 \"<system-info>对话摘要：\\n\"\n#                 \"主题：{main_topic}\\n\\n\"\n#                 \"关键观点：\\n{key_points}\\n\\n\"\n#                 \"待完成任务：\\n{pending_tasks}\"\n#                 \"</system-info>\"\n#             ),\n#         ),\n#     )\n#\n# ``summary_template`` 使用 ``summary_schema`` 中定义的字段作为占位符\n# （例如 ``{main_topic}``、``{key_points}``）。在 LLM 生成结构化摘要后，\n# 这些占位符将被实际值替换。\n#\n# .. note:: 智能体确保工具使用和工具结果对在压缩过程中保持在一起，以维护对话流程的完整性。\n#\n# .. tip:: 可以通过指定不同的 ``compression_model`` 和 ``compression_formatter`` 来使用更小、更快的模型进行压缩，以降低成本和延迟。\n#\n#\n#\n# 并行工具调用\n# ----------------------------------------\n# ``ReActAgent`` 通过在其构造函数中提供 ``parallel_tool_calls`` 参数来支持并行工具调用。\n# 当 LLM 生成多个工具调用且 ``parallel_tool_calls`` 设置为 ``True`` 时，\n# 它们将通过 ``asyncio.gather`` 函数并行执行。\n#\n# .. note:: ``ReActAgent`` 中的工具并行调用是基于异步 ``asyncio.gather`` 实现的，因此，只有当工具函数是异步函数，同时工具函数内也为异步逻辑时，才能最大程度发挥工具并行执行的效果\n#\n# .. note:: 运行时请确保模型层面支持工具并行调用，并且相应参数设置正确（可以通过 ``generate_kwargs`` 传入），例如对于DashScope API，需要设置 ``parallel_tool_calls`` 为 ``True``，否则将无法进行并行工具调用。\n\n\n# 准备一个工具函数\nasync def example_tool_function(tag: str) -> ToolResponse:\n    \"\"\"一个示例工具函数\"\"\"\n    start_time = datetime.now().strftime(\"%H:%M:%S.%f\")\n\n    # 休眠 3 秒以模拟长时间运行的任务\n    await asyncio.sleep(3)\n\n    end_time = datetime.now().strftime(\"%H:%M:%S.%f\")\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=f\"标签 {tag} 开始于 {start_time}，结束于 {end_time}。\",\n            ),\n        ],\n    )\n\n\ntoolkit = Toolkit()\ntoolkit.register_tool_function(example_tool_function)\n\n# 创建一个 ReAct 智能体\nagent = ReActAgent(\n    name=\"Jarvis\",\n    sys_prompt=\"你是一个名为 Jarvis 的有用助手。\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        # 启用并行工具调用\n        generate_kwargs={\n            \"parallel_tool_calls\": True,\n        },\n    ),\n    memory=InMemoryMemory(),\n    formatter=DashScopeChatFormatter(),\n    toolkit=toolkit,\n    parallel_tool_calls=True,\n)\n\n\nasync def example_parallel_tool_calls() -> None:\n    \"\"\"并行工具调用示例\"\"\"\n    # 提示智能体同时生成两个工具调用\n    await agent(\n        Msg(\n            \"user\",\n            \"同时生成两个 'example_tool_function' 函数的工具调用，标签分别为 'tag1' 和 'tag2'，以便它们可以并行执行。\",\n            \"user\",\n        ),\n    )\n\n\nasyncio.run(example_parallel_tool_calls())\n\n# %%\n# 结构化输出\n# ----------------------------------------\n# AgentScope 中的结构化输出是与工具调用紧密结合的。具体来说，``ReActAgent`` 类在其 ``__call__`` 函数中接收 ``pydantic.BaseModel`` 的子类作为 ``structured_model`` 参数。\n# 从而提供复杂的结构化输出限制。\n# 然后我们可以从 返回消息的 ``metadata`` 字段获取结构化输出。\n#\n# 以介绍爱因斯坦为例：\n#\n\n# 创建一个 ReAct 智能体\nagent = ReActAgent(\n    name=\"Jarvis\",\n    sys_prompt=\"你是一个名为 Jarvis 的有用助手。\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n    ),\n    formatter=DashScopeChatFormatter(),\n)\n\n\n# 结构化模型\nclass Model(BaseModel):\n    name: str = Field(description=\"人物的姓名\")\n    description: str = Field(description=\"人物的一句话描述\")\n    age: int = Field(description=\"年龄\")\n    honor: list[str] = Field(description=\"人物荣誉列表\")\n\n\nasync def example_structured_output() -> None:\n    \"\"\"结构化输出示例\"\"\"\n    res = await agent(\n        Msg(\n            \"user\",\n            \"介绍爱因斯坦\",\n            \"user\",\n        ),\n        structured_model=Model,\n    )\n    print(\"\\n结构化输出：\")\n    print(json.dumps(res.metadata, indent=4, ensure_ascii=False))\n\n\nasyncio.run(example_structured_output())\n\n# %%\n# 自定义智能体\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope 提供了两个基类：``AgentBase`` 和 ``ReActAgentBase``，它们在抽象方法和支持的钩子函数方面有所不同。\n# 具体来说，``ReActAgentBase`` 扩展了 ``AgentBase``，增加了额外的 ``_reasoning`` 和 ``_acting`` 抽象方法，以及它们的前置和后置钩子函数。\n#\n# 开发者可以根据需要选择继承这两个基类中的任一个。\n# 我们总结了 ``agentscope.agent`` 模块下的智能体如下：\n#\n# .. list-table:: AgentScope 中的智能体类\n#     :header-rows: 1\n#\n#     * - 类\n#       - 抽象方法\n#       - 支持的钩子函数\n#       - 描述\n#     * - ``AgentBase``\n#       - | ``reply``\n#         | ``observe``\n#         | ``print``\n#         | ``handle_interrupt``\n#       - | pre\\_/post_reply\n#         | pre\\_/post_observe\n#         | pre\\_/post_print\n#       - 所有智能体的基类，提供基本接口和钩子。\n#     * - ``ReActAgentBase``\n#       - | ``reply``\n#         | ``observe``\n#         | ``print``\n#         | ``handle_interrupt``\n#         | ``_reasoning``\n#         | ``_acting``\n#       - | pre\\_/post_reply\n#         | pre\\_/post_observe\n#         | pre\\_/post_print\n#         | pre\\_/post_reasoning\n#         | pre\\_/post_acting\n#       - ReAct 类智能体的抽象类，扩展了 ``AgentBase``，增加了 ``_reasoning`` 和 ``_acting`` 抽象方法及其钩子。\n#     * - ``ReActAgent``\n#       - \\-\n#       - | pre\\_/post_reply\n#         | pre\\_/post_observe\n#         | pre\\_/post_print\n#         | pre\\_/post_reasoning\n#         | pre\\_/post_acting\n#       - ``ReActAgentBase`` 的实现\n#     * - ``UserAgent``\n#       -\n#       -\n#       - 代表用户的特殊智能体，用于与智能体交互\n#     * - ``A2aAgent``\n#       - \\-\n#       - | pre\\_/post_reply\n#         | pre\\_/post_observe\n#         | pre\\_/post_print\n#       - 用于与远程 A2A 代理通信的智能体，详见 :ref:`a2a`\n#\n#\n#\n# 进一步阅读\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# - :ref:`tool`\n# - :ref:`hook`\n# - :ref:`a2a`\n#\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_agent_skill.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _agent_skill:\n\n智能体技能\n============================\n\n`智能体技能（Agent skill） <https://claude.com/blog/skills>`_ 是 Anthropic 提出的一种提升智能体在特定任务上能力的方法。\n\nAgentScope 通过 ``Toolkit`` 类提供了对智能体技能的内置支持，让开发者可以注册和管理智能体技能。\n\n相关 API 如下：\n\n.. list-table:: ``Toolkit`` 类中的智能体技能 API\n    :header-rows: 1\n\n    * - API\n      - 描述\n    * - ``register_agent_skill``\n      - 从指定目录注册智能体技能\n    * - ``remove_agent_skill``\n      - 根据名称移除已注册的智能体技能\n    * - ``get_agent_skill_prompt``\n      - 获取所有已注册智能体技能的提示词，可以附加到智能体的系统提示词中\n\n本节将演示如何注册智能体技能并在 ReActAgent 类中使用它们。\n\"\"\"\nimport os\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import Toolkit\n\n# %%\n# 注册智能体技能\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# 首先，我们需要准备一个智能体技能目录，该目录需要遵循 `Anthropic blog <https://claude.com/blog/skills>`_ 中指定的要求。\n#\n# .. note:: 技能目录必须包含一个 ``SKILL.md`` 文件，其中包含 YAML 前置元数据和指令说明。\n#\n# 这里我们创建一个示例技能目录 ``sample_skill``，包含以下文件：\n#\n# .. code-block:: markdown\n#\n#   ---\n#   name: sample_skill\n#   description: 用于演示的示例智能体技能\n#   ---\n#\n#   # 示例技能\n#   ...\n#\n\nos.makedirs(\"sample_skill\", exist_ok=True)\nwith open(\"sample_skill/SKILL.md\", \"w\", encoding=\"utf-8\") as f:\n    f.write(\n        \"\"\"---\nname: sample_skill\ndescription: 用于演示的示例智能体技能\n---\n\n# 示例技能\n...\n\"\"\",\n    )\n\n# %%\n# 然后，我们可以使用 ``Toolkit`` 类的 ``register_agent_skill`` API 注册技能。\n#\n\ntoolkit = Toolkit()\n\ntoolkit.register_agent_skill(\"sample_skill\")\n\n# %%\n# 之后，我们可以使用 ``get_agent_skill_prompt`` API 获取所有已注册智能体技能的提示词\n\nagent_skill_prompt = toolkit.get_agent_skill_prompt()\nprint(\"智能体技能提示词:\")\nprint(agent_skill_prompt)\n\n# %%\n# 当然，我们也可以在创建 ``Toolkit`` 实例时自定义提示词模板。\n\ncustom_toolkit = Toolkit(\n    # 向智能体/大语言模型介绍如何使用技能的指令\n    agent_skill_instruction=\"<system-info>为你提供了一组技能，每个技能都在一个目录中，并由 SKILL.md 文件进行描述。</system-info>\",\n    # 用于格式化每个技能提示词的模板，必须包含 {name}、{description} 和 {dir} 字段\n    agent_skill_template=\"- {name}(in directory '{dir}'): {description}\",\n)\n\ncustom_toolkit.register_agent_skill(\"sample_skill\")\nagent_skill_prompt = custom_toolkit.get_agent_skill_prompt()\nprint(\"自定义智能体技能提示词:\")\nprint(agent_skill_prompt)\n\n# %%\n# 在 ReActAgent 中集成智能体技能\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# AgentScope 中的 `ReActAgent` 类会自动将智能体技能提示词附加到系统提示词中。\n#\n# 我们可以按如下方式创建一个带有已注册智能体技能的 ReAct 智能体：\n#\n# .. important:: 使用智能体技能时，智能体必须配备文本文件读取或 shell 命令工具，以便访问 `SKILL.md` 文件中的技能指令。\n#\n\nagent = ReActAgent(\n    name=\"Friday\",\n    sys_prompt=\"你是一个名为 Friday 的智能助手。\",\n    model=DashScopeChatModel(\n        model_name=\"qwen3-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n    ),\n    memory=InMemoryMemory(),\n    formatter=DashScopeChatFormatter(),\n    toolkit=toolkit,\n)\n\nprint(\"带有智能体技能的系统提示词:\")\nprint(agent.sys_prompt)\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_embedding.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _embedding:\n\n嵌入(Embedding)\n=========================\n\nAgentScope 中，嵌入模块提供了用于向量生成的统一接口，具有以下特性：\n\n- 支持 **缓存 embedding** 以避免冗余的 API 调用\n- 支持 **不同 embedding API 提供商** 并提供一致的接口\n\nAgentScope 内置支持以下 API：\n\n.. list-table::\n    :header-rows: 1\n\n    * - API 提供商\n      - 类\n    * - OpenAI\n      - ``OpenAITextEmbedding``\n    * - Gemini\n      - ``GeminiTextEmbedding``\n    * - DashScope\n      - ``DashScopeTextEmbedding``, ``DashScopeMultiModalEmbedding``\n    * - Ollama\n      - ``OllamaTextEmbedding``\n\n所有类都继承自 ``EmbeddingModelBase``，实现了 ``__call__`` 方法并生成包含嵌入和使用信息的 ``EmbeddingResponse`` 对象。\n其中 ``DashScopeMultiModalEmbedding`` 支持文本，图像和视频的多模态嵌入。\n\n以 DashScope 嵌入类为例，可以按如下方式使用：\n\"\"\"\n\nimport asyncio\nimport os\nimport tempfile\n\nfrom agentscope.embedding import DashScopeTextEmbedding, FileEmbeddingCache\n\n\nasync def example_dashscope_embedding() -> None:\n    \"\"\"DashScope 文本嵌入的使用示例。\"\"\"\n    texts = [\n        \"法国的首都是什么？\",\n        \"巴黎是法国的首都城市。\",\n    ]\n\n    # 初始化 DashScope 文本嵌入实例\n    embedding_model = DashScopeTextEmbedding(\n        model_name=\"text-embedding-v2\",\n        api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n    )\n\n    # 从模型获取嵌入\n    response = await embedding_model(texts)\n\n    print(\"嵌入 ID: \", response.id)\n    print(\"嵌入创建时间: \", response.created_at)\n    print(\"嵌入使用情况: \", response.usage)\n    print(\"嵌入向量:\")\n    print(response.embeddings)\n\n\nasyncio.run(example_dashscope_embedding())\n\n# %%\n# 可以通过继承 ``EmbeddingModelBase`` 并实现 ``__call__`` 方法来自定义 embedding 模型。\n#\n# Embedding 缓存\n# ---------------------\n# AgentScope 提供了用于缓存 embedding 的基类 ``EmbeddingCacheBase``，以及基于文件的实现 ``FileEmbeddingCache``。\n# 它在 embedding 模块中的工作方式如下：\n#\n# .. image:: ../../_static/images/embedding_cache.png\n#   :align: center\n#   :width: 90%\n#\n# 要使用缓存，只需将 ``FileEmbeddingCache`` 实例（或自定义缓存）传给模型的构造函数，如下所示：\n#\n\n\nasync def example_embedding_cache() -> None:\n    \"\"\"演示带有缓存功能的 embedding。\"\"\"\n    # 示例文本\n    texts = [\n        \"法国的首都是什么？\",\n        \"巴黎是法国的首都城市。\",\n    ]\n\n    # 为缓存演示创建临时目录\n    # 在实际应用中，建议使用持久目录以最大发挥缓存效果\n    cache_dir = tempfile.mkdtemp(prefix=\"embedding_cache_\")\n    print(f\"使用缓存目录: {cache_dir}\")\n\n    # 使用缓存初始化嵌入模型\n    # 为演示目的，我们将缓存限制为 100 个文件和 10MB\n    embedder = DashScopeTextEmbedding(\n        model_name=\"text-embedding-v3\",\n        api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n        embedding_cache=FileEmbeddingCache(\n            cache_dir=cache_dir,\n            max_file_number=100,\n            max_cache_size=10,  # 最大缓存大小（MB）\n        ),\n    )\n\n    # 第一次调用 - 将从 API 获取并存储在缓存中\n    print(\"\\n=== 第一次 API 调用（无缓存命中）===\")\n    start_time = asyncio.get_event_loop().time()\n    response1 = await embedder(texts)\n    elapsed_time1 = asyncio.get_event_loop().time() - start_time\n    print(f\"来源: {response1.source}\")  # 应该是 'api'\n    print(f\"耗时: {elapsed_time1:.4f} 秒\")\n    print(f\"使用的 token: {response1.usage.tokens}\")\n\n    # 使用相同文本的第二次调用 - 应该使用缓存\n    print(\"\\n=== 第二次 API 调用（预期缓存命中）===\")\n    start_time = asyncio.get_event_loop().time()\n    response2 = await embedder(texts)\n    elapsed_time2 = asyncio.get_event_loop().time() - start_time\n    print(f\"来源: {response2.source}\")  # 应该是 'cache'\n    print(f\"耗时: {elapsed_time2:.4f} 秒\")\n    print(\n        f\"使用的 token: {response2.usage.tokens}\",\n    )  # 缓存结果应该为 0\n    print(\n        f\"速度提升: 使用缓存快 {elapsed_time1 / elapsed_time2:.1f} 倍\",\n    )\n\n\nasyncio.run(example_embedding_cache())\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_eval.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _eval:\n\n智能体评测\n=========================\n\nAgentScope 提供了一个内置的评测框架，用于评测智能体在不同任务和基准测试中的性能，主要特性包括：\n\n- 基于 `Ray <https://github.com/ray-project/ray>`_ 的并行和分布式评估\n- 支持中断后继续评估\n- [开发中] 评估结果可视化\n\n.. note:: 我们正在持续集成新的基准测试到 AgentScope 中：\n\n - ✅ `ACEBench <https://github.com/ACEBench/ACEBench>`_\n - 🚧 `GAIA <https://huggingface.co/datasets/gaia-benchmark/GAIA/tree/main>`_ 基准测试\n\n\n概述\n---------------------------\n\nAgentScope 评估框架由几个关键组件组成：\n\n- **基准测试 (Benchmark)**: 用于系统性评估的任务集合\n    - **任务 (Task)**: 包含输入、标准答案和指标的独立评估单元\n        - **指标 (Metric)**: 评估解决方案质量的测量函数\n- **评估器 (Evaluator)**: 运行评估的引擎，聚合结果并分析性能\n    - **评估器存储 (Evaluator Storage)**: 用于记录和检索评估结果的持久化存储\n- **解决方案 (Solution)**: 用户定义的解决方案\n\n.. figure:: ../../_static/images/evaluation.png\n    :width: 90%\n    :alt: AgentScope 评估框架\n\n    *AgentScope 评估框架*\n\nAgentScope 当前的实现包括：\n\n- 评估器：\n    - ``RayEvaluator``: 基于 ray 的评估器，支持并行和分布式评估。\n    - ``GeneralEvaluator``: 通用评估器，按顺序运行任务，便于调试。\n- 基准测试：\n    - ``ACEBench``: 用于评估智能体能力的基准测试。\n\n我们在 `GitHub 仓库 <https://github.com/agentscope-ai/agentscope/tree/main/examples/evaluation/ace_bench>`_ 中提供了一个使用 ``RayEvaluator`` 和 ACEBench 中智能体多步骤任务的玩具示例。\n\n核心组件\n---------------\n我们将构建一个简单的玩学问题基准测试来演示如何使用 AgentScope 评估模块。\n\"\"\"\n\nTOY_BENCHMARK = [\n    {\n        \"id\": \"math_problem_1\",\n        \"question\": \"What is 2 + 2?\",\n        \"ground_truth\": 4.0,\n        \"tags\": {\n            \"difficulty\": \"easy\",\n            \"category\": \"math\",\n        },\n    },\n    {\n        \"id\": \"math_problem_2\",\n        \"question\": \"What is 12345 + 54321 + 6789 + 9876?\",\n        \"ground_truth\": 83331,\n        \"tags\": {\n            \"difficulty\": \"medium\",\n            \"category\": \"math\",\n        },\n    },\n]\n\n# %%\n# 从任务、解决方案和指标到基准测试\n# ~~~~~~~~~~~~~~~~~~~\n#\n# - 一个 ``SolutionOutput`` (Agent解决方案输出) 包含智能体生成的所有信息，包括轨迹和最终输出。\n# - 一个 ``Metric`` (评测指标) 代表一个单一的评估可调用实例，它将生成的解决方案（例如，轨迹或最终输出）与标准答案进行比较。\n# 在这个示例中，我们定义了一个指标，简单地检查解决方案中的 ``output`` 字段是否与标准答案匹配。\n\nfrom agentscope.evaluate import (\n    SolutionOutput,\n    MetricBase,\n    MetricResult,\n    MetricType,\n)\n\n\nclass CheckEqual(MetricBase):\n    def __init__(\n        self,\n        ground_truth: float,\n    ):\n        super().__init__(\n            name=\"math check number equal\",\n            metric_type=MetricType.NUMERICAL,\n            description=\"Toy metric checking if two numbers are equal\",\n            categories=[],\n        )\n        self.ground_truth = ground_truth\n\n    async def __call__(\n        self,\n        solution: SolutionOutput,\n    ) -> MetricResult:\n        if solution.output == self.ground_truth:\n            return MetricResult(\n                name=self.name,\n                result=1.0,\n                message=\"Correct\",\n            )\n        else:\n            return MetricResult(\n                name=self.name,\n                result=0.0,\n                message=\"Incorrect\",\n            )\n\n\n# %%\n# - 一个 ``Task`` (任务) 是基准测试中的一个单元，包含智能体执行和评估所需的所有信息（例如，输入/查询及其标准答案）。\n# - 一个 ``Benchmark`` (基准测试) 组织多个任务进行系统性评估。\n\nfrom typing import Generator\nfrom agentscope.evaluate import (\n    Task,\n    BenchmarkBase,\n)\n\n\nclass ToyBenchmark(BenchmarkBase):\n    def __init__(self):\n        super().__init__(\n            name=\"Toy bench\",\n            description=\"A toy benchmark for demonstrating the evaluation module.\",\n        )\n        self.dataset = self._load_data()\n\n    @staticmethod\n    def _load_data() -> list[Task]:\n        dataset = []\n        for item in TOY_BENCHMARK:\n            dataset.append(\n                Task(\n                    id=item[\"id\"],\n                    input=item[\"question\"],\n                    ground_truth=item[\"ground_truth\"],\n                    tags=item.get(\"tags\", {}),\n                    metrics=[\n                        CheckEqual(item[\"ground_truth\"]),\n                    ],\n                    metadata={},\n                ),\n            )\n        return dataset\n\n    def __iter__(self) -> Generator[Task, None, None]:\n        \"\"\"遍历基准测试。\"\"\"\n        for task in self.dataset:\n            yield task\n\n    def __getitem__(self, index: int) -> Task:\n        \"\"\"根据索引获取任务。\"\"\"\n        return self.dataset[index]\n\n    def __len__(self) -> int:\n        \"\"\"获取基准测试的长度。\"\"\"\n        return len(self.dataset)\n\n\n# %%\n# 评估器\n# ~~~~~~~~~~\n#\n# 评估器 (Evaluators) 管理评估过程。它们可以自动遍历\n# 基准测试中的任务，并将每个任务输入到解决方案生成函数中，\n# 开发者需要在其中定义运行智能体和检索\n# 执行结果和轨迹的逻辑。下面是一个\n# 使用我们的玩具基准测试运行 ``GeneralEvaluator`` (通用评估器) 的示例。如果有一个大型\n# 基准测试，开发者希望通过并行化更高效地进行评估，\n# ``RayEvaluator`` (Ray评估器) 也可作为内置解决方案使用。\n\n\nimport os\nimport asyncio\nfrom typing import Callable\nfrom pydantic import BaseModel\n\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.agent import ReActAgent\n\nfrom agentscope.evaluate import (\n    GeneralEvaluator,\n    FileEvaluatorStorage,\n)\n\n\nclass ToyBenchAnswerFormat(BaseModel):\n    answer_as_number: float\n\n\nasync def toy_solution_generation(\n    task: Task,\n    pre_hook: Callable,\n) -> SolutionOutput:\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"You are a helpful assistant named Friday. \"\n        \"Your target is to solve the given task with your tools. \"\n        \"Try to solve the task as best as you can.\",\n        model=DashScopeChatModel(\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            model_name=\"qwen-max\",\n            stream=False,\n        ),\n        formatter=DashScopeChatFormatter(),\n    )\n    agent.register_instance_hook(\n        \"pre_print\",\n        \"save_logging\",\n        pre_hook,\n    )\n    msg_input = Msg(\"user\", task.input, role=\"user\")\n    res = await agent(\n        msg_input,\n        structured_model=ToyBenchAnswerFormat,\n    )\n    return SolutionOutput(\n        success=True,\n        output=res.metadata.get(\"answer_as_number\", None),\n        trajectory=[],\n    )\n\n\nasync def main() -> None:\n    evaluator = GeneralEvaluator(\n        name=\"Toy benchmark evaluation\",\n        benchmark=ToyBenchmark(),\n        # 重复多少次\n        n_repeat=1,\n        storage=FileEvaluatorStorage(\n            save_dir=\"./results\",\n        ),\n        # 使用多少个工作进程\n        n_workers=1,\n    )\n\n    # 运行评估\n    await evaluator.run(toy_solution_generation)\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_eval_openjudge.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nOpenJudge 评估器\n=======================\n\n[OpenJudge](https://github.com/agentscope-ai/OpenJudge) 是一个专为评估LLM/Agent应用质量而设计的评估框架。通过将 OpenJudge 集成到 AgentScope 中，您可以将 AgentScope 的原生评估能力从基础的执行检查扩展到深度的语义质量分析。\n\n本指南中我们将介绍如何使用 OpenJudge 的评估器(Grader)作为 AgentScope 的评估指标(Metric)来评估您的智能体应用。\n\n.. note::\n   在运行本教程之前，请安装必要的依赖：\n\n   .. code-block:: bash\n\n       pip install agentscope py-openjudge\n\n为什么选择 OpenJudge？\n----------------------\n\n虽然 AgentScope 提供了强大的评估框架用于定义评估逻辑，但实现复杂的语义级指标（如“幻觉检测”或“回复相关性”）通常需要大量的 Prompt 工程和流程构建工作。\n\n集成 OpenJudge 可以为 AgentScope 带来了三个维度的能力提升：\n\n1.  **提升评估深度**：从简单的成功/失败检查升级为多维度的质量评估（如准确性、安全性、语气等）。\n2.  **开箱即用的 Grader**：直接使用 50+ 个预置的、专家级验证过的 Grader，无需手动编写评估 Prompt，详情请参阅 [OpenJudge官方文档](https://agentscope-ai.github.io/OpenJudge/built_in_graders/overview/)。\n3.  **闭环迭代**：将 OpenJudge 无缝嵌入 AgentScope 的 ``Benchmark`` 中，同时获取量化的分数和定性的理由分析。\n\n如何使用 OpenJudge 进行评估\n---------------------------\n\n我们将构建一个简单的问答（QA）基准测试，演示如何通过集成 OpenJudge 的 Grader 来使用 AgentScope 的评估模块。\n\"\"\"\n\n# %%\nQA_BENCHMARK_DATASET = [\n    {\n        \"id\": \"qa_task_1\",\n        \"question\": \"What are the health benefits of regular exercise?\",\n        \"reference_output\": \"Regular exercise improves cardiovascular health, strengthens muscles and bones, \"\n        \"helps maintain a healthy weight, and can improve mental health by reducing anxiety and depression.\",\n        \"ground_truth\": \"Answers should cover physical and mental health benefits\",\n        \"difficulty\": \"medium\",\n        \"category\": \"health\",\n    },\n    {\n        \"id\": \"qa_task_2\",\n        \"question\": \"Describe the main causes of climate change.\",\n        \"reference_output\": \"Climate change is primarily caused by increased concentrations of greenhouse gases \"\n        \"in the atmosphere due to human activities like burning fossil fuels, deforestation, and industrial processes.\",\n        \"ground_truth\": \"Answers should mention greenhouse gases and human activities\",\n        \"difficulty\": \"hard\",\n        \"category\": \"environment\",\n    },\n    {\n        \"id\": \"qa_task_3\",\n        \"question\": \"What is the significance of the Turing Test in AI?\",\n        \"reference_output\": \"The Turing Test, proposed by Alan Turing, is a measure of a machine's ability to exhibit\"\n        \" intelligent behavior equivalent to, or indistinguishable from, that of a human.\",\n        \"ground_truth\": \"Should mention Alan Turing, purpose of the test, and its implications for AI\",\n        \"difficulty\": \"hard\",\n        \"category\": \"technology\",\n    },\n]\n\n\n# %% [markdown]\n# AgentScope Metric vs. OpenJudge Grader\n# ~~~~~~~~~~\n# 为了使 AgentScope 兼容 OpenJudge，我们需要一个适配器（Adapter）来完成两个框架间的转换。\n# 这个适配器继承自 AgentScope 的 ``MetricBase``，并充当通往 OpenJudge ``BaseGrader`` 的桥梁。\n#\n# * **AgentScope Metric**: 一个通用的评估单元，接收 ``SolutionOutput`` 并返回 ``MetricResult``。\n# * **OpenJudge Grader**: 一个特定的评估单元（例如 ``RelevanceGrader``），需要特定的语义输入（如 ``query``, ``response``, ``context``），返回``GraderResult``。\n#\n# 这个“适配器”允许您将 *任何* OpenJudge Grader 无缝插入到您的 AgentScope 基准测试中。\n\n# %%\nfrom openjudge.graders.base_grader import BaseGrader\nfrom openjudge.graders.schema import GraderScore, GraderError\nfrom openjudge.utils.mapping import parse_data_with_mapper\nfrom agentscope.evaluate import (\n    MetricBase,\n    MetricType,\n    MetricResult,\n    SolutionOutput,\n)\n\n\nclass OpenJudgeMetric(MetricBase):\n    def __init__(\n        self,\n        grader_cls: type[BaseGrader],\n        data: dict,\n        mapper: dict,\n        name: str | None = None,\n        description: str | None = None,\n        **grader_kwargs,\n    ):\n        \"\"\"Initializes the OpenJudgeMetric.\n\n        Args:\n            grader_cls (`type[BaseGrader]`):\n                The OpenJudge grader class to be wrapped.\n            data (`dict`):\n                The static data for the task.\n            mapper (`dict`):\n                The mapper to extract grader inputs from combined data.\n            name (`str | None`, optional):\n                The name of the metric. Defaults to the grader's name.\n            description (`str | None`, optional):\n                The description of the metric. Defaults to the grader's\n                description.\n            **grader_kwargs:\n                Additional keyword arguments for the grader initialization.\n        \"\"\"\n        self.grader = grader_cls(**grader_kwargs)\n\n        super().__init__(\n            name=name or self.grader.name,\n            metric_type=MetricType.NUMERICAL,\n            description=description or self.grader.description,\n        )\n\n        self.data = data\n        self.mapper = mapper\n\n    async def __call__(self, solution: SolutionOutput) -> MetricResult:\n        \"\"\"针对 Agent 的输出执行封装好的 OpenJudge Grader。\"\"\"\n        if not solution.success:\n            return MetricResult(\n                name=self.name,\n                result=0.0,\n                message=\"Solution failed\",\n            )\n\n        try:\n            # 1. 构建上下文\n            # 将静态的任务上下文 (data) 和动态的 Agent 输出 (solution) 组合\n            combined_data = {\n                \"data\": self.data,\n                \"solution\": {\n                    \"output\": solution.output,\n                    \"meta\": solution.meta,\n                    \"trajectory\": getattr(solution, \"trajectory\", []),\n                },\n            }\n\n            # 2. 数据映射\n            # 使用 mapper 从组合数据中提取Grader需要的 'query', 'response', 'context' 等参数\n            grader_inputs = parse_data_with_mapper(\n                combined_data,\n                self.mapper,\n            )\n\n            ## 3. 执行评估\n            result = await self.grader.aevaluate(**grader_inputs)\n\n            # 4. 格式化结果\n            if isinstance(result, GraderScore):\n                return MetricResult(\n                    name=self.name,\n                    result=result.score,\n                    # 保留 OpenJudge 提供的详细理由\n                    message=result.reason or \"\",\n                )\n            elif isinstance(result, GraderError):\n                return MetricResult(\n                    name=self.name,\n                    result=0.0,\n                    message=f\"Error: {result.error}\",\n                )\n            else:\n                return MetricResult(\n                    name=self.name,\n                    result=0.0,\n                    message=\"Unknown result type\",\n                )\n\n        except Exception as e:\n            return MetricResult(\n                name=self.name,\n                result=0.0,\n                message=f\"Exception: {str(e)}\",\n            )\n\n\n# %% [markdown]\n# 从 OpenJudge 到 AgentScope\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# OpenJudge 提供了丰富的内置 Grader 集合。在当前实例中，我们选择两个适合问答任务的常用 Grader：\n#\n# * **RelevanceGrader (相关性)**：评估 Agent 的回答是否直接回应了用户的查询（忽略事实准确性）。\n# * **CorrectnessGrader (正确性)**：根据提供的参考答案（Ground Truth）验证回答的事实准确性。\n#\n# .. tip::\n#    OpenJudge 提供了 50+ 种内置 Grader，涵盖 **幻觉检测**、**安全性**、**代码质量** 和 **JSON 格式化** 等多个维度。\n#    请查阅 `OpenJudge 官方文档 <https://agentscope-ai.github.io/OpenJudge/built_in_graders/overview/>`_ 获取完整列表。\n#\n# .. note::\n#    在运行以下示例之前，请确保您已设置 ``DASHSCOPE_API_KEY`` 环境变量。\n\n# %%\nimport os\nfrom typing import Generator\nfrom openjudge.graders.common.relevance import RelevanceGrader\nfrom openjudge.graders.common.correctness import CorrectnessGrader\nfrom agentscope.evaluate import (\n    Task,\n    BenchmarkBase,\n)\n\n\nclass QABenchmark(BenchmarkBase):\n    def __init__(self):\n        super().__init__(\n            name=\"QA Quality Benchmark\",\n            description=\"Benchmark to evaluate QA systems using OpenJudge grader classes\",\n        )\n        self.dataset = self._load_data()\n\n    def _load_data(self):\n        tasks = []\n        # 配置 LLM Grader 的模型参数\n        # 注意：如果不使用环境变量，请在此处设置 \"api_key\"\n        model_config = {\n            \"model\": \"qwen3-32b\",\n            \"api_key\": os.environ.get(\"DASHSCOPE_API_KEY\"),\n            \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n        }\n\n        for data in QA_BENCHMARK_DATASET:\n            # 定义映射关系：左侧是 OpenJudge 的键，右侧是 AgentScope 的数据路径\n            mapper = {\n                \"query\": \"data.input\",\n                \"response\": \"solution.output\",\n                \"context\": \"data.ground_truth\",\n                \"reference_response\": \"data.reference_output\",\n            }\n\n            # 通过 Adapter 实例化 Metrics\n            metrics = [\n                OpenJudgeMetric(\n                    grader_cls=RelevanceGrader,\n                    data=data,\n                    mapper=mapper,\n                    name=\"Relevance\",\n                    model=model_config,\n                ),\n                OpenJudgeMetric(\n                    grader_cls=CorrectnessGrader,\n                    data=data,\n                    mapper=mapper,\n                    name=\"Correctness\",\n                    model=model_config,\n                ),\n            ]\n\n            # 创建 Task\n            task = Task(\n                id=data[\"id\"],\n                input=data[\"question\"],\n                ground_truth=data[\"ground_truth\"],\n                metrics=metrics,\n            )\n\n            tasks.append(task)\n\n        return tasks\n\n    def __iter__(self) -> Generator[Task, None, None]:\n        yield from self.dataset\n\n    def __getitem__(self, index: int) -> Task:\n        return self.dataset[index]\n\n    def __len__(self) -> int:\n        return len(self.dataset)\n\n\n# %% [markdown]\n# 运行评估\n# ~~~~~~~~~~\n# 最后，使用 AgentScope 的 ``GeneralEvaluator`` 对一个简单的QA Agent进行评估测试。\n# 我们将收集到来自 OpenJudge Grader 的 **量化分数 (Score)** 和 **定性理由 (Reasoning)**。\n\n# %%\n\nimport asyncio\nfrom typing import Callable\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.evaluate import GeneralEvaluator\nfrom agentscope.evaluate import FileEvaluatorStorage\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.message import Msg\nfrom agentscope.model import OpenAIChatModel\n\n\nasync def qa_agent(task: Task, pre_hook: Callable) -> SolutionOutput:\n    model = OpenAIChatModel(\n        model_name=\"qwen3-32b\",\n        api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n    )\n    agent = ReActAgent(\n        name=\"QAAgent\",\n        sys_prompt=\"You are an expert at answering questions. Provide clear, accurate, and comprehensive answers.\",\n        model=model,\n        formatter=DashScopeChatFormatter(),\n    )\n\n    # Generate response\n    msg_input = Msg(name=\"User\", content=task.input, role=\"user\")\n    response = await agent(msg_input)\n    response_text = response.content\n\n    return SolutionOutput(\n        success=True,\n        output=response_text,\n        trajectory=[\n            task.input,\n            response_text,\n        ],  # Store the interaction trajectory\n    )\n\n\nasync def main() -> None:\n    evaluator = GeneralEvaluator(\n        name=\"OpenJudge Integration Demo\",\n        benchmark=QABenchmark(),\n        # Repeat how many times\n        n_repeat=1,\n        storage=FileEvaluatorStorage(\n            save_dir=\"./results\",\n        ),\n        # How many workers to use\n        n_workers=1,\n    )\n\n    await evaluator.run(qa_agent)\n\n\n# %% [markdown]\n#\n# ~~~~~~~~~~\n# 最后，使用 AgentScope 的 ``GeneralEvaluator`` 对一个简单的QA Agent进行评估测试。\n# 我们将收集到来自 OpenJudge Grader 的 **量化分数 (Score)** 和 **定性理由 (Reasoning)**。\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_hook.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _hook:\n\n智能体钩子函数\n===========================\n\n钩子（Hook）是 AgentScope 中的扩展点，允许开发者在特定位置自定义智能体行为，提供了一种灵活的方式来修改或扩展智能体的功能，而无需更改其核心实现。\n\n在 AgentScope 中，钩子围绕智能体的核心函数实现：\n\n\n.. list-table:: AgentScope 中支持的钩子类型\n    :header-rows: 1\n\n    * - 智能体类\n      - 核心函数\n      - 钩子类型\n      - 描述\n    * - | ``AgentBase`` 及其子类\n      - ``reply``\n      - | ``pre_reply``\n        | ``post_reply``\n      - 智能体回复消息前/后的钩子\n    * -\n      - ``print``\n      - | ``pre_print``\n        | ``post_print``\n      - 向目标输出（如终端、Web 界面）打印消息前/后的钩子\n    * -\n      - ``observe``\n      - | ``pre_observe``\n        | ``post_observe``\n      - 从环境或其它智能体观察消息前/后的钩子\n    * - | ``ReActAgentBase`` 及其子类\n      - | ``reply``\n        | ``print``\n        | ``observe``\n      - | ``pre_reply``\n        | ``post_reply``\n        | ``pre_print``\n        | ``post_print``\n        | ``pre_observe``\n        | ``post_observe``\n      -\n    * -\n      - ``_reasoning``\n      - | ``pre_reasoning``\n        | ``post_reasoning``\n      - 智能体推理过程前/后的钩子\n    * -\n      - ``_acting``\n      - | ``pre_acting``\n        | ``post_acting``\n      - 智能体行动过程前/后的钩子\n\n.. tip:: 由于 AgentScope 中的钩子函数是通过 meta class 实现的，因此支持继承。\n\n为了简化使用，AgentScope 为所有钩子提供了统一的签名。\n\n\"\"\"\nimport asyncio\nfrom typing import Any, Type\n\nfrom agentscope.agent import ReActAgentBase, AgentBase\nfrom agentscope.message import Msg\n\n\n# %%\n# 钩子签名\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# AgentScope 为所有前置（pre_）和后置（post_）钩子提供统一的钩子签名，如下所示：\n#\n# **前置钩子签名**\n#\n# .. list-table:: 所有前置钩子的签名\n#   :header-rows: 1\n#\n#   * -\n#     - 名称\n#     - 描述\n#   * - 参数\n#     - ``self: AgentBase | ReActAgentBase``\n#     - 智能体实例\n#   * -\n#     - ``kwargs: dict[str, Any]``\n#     - | 目标函数的输入参数，或来自最近\n#       | 一个非 None 返回值的钩子修\n#       | 改后的参数\n#   * - 返回值\n#     - ``dict[str, Any] | None``\n#     - 修改后的参数或 None\n#\n# .. note:: 核心函数的所有位置参数（*args）和关键字参数（**kwargs）被统一成单个 ``kwargs`` 字典传递给钩子函数\n#\n# 前置钩子模板定义如下：\n#\n\n\ndef pre_hook_template(\n    self: AgentBase | ReActAgentBase,\n    kwargs: dict[str, Any],\n) -> dict[str, Any] | None:  # 修改后的输入\n    \"\"\"前置钩子模板。\"\"\"\n    pass\n\n\n# %%\n# **后置钩子签名**\n#\n# 对于后置钩子，在签名中增加了一个额外的 ``output`` 参数，表示目标函数的输出。\n# 如果核心函数没有输出，``output`` 参数将为 ``None``。\n#\n# .. list-table:: 所有后置钩子的签名\n#   :header-rows: 1\n#\n#   * -\n#     - 名称\n#     - 描述\n#   * - 参数\n#     - ``self: AgentBase | ReActAgentBase``\n#     - 智能体实例\n#   * -\n#     - ``kwargs: dict[str, Any]``\n#     - | 包含目标函数所有参数的字典\n#   * -\n#     - ``output: Any``\n#     - | 目标函数的输出或来自前序钩子\n#       | 最近一个非 None 返回值\n#   * - 返回值\n#     - ``dict[str, Any] | None``\n#     - 修改后的输出或 None\n#\n\n\ndef post_hook_template(\n    self: AgentBase | ReActAgentBase,\n    kwargs: dict[str, Any],\n    output: Any,  # 目标函数的输出\n) -> Any:  # 修改后的输出\n    \"\"\"后置钩子模板。\"\"\"\n    pass\n\n\n# %%\n# 钩子管理\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# AgentScope 提供实例级（instance）和类级（class）钩子，其区别在于钩子函数的作用范围。\n# 它们按以下顺序执行：\n#\n# .. image:: ../../_static/images/sequential_hook.png\n#   :width: 90%\n#   :align: center\n#   :alt: AgentScope 中的钩子\n#   :class: bordered-image\n#\n# AgentScope 提供内置方法来管理实例级和类级的钩子，如下所示：\n#\n# .. list-table:: AgentScope 中的钩子管理方法\n#   :header-rows: 1\n#\n#   * - 级别\n#     - 方法\n#     - 描述\n#   * - 实例级\n#     - ``register_instance_hook``\n#     - | 为当前对象注册具有给定钩子类型\n#       | 和名称的钩子。\n#   * -\n#     - ``remove_instance_hook``\n#     - | 移除当前对象具有给定钩子类型\n#       | 和名称的钩子。\n#   * -\n#     - ``clear_instance_hooks``\n#     - | 清除当前对象具有给定钩子类型\n#       | 的所有钩子。\n#   * - 类级\n#     - ``register_class_hook``\n#     - | 为该类的所有对象注册具有给定\n#       | 钩子类型和名称的钩子。\n#   * -\n#     - ``remove_class_hook``\n#     - | 移除该类所有对象具有给定\n#       | 钩子类型和名称的钩子。\n#   * -\n#     - ``clear_class_hooks``\n#     - | 清除该类所有对象具有给定\n#       | 钩子类型的所有钩子。\n#\n# 使用钩子时，开发者需要注意以下规则：\n#\n# .. important:: **执行顺序**\n#\n#  - 钩子按注册顺序执行\n#  - 多个钩子可以链式连接\n#  **返回值处理**\n#\n#  - 对于前置钩子：非 None 返回值会传递给下一个钩子或核心函数\n#   - 当钩子返回 None 时，下一个钩子将使用前序钩子中最近的非 None 返回值\n#   - 如果所有前序钩子都返回 None，那该钩子接收原始参数的副本作为输入\n#   - 最后一个非 None 返回值（或如果所有钩子都返回 None 则使用原始参数）传递给核心函数\n#  - 对于后置钩子：工作方式与前置钩子相似。\n#  **重要提示**：不要在钩子内调用核心函数（reply/speak/observe/_reasoning/_acting）以避免循环调用！\n#\n# 以下面的智能体为例，我们可以看到如何注册、移除和清除钩子：\n#\n\n\n# 创建一个简单的测试智能体类\nclass TestAgent(AgentBase):\n    \"\"\"用于演示钩子的测试智能体。\"\"\"\n\n    async def reply(self, msg: Msg) -> Msg:\n        \"\"\"回复消息。\"\"\"\n        return msg\n\n\n# %%\n# 我们创建一个实例级钩子和一个类级钩子来在回复前修改消息内容。\n#\n\n\n# 创建两个前置回复钩子\ndef instance_pre_reply_hook(\n    self: AgentBase,\n    kwargs: dict[str, Any],\n) -> dict[str, Any]:\n    \"\"\"修改消息内容的前置回复钩子。\"\"\"\n    msg = kwargs[\"msg\"]\n    msg.content += \"[instance-pre-reply]\"\n    # 返回修改后的 kwargs\n    return {\n        **kwargs,\n        \"msg\": msg,\n    }\n\n\ndef cls_pre_reply_hook(\n    self: AgentBase,\n    kwargs: dict[str, Any],\n) -> dict[str, Any]:\n    \"\"\"修改消息内容的前置回复钩子。\"\"\"\n    msg = kwargs[\"msg\"]\n    msg.content += \"[cls-pre-reply]\"\n    # 返回修改后的 kwargs\n    return {\n        **kwargs,\n        \"msg\": msg,\n    }\n\n\n# 注册类钩子\nTestAgent.register_class_hook(\n    hook_type=\"pre_reply\",\n    hook_name=\"test_pre_reply\",\n    hook=cls_pre_reply_hook,\n)\n\n# 注册实例钩子\nagent = TestAgent()\nagent.register_instance_hook(\n    hook_type=\"pre_reply\",\n    hook_name=\"test_pre_reply\",\n    hook=instance_pre_reply_hook,\n)\n\n\nasync def example_test_hook() -> None:\n    \"\"\"测试钩子的示例函数。\"\"\"\n    msg = Msg(\n        name=\"user\",\n        content=\"Hello, world!\",\n        role=\"user\",\n    )\n    res = await agent(msg)\n    print(\"响应内容：\", res.content)\n    TestAgent.clear_class_hooks()\n\n\nasyncio.run(example_test_hook())\n\n# %%\n# 我们可以看到 \"[instance-pre-reply]\" 和 \"[cls-pre-reply]\" 被添加到了消息内容中。\n#\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_long_term_memory.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _long-term-memory:\n\n长期记忆\n========================\n\nAgentScope 为长期记忆提供了一个基类 ``LongTermMemoryBase`` 和一个基于 `mem0 <https://github.com/mem0ai/mem0>`_ 的具体实现 ``Mem0LongTermMemory``。\n结合 :ref:`agent` 章节中 ``ReActAgent`` 类的设计，我们提供了两种长期记忆模式：\n\n- ``agent_control``：智能体通过工具调用自主管理长期记忆。\n- ``static_control``：开发者通过编程显式控制长期记忆操作。\n\n当然，开发者也可以使用 ``both`` 参数，将同时激活上述两种记忆管理模式。\n\n.. hint:: 不同的记忆模式适用于不同的使用场景，开发者可以根据需要选择合适的模式。\n\n使用 mem0 长期记忆\n~~~~~~~~~~~~~~~~~~~~~~~~\n\n.. note:: 在 GitHub 仓库的 ``examples/long_term_memory/mem0`` 目录下我们提供了 mem0 长期记忆的使用示例。\n\n\"\"\"\n\nimport os\nimport asyncio\n\nfrom agentscope.message import Msg\nfrom agentscope.memory import InMemoryMemory, Mem0LongTermMemory\nfrom agentscope.agent import ReActAgent\nfrom agentscope.embedding import DashScopeTextEmbedding\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import Toolkit\n\n\n# 创建 mem0 长期记忆实例\nlong_term_memory = Mem0LongTermMemory(\n    agent_name=\"Friday\",\n    user_name=\"user_123\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max-latest\",\n        api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n        stream=False,\n    ),\n    embedding_model=DashScopeTextEmbedding(\n        model_name=\"text-embedding-v2\",\n        api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n    ),\n    on_disk=False,\n)\n\n# %%\n# ``Mem0LongTermMemory`` 类提供了两个操作长期记忆的方法，``record`` 和 ``retrieve``。\n# 它们接收消息对象的列表作为输入，分别记录和检索长期记忆中的信息。\n#\n# 例如下面的例子中，我们先存入用户的一条偏好，然后在长期记忆中检索相关信息。\n#\n\n\n# 基本使用示例\nasync def basic_usage():\n    \"\"\"基本使用示例\"\"\"\n    # 记录记忆\n    await long_term_memory.record([Msg(\"user\", \"我喜欢住民宿\", \"user\")])\n\n    # 检索记忆\n    results = await long_term_memory.retrieve(\n        [Msg(\"user\", \"我的住宿偏好\", \"user\")],\n    )\n    print(f\"检索结果: {results}\")\n\n\nasyncio.run(basic_usage())\n\n\n# %%\n# 与 ReAct 智能体集成\n# ----------------------------------------\n# AgentScope 中的 ``ReActAgent`` 在构造函数中包含 ``long_term_memory`` 和 ``long_term_memory_mode`` 两个参数，\n# 其中 ``long_term_memory`` 用于指定长期记忆实例，``long_term_memory_mode`` 的取值为 ``\"agent_control\"``, ``\"static_control\"`` 或 ``\"both\"``。\n#\n# 当 ``long_term_memory_mode`` 设置为 ``\"agent_control\"`` 或 ``both`` 时，在 ``ReActAgent`` 的构造函数中将\n# 注册两个工具函数：``record_to_memory`` 和 ``retrieve_from_memory``。\n# 从而使智能体能够自主的管理长期记忆。\n#\n# .. note:: 为了达到最好的效果，``\"agent_control\"`` 模式可能还需要在系统提示（system prompt）中添加相应的说明。\n#\n\n# 创建带有长期记忆的 ReAct 智能体\nagent = ReActAgent(\n    name=\"Friday\",\n    sys_prompt=\"你是一个具有长期记忆功能的助手。\",\n    model=DashScopeChatModel(\n        api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n        model_name=\"qwen-max-latest\",\n    ),\n    formatter=DashScopeChatFormatter(),\n    toolkit=Toolkit(),\n    memory=InMemoryMemory(),\n    long_term_memory=long_term_memory,\n    long_term_memory_mode=\"static_control\",  # 使用 static_control 模式\n)\n\n\nasync def record_preferences():\n    \"\"\"ReAct agent integration example\"\"\"\n    # 对话示例\n    msg = Msg(\"user\", \"我去杭州旅行时，喜欢住民宿\", \"user\")\n    await agent(msg)\n\n\nasyncio.run(record_preferences())\n\n# %%\n# 然后我们清空智能体的短期记忆，以避免造成干扰，并测试智能体是否会记住之前的对话。\n#\n\n\nasync def retrieve_preferences():\n    \"\"\"Retrieve user preferences from long-term memory\"\"\"\n    # 我们清空智能体的短期记忆，以避免造成干扰\n    await agent.memory.clear()\n\n    # 测试智能体是否会记住之前的对话\n    msg2 = Msg(\"user\", \"我有什么偏好？简要的回答我\", \"user\")\n    await agent(msg2)\n\n\nasyncio.run(retrieve_preferences())\n\n# %%\n# 使用 ReMe 个人长期记忆\n# ~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# .. note:: 在 GitHub 仓库的 ``examples/long_term_memory/reme`` 目录下我们提供了 ReMe 长期记忆的使用示例。\n#\n# .. code-block:: python\n#     :caption: 安装 ReMe 依赖\n#\n#     from agentscope.memory import ReMePersonalLongTermMemory\n#\n#     # 创建 ReMe 个人长期记忆实例\n#     reme_long_term_memory = ReMePersonalLongTermMemory(\n#         agent_name=\"Friday\",\n#         user_name=\"user_123\",\n#         model=DashScopeChatModel(\n#             model_name=\"qwen3-max\",\n#             api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n#             stream=False,\n#         ),\n#         embedding_model=DashScopeTextEmbedding(\n#             model_name=\"text-embedding-v4\",\n#             api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n#             dimensions=1024,\n#         ),\n#     )\n#\n#\n# ``ReMePersonalLongTermMemory`` 类提供了四个操作长期记忆的方法。\n# 它们分别是用于工具调用的 ``record_to_memory`` 和 ``retrieve_from_memory``，\n# 以及用于直接调用的 ``record`` 和 ``retrieve``。\n#\n# 例如下面的例子中，我们使用 ``record_to_memory`` 记录用户偏好。\n#\n# .. code-block:: python\n#     :caption: 创建 ReMe 个人长期记忆实例\n#\n#     async def test_record_to_memory():\n#         \"\"\"测试 record_to_memory 工具函数接口\"\"\"\n#         async with reme_long_term_memory:\n#             result = await reme_long_term_memory.record_to_memory(\n#                 thinking=\"用户正在分享他们的旅行偏好和习惯\",\n#                 content=[\n#                     \"我去杭州旅行时喜欢住民宿\",\n#                     \"我喜欢早上去西湖游玩\",\n#                     \"我喜欢喝龙井茶\",\n#                 ],\n#             )\n#             # 提取结果文本\n#             result_text = \" \".join(\n#                 block.get(\"text\", \"\")\n#                 for block in result.content\n#                 if block.get(\"type\") == \"text\"\n#             )\n#             print(f\"记录结果: {result_text}\")\n#\n#\n# 然后我们使用 ``retrieve_from_memory`` 检索相关记忆。\n#\n# .. code-block:: python\n#     :caption: 使用 retrieve_from_memory 检索记忆\n#\n#     async def test_retrieve_from_memory():\n#         \"\"\"测试 retrieve_from_memory 工具函数接口\"\"\"\n#         async with reme_long_term_memory:\n#             # 先记录一些内容\n#             await reme_long_term_memory.record_to_memory(\n#                 thinking=\"用户正在分享旅行偏好\",\n#                 content=[\"我去杭州旅行时喜欢住民宿\"],\n#             )\n#\n#             # 然后检索\n#             result = await reme_long_term_memory.retrieve_from_memory(\n#                 keywords=[\"杭州旅行\", \"茶偏好\"],\n#             )\n#             retrieved_text = \" \".join(\n#                 block.get(\"text\", \"\")\n#                 for block in result.content\n#                 if block.get(\"type\") == \"text\"\n#             )\n#             print(f\"检索到的记忆: {retrieved_text}\")\n#\n#\n# 除了工具函数接口，我们也可以使用 ``record`` 方法直接记录消息对话。\n#\n# .. code-block:: python\n#     :caption: 使用 record 直接记录消息\n#\n#     async def test_record_direct():\n#         \"\"\"测试 record 直接记录方法\"\"\"\n#         async with reme_long_term_memory:\n#             await reme_long_term_memory.record(\n#                 msgs=[\n#                     Msg(\n#                         role=\"user\",\n#                         content=\"我是一名软件工程师，喜欢远程工作\",\n#                         name=\"user\",\n#                     ),\n#                     Msg(\n#                         role=\"assistant\",\n#                         content=\"明白了！您是一名重视远程工作灵活性的软件工程师。\",\n#                         name=\"assistant\",\n#                     ),\n#                     Msg(\n#                         role=\"user\",\n#                         content=\"我通常早上9点开始工作，会先喝一杯咖啡\",\n#                         name=\"user\",\n#                     ),\n#                 ],\n#             )\n#             print(\"成功记录了对话消息\")\n#\n#\n# 类似地，我们使用 ``retrieve`` 方法检索相关记忆。\n#\n# .. code-block:: python\n#     :caption: 使用 retrieve 直接检索消息\n#\n#     async def test_retrieve_direct():\n#         \"\"\"测试 retrieve 直接检索方法\"\"\"\n#         async with reme_long_term_memory:\n#             # 先记录一些内容\n#             await reme_long_term_memory.record(\n#                 msgs=[\n#                     Msg(\n#                         role=\"user\",\n#                         content=\"我是一名软件工程师，喜欢远程工作\",\n#                         name=\"user\",\n#                     ),\n#                 ],\n#             )\n#\n#             # 然后检索\n#             memories = await reme_long_term_memory.retrieve(\n#                 msg=Msg(\n#                     role=\"user\",\n#                     content=\"你知道我的工作偏好吗？\",\n#                     name=\"user\",\n#                 ),\n#             )\n#             print(f\"检索到的记忆: {memories if memories else '未找到相关记忆'}\")\n#\n#\n# 与 ReAct 智能体集成\n# ----------------------------------------\n# AgentScope 中的 ``ReActAgent`` 在构造函数中包含 ``long_term_memory`` 和 ``long_term_memory_mode`` 两个参数。\n#\n# 当 ``long_term_memory_mode`` 设置为 ``\"agent_control\"`` 或 ``both`` 时，在 ``ReActAgent`` 的构造函数中将\n# 注册 ``record_to_memory`` 和 ``retrieve_from_memory`` 工具函数，使智能体能够自主的管理长期记忆。\n#\n# .. note:: 为了达到最好的效果，``\"agent_control\"`` 模式可能还需要在系统提示（system prompt）中添加相应的说明。\n#\n# .. code-block:: python\n#     :caption: 创建带有长期记忆的 ReAct 智能体\n#\n#     # 创建带有长期记忆的 ReAct 智能体（agent_control 模式）\n#     async def test_react_agent_with_reme():\n#         \"\"\"测试 ReActAgent 与 ReMe 个人记忆的集成\"\"\"\n#         async with reme_long_term_memory:\n#             agent_with_reme = ReActAgent(\n#                 name=\"Friday\",\n#                 sys_prompt=(\n#                     \"你是一个名为 Friday 的助手，具有长期记忆能力。\"\n#                     \"\\n\\n## 记忆管理指南：\\n\"\n#                     \"1. **记录记忆**：当用户分享个人信息、偏好、习惯或关于自己的事实时，\"\n#                     \"始终使用 `record_to_memory` 记录这些信息以供将来参考。\\n\"\n#                     \"\\n2. **检索记忆**：在回答关于用户偏好、过去信息或个人详细信息的问题之前，\"\n#                     \"你必须首先调用 `retrieve_from_memory` 来检查是否有任何相关的存储信息。\"\n#                     \"不要仅依赖当前对话上下文。\\n\"\n#                     \"\\n3. **何时检索**：在以下情况下调用 `retrieve_from_memory`：\\n\"\n#                     \"   - 用户问类似'我喜欢什么？'、'我的偏好是什么？'、\"\n#                     \"'你对我了解多少？'的问题\\n\"\n#                     \"   - 用户询问他们过去的行为、习惯或偏好\\n\"\n#                     \"   - 用户提到他们之前提到的信息\\n\"\n#                     \"   - 你需要关于用户的上下文来提供个性化的响应\\n\"\n#                     \"\\n在声称不了解用户的某些信息之前，始终先检查你的记忆。\"\n#                 ),\n#                 model=DashScopeChatModel(\n#                     model_name=\"qwen3-max\",\n#                     api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n#                     stream=False,\n#                 ),\n#                 formatter=DashScopeChatFormatter(),\n#                 toolkit=Toolkit(),\n#                 memory=InMemoryMemory(),\n#                 long_term_memory=reme_long_term_memory,\n#                 long_term_memory_mode=\"agent_control\",  # 使用 agent_control 模式\n#             )\n#\n#             # 用户分享偏好\n#             msg = Msg(\n#                 role=\"user\",\n#                 content=\"我去杭州旅行时，喜欢住民宿\",\n#                 name=\"user\",\n#             )\n#             response = await agent_with_reme(msg)\n#             print(f\"智能体响应: {response.get_text_content()}\")\n#\n#             # 清空短期记忆以测试长期记忆\n#             await agent_with_reme.memory.clear()\n#\n#             # 查询偏好\n#             msg2 = Msg(role=\"user\", content=\"我有什么偏好？\", name=\"user\")\n#             response2 = await agent_with_reme(msg2)\n#             print(f\"智能体响应: {response2.get_text_content()}\")\n#\n#\n# 然后我们清空智能体的短期记忆，以避免造成干扰，并测试智能体是否会记住之前的对话。\n#\n# .. code-block:: python\n#     :caption: 测试 ReAct 智能体是否记住偏好\n#\n#     async def retrieve_reme_preferences():\n#         \"\"\"从长期记忆中检索用户偏好\"\"\"\n#         async with reme_long_term_memory:\n#             # 创建智能体（这里可以复用之前创建的智能体，为了示例完整性重新创建）\n#             agent_with_reme = ReActAgent(\n#                 name=\"Friday\",\n#                 sys_prompt=\"你是一个具有长期记忆功能的助手。\",\n#                 model=DashScopeChatModel(\n#                     api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n#                     model_name=\"qwen3-max\",\n#                     stream=False,\n#                 ),\n#                 formatter=DashScopeChatFormatter(),\n#                 toolkit=Toolkit(),\n#                 memory=InMemoryMemory(),\n#                 long_term_memory=reme_long_term_memory,\n#                 long_term_memory_mode=\"agent_control\",\n#             )\n#\n#             # 我们清空智能体的短期记忆，以避免造成干扰\n#             await agent_with_reme.memory.clear()\n#\n#             # 测试智能体是否会记住之前的对话\n#             msg2 = Msg(\"user\", \"我有什么偏好？简要的回答我\", \"user\")\n#             await agent_with_reme(msg2)\n#\n#\n# 自定义长期记忆\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope 提供了 ``LongTermMemoryBase`` 基类，它定义了长期记忆的基本接口。\n#\n# 开发者可以继承 ``LongTermMemoryBase`` 并实现以下的抽象方法来定义自己的长期记忆类：\n#\n# .. list-table:: AgentScope 中的长期记忆类\n#     :header-rows: 1\n#\n#     * - 类\n#       - 抽象方法\n#       - 描述\n#     * - ``LongTermMemoryBase``\n#       - | ``record``\n#         | ``retrieve``\n#         | ``record_to_memory``\n#         | ``retrieve_from_memory``\n#       - - 如果想支持 \"static_control\" 模式，必须实现 ``record`` 和 ``retrieve`` 方法。\n#         - 想要支持 \"agent_control\" 模式，必须实现 ``record_to_memory`` 和 ``retrieve_from_memory`` 方法。\n#     * - ``Mem0LongTermMemory``\n#       - | ``record``\n#         | ``retrieve``\n#         | ``record_to_memory``\n#         | ``retrieve_from_memory``\n#       - 基于 mem0 库的长期记忆实现，支持向量存储和检索。\n#     * - ``ReMePersonalLongTermMemory``\n#       - | ``record``\n#         | ``retrieve``\n#         | ``record_to_memory``\n#         | ``retrieve_from_memory``\n#       - 基于 ReMe 框架的个人记忆实现，提供强大的记忆管理和检索功能。\n#\n#\n# 进一步阅读\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# - :ref:`memory` - 基础记忆系统\n# - :ref:`agent` - ReAct 智能体\n# - :ref:`tool` - 工具系统\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_mcp.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _mcp:\n\nMCP\n=========================\n\n本章将介绍 AgentScope 对 MCP（Model Context Protocol）的以下支持：\n\n- 支持 **HTTP** （StreamableHTTP 和 SSE）和 **StdIO** 类型的 MCP 服务器\n- 提供 **有状态** 和 **无状态** 两种 MCP 客户端\n- 提供 **MCP 级别** 和 **函数级别** 的 MCP 工具管理\n\n这里的有状态/无状态是指客户端是否会维持与 MCP 服务器的会话（session）。\n无状态客户端只会在调用工具发生时建立会话，并在工具调用结束后立即销毁会话，是一种轻量化的使用方式。\n\n下表总结了支持的 MCP 客户端类型和协议：\n\n.. list-table:: 支持的 MCP 客户端类型和协议\n    :header-rows: 1\n\n    * - 客户端类型\n      - HTTP（StreamableHTTP 和 SSE）\n      - StdIO\n    * - 有状态客户端\n      - ``HttpStatefulClient``\n      - ``StdIOStatefulClient``\n    * - 无状态客户端\n      - ``HttpStatelessClient``\n      -\n\n\"\"\"\nimport asyncio\nimport json\nimport os\n\nfrom agentscope.mcp import HttpStatefulClient, HttpStatelessClient\nfrom agentscope.tool import Toolkit\n\n# %%\n# MCP 客户端\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# 在 AgentScope 中，MCP 客户端负责\n#\n# - 连接到 MCP 服务器，\n# - 从服务器获取工具函数，以及\n# - 调用 MCP 服务器中的工具函数。\n#\n# AgentScope 中有两种类型的 MCP 客户端：**有状态** 和 **无状态**。\n# 它们仅在 **如何管理与 MCP 服务器的会话** 方面有所不同。\n#\n# - 有状态客户端：有状态 MCP 客户端在其生命周期内 **维持与 MCP 服务器的持久会话**。开发者应显式调用 ``connect()`` 和 ``close()`` 方法来管理会话的生命周期。\n# - 无状态客户端：无状态 MCP 客户端在调用工具函数时创建新会话，在工具函数调用完成后立即销毁会话，更加轻量化。\n#\n# .. note:: - StdIO MCP 服务器只有有状态客户端，当调用 ``connect()`` 时，它将在本地启动 MCP 服务器然后连接到它。\n#  - 对于有状态客户端，开发者必须确保在调用工具函数时客户端已连接。\n#  - 当有多个 `HttpStatefulClient` 或 `StdIOStatefulClient` 建立连接时，应按照后进先出 (LIFO) 的顺序关闭它们以避免引发错误。\n#\n# 以高德地图 MCP 服务器为例，有状态和无状态客户端的创建非常相似：\n#\n\nstateful_client = HttpStatefulClient(\n    # 用于标识 MCP 的名称\n    name=\"mcp_services_stateful\",\n    transport=\"streamable_http\",\n    url=f\"https://mcp.amap.com/mcp?key={os.environ['GAODE_API_KEY']}\",\n)\n\nstateless_client = HttpStatelessClient(\n    # 用于标识 MCP 的名称\n    name=\"mcp_services_stateless\",\n    transport=\"streamable_http\",\n    url=f\"https://mcp.amap.com/mcp?key={os.environ['GAODE_API_KEY']}\",\n)\n\n# %%\n# 有状态和无状态客户端都提供以下方法：\n#\n# .. list-table:: MCP 客户端方法\n#    :header-rows: 1\n#\n#    * - 方法\n#      - 描述\n#    * - ``list_tools``\n#      - 列出 MCP 服务器中所有可用的工具。\n#    * - ``get_callable_function``\n#      - 通过名称从 MCP 服务器获取可调用的函数对象。\n#\n# MCP 作为工具\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope 提供了对 MCP 工具的细粒度管理，包括 MCP 级别和函数级别的管理。\n#\n# MCP 级别管理\n# --------------------------------\n# 您可以将 MCP 服务器的所有工具一次性注册到 ``Toolkit`` 中，如下所示。\n#\n# .. tip:: 可选地，开发者可以通过指定组名来管理工具。有关分组工具管理，请参考 :ref:`tool` 部分。\n#\n\ntoolkit = Toolkit()\n\n\nasync def example_register_stateless_mcp() -> None:\n    \"\"\"注册无状态客户端 MCP 工具的示例。\"\"\"\n    # 从 MCP 服务器注册所有工具\n    await toolkit.register_mcp_client(\n        stateless_client,\n        # group_name=\"map_services\",  # 可选的组名\n    )\n\n    print(\"注册的 MCP 工具总数：\", len(toolkit.get_json_schemas()))\n\n    maps_geo = next(\n        tool\n        for tool in toolkit.get_json_schemas()\n        if tool[\"function\"][\"name\"] == \"maps_geo\"\n    )\n    print(\"\\n示例 ``maps_geo`` 函数：\")\n    print(\n        json.dumps(\n            maps_geo,\n            indent=4,\n            ensure_ascii=False,\n        ),\n    )\n\n\nasyncio.run(example_register_stateless_mcp())\n\n# %%\n# 要移除已注册的工具，可以使用 ``remove_tool_function`` 函数，或使用 ``remove_mcp_clients`` 移除特定 MCP 的所有工具。\n#\n\n\nasync def example_remove_mcp_tools() -> None:\n    \"\"\"移除 MCP 工具的示例。\"\"\"\n    print(\"移除前的工具总数：\", len(toolkit.get_json_schemas()))\n\n    # 通过名称移除特定的工具函数\n    toolkit.remove_tool_function(\"maps_geo\")\n    print(\"工具数量：\", len(toolkit.get_json_schemas()))\n\n    # 通过名称移除 MCP 客户端的所有工具\n    await toolkit.remove_mcp_clients(client_names=[\"mcp_services_stateless\"])\n    print(\"工具数量：\", len(toolkit.get_json_schemas()))\n\n\nasyncio.run(example_remove_mcp_tools())\n\n# %%\n# 函数级别管理\n# --------------------------------\n# 注意到开发者有对 MCP 工具进行更细粒度控制的需求，例如对工具结果进行后处理，或使用它们创建更复杂的工具函数。\n#\n# 因此，AgentScope 支持通过工具名从 MCP 客户端获取可调用的函数对象，这样开发者可以\n#\n# - 直接调用它，\n# - 将其包装到自己的函数中，或以任何其它方式进行使用。\n#\n# 此外，开发者可以指定是否将工具函数执行结果包装成 ``ToolResponse`` 对象，以便与 ``Toolkit`` 无缝使用。\n# 如果设置 ``wrap_tool_result=False``，将返回原始结果类型 ``mcp.types.CallToolResult``。\n#\n# 以 ``maps_geo`` 函数为例，可以将其获取为可调用的函数对象，如下所示：\n#\n\n\nasync def example_function_level_usage() -> None:\n    \"\"\"使用函数级别 MCP 工具的示例。\"\"\"\n    func_obj = await stateless_client.get_callable_function(\n        func_name=\"maps_geo\",\n        # 是否将工具结果包装到 AgentScope 的 ToolResponse 中\n        wrap_tool_result=True,\n    )\n\n    # 您可以获取其名称、描述和 JSON schema\n    print(\"函数名称：\", func_obj.name)\n    print(\"函数描述：\", func_obj.description)\n    print(\n        \"函数 JSON schema：\",\n        json.dumps(func_obj.json_schema, indent=4, ensure_ascii=False),\n    )\n\n    # 直接调用函数对象\n    res = await func_obj(\n        address=\"天安门广场\",\n        city=\"北京\",\n    )\n    print(\"\\n函数调用结果：\")\n    print(res)\n\n\nasyncio.run(example_function_level_usage())\n\n# %%\n# 进一步阅读\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# 有关更多详细信息，请参见：\n#\n# - :ref:`tool`\n# - :ref:`agent`\n#\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_memory.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _memory:\n\n记忆\n========================\n\nAgentScope 中的记忆模块负责：\n\n- 存储消息对象（``Msg``）\n- 利用标记（mark）管理消息\n\n**标记** 是与记忆中每条消息关联的字符串标签，可用于根据消息的上下文或目的对消息进行分类、过滤和检索。\n可用于实现进阶的记忆管理功能，例如在 `ReActAgent` 类中，使用``\"hint\"``标签标记一次性的提示消息，\n以便在使用完成后将其从记忆中删除。\n\n.. note:: AgentScope 中的记忆模块仅提供消息存储和管理的原子功能，记忆压缩等算法逻辑在 `智能体 <agent>`_ 中实现。\n\n目前，AgentScope 提供以下记忆存储实现：\n\n.. list-table:: AgentScope 中的内置记忆类\n    :header-rows: 1\n\n    * - 类\n      - 描述\n    * - ``InMemoryMemory``\n      - 简单的内存记忆存储实现。\n    * - ``AsyncSQLAlchemyMemory``\n      - 基于异步 SQLAlchemy 的记忆存储实现，支持如 SQLite、PostgreSQL、MySQL 等多种关系数据库。\n    * - ``RedisMemory``\n      - 基于 Redis 的记忆存储实现。\n\n.. tip:: 如果您有兴趣贡献新的记忆存储实现，请参考 `贡献指南 <https://github.com/agentscope-ai/agentscope/blob/main/CONTRIBUTING.md#types-of-contributions>`_。\n\n以上所有记忆类均继承自基类 ``MemoryBase``，并提供以下方法来管理记忆中的消息：\n\n.. list-table:: 记忆类提供的方法\n    :header-rows: 1\n\n    * - 方法\n      - 描述\n    * - ``add(\n            memories: Msg | list[Msg] | None,\n            marks: str | list[str] | None = None,\n        ) -> None``\n      - 将 ``Msg`` 对象添加到记忆存储中，并使用给定的标记（如果提供）。\n    * - ``delete(msg_ids: list[str]) -> int``\n      - 通过ID从记忆存储中删除消息。\n    * - ``delete_by_mark(mark: str | list[str]) -> int``\n      - 通过标记从记忆中删除消息。\n    * - ``size() -> int``\n        - 获取记忆存储的大小。\n    * - ``clear() -> None``\n      - 清空记忆存储。\n    * - ``get_memory(\n            mark: str | None = None,\n            exclude_mark: str | None = None,\n        ) -> list[Msg]``\n      - 通过标记从记忆中获取消息（如果提供）。否则，获取所有消息。如果使用 ``update_compressed_summary`` 方法存储压缩摘要，它将附加到返回消息的头部。\n    * - ``update_messages_mark(\n            new_mark: str | None,\n            old_mark: str | None = None,\n            msg_ids: list[str] | None = None,\n        ) -> int``\n      - 统一的方法，用于更新存储中消息的标记（添加、删除或更改标记）。\n    * - ``update_compressed_summary(\n            summary: str,\n        ) -> None``\n      - 更新存储在记忆中的摘要属性。\n\"\"\"\nimport asyncio\nimport json\n\nimport fakeredis\nfrom sqlalchemy.ext.asyncio import create_async_engine\n\nfrom agentscope.memory import (\n    InMemoryMemory,\n    AsyncSQLAlchemyMemory,\n    RedisMemory,\n)\nfrom agentscope.message import Msg\n\n\n# %%\n# 内存记忆\n# ~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# 内存记忆提供了一种在内存中存储消息的简单方式。\n# 结合 :ref:`state` 模块，它可以在不同用户和会话之间持久化记忆内容。\n#\n\n\nasync def in_memory_example():\n    \"\"\"使用InMemoryMemory在内存中存储消息的示例。\"\"\"\n    memory = InMemoryMemory()\n    await memory.add(\n        Msg(\"Alice\", \"生成一份关于AgentScope的报告\", \"user\"),\n    )\n\n    # 添加一条带有标记\"hint\"的提示消息\n    await memory.add(\n        [\n            Msg(\n                \"system\",\n                \"<system-hint>首先创建一个计划来收集信息，然后逐步生成报告。</system-hint>\",\n                \"system\",\n            ),\n        ],\n        marks=\"hint\",\n    )\n\n    msgs = await memory.get_memory(mark=\"hint\")\n    print(\"带有标记'hint'的消息：\")\n    for msg in msgs:\n        print(f\"- {msg}\")\n\n    # 所有存储的消息都可以通过 ``state_dict`` 和 ``load_state_dict`` 方法导出和加载。\n    state = memory.state_dict()\n    print(\"记忆的状态字典：\")\n    print(json.dumps(state, indent=2, ensure_ascii=False))\n\n    # 通过标记删除消息\n    deleted_count = await memory.delete_by_mark(\"hint\")\n    print(f\"删除了 {deleted_count} 条带有标记'hint'的消息。\")\n\n    print(\"删除后的记忆状态字典：\")\n    state = memory.state_dict()\n    print(json.dumps(state, indent=2, ensure_ascii=False))\n\n\nasyncio.run(in_memory_example())\n\n# %%\n# 关系数据库记忆（Relational Database Memory）\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope 通过 SQLAlchemy 提供统一的接口来使用关系数据库，支持：\n#\n# - 多种数据库，如 SQLite、PostgreSQL、MySQL 等\n# - 用户和会话管理\n# - 生产环境中的连接池\n#\n# 具体来说，这里我们以SQLite支持的记忆为例。\n\n\nasync def sqlalchemy_example() -> None:\n    \"\"\"使用 AsyncSQLAlchemyMemory 在 SQLite 数据库中存储消息的示例。\"\"\"\n\n    # 首先创建一个异步 SQLAlchemy 引擎\n    engine = create_async_engine(\"sqlite+aiosqlite:///./test_memory.db\")\n\n    # 然后使用该引擎创建记忆\n    memory = AsyncSQLAlchemyMemory(\n        engine_or_session=engine,\n        # 可选传入指定user_id和session_id\n        user_id=\"user_1\",\n        session_id=\"session_1\",\n    )\n\n    await memory.add(\n        Msg(\"Alice\", \"生成一份关于AgentScope的报告\", \"user\"),\n    )\n\n    await memory.add(\n        [\n            Msg(\n                \"system\",\n                \"<system-hint>首先创建一个计划来收集信息，然后逐步生成报告。</system-hint>\",\n                \"system\",\n            ),\n        ],\n        marks=\"hint\",\n    )\n\n    msgs = await memory.get_memory(mark=\"hint\")\n    print(\"带有标记'hint'的消息：\")\n    for msg in msgs:\n        print(f\"- {msg}\")\n\n    # 完成后关闭引擎\n    await memory.close()\n\n\nasyncio.run(sqlalchemy_example())\n\n# %%\n# 可选地，您也可以将 ``AsyncSQLAlchemyMemory`` 用作异步上下文管理器，退出上下文时会话将自动关闭。\n\n\nasync def sqlalchemy_context_example() -> None:\n    \"\"\"使用 AsyncSQLAlchemyMemory 作为异步上下文管理器的示例。\"\"\"\n    engine = create_async_engine(\"sqlite+aiosqlite:///./test_memory.db\")\n    async with AsyncSQLAlchemyMemory(\n        engine_or_session=engine,\n        user_id=\"user_1\",\n        session_id=\"session_1\",\n    ) as memory:\n        await memory.add(\n            Msg(\"Alice\", \"生成一份关于 AgentScope 的报告\", \"user\"),\n        )\n\n        msgs = await memory.get_memory()\n        print(\"记忆中的所有消息：\")\n        for msg in msgs:\n            print(f\"- {msg}\")\n\n\nasyncio.run(sqlalchemy_context_example())\n\n# %%\n# 在生产环境中，例如使用FastAPI时，可以按如下方式启用连接池：\n#\n# .. code-block:: python\n#    :caption: FastAPI中使用连接池的SQLAlchemy记忆\n#\n#    from typing import AsyncGenerator\n#\n#     from fastapi import FastAPI, Depends\n#     from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession\n#\n#     from agentscope.agent import ReActAgent\n#     from agentscope.pipeline import stream_printing_messages\n#\n#\n#     app = FastAPI()\n#\n#     # 创建带连接池的异步SQLAlchemy引擎\n#     engine = create_async_engine(\n#         \"sqlite+aiosqlite:///./test_memory.db\",\n#         pool_size=10,\n#         max_overflow=20,\n#         pool_timeout=30,\n#         # ...  其他连接池设置\n#     )\n#\n#     # 创建会话制造器\n#     async_session_marker = async_sessionmaker(\n#         engine,\n#         expire_on_commit=False,\n#         autocommit=False,\n#         autoflush=False,\n#     )\n#\n#     async def get_db() -> AsyncGenerator[AsyncSession, None]:\n#         async with async_session_marker() as session:\n#             try:\n#                 yield session\n#                 await session.commit()\n#             except Exception:\n#                 await session.rollback()\n#                 raise\n#             finally:\n#                 await session.close()\n#\n#     @app.post(\"/chat\")\n#     async def chat_endpoint(\n#         user_id: str,\n#         session_id: str,\n#         input: str,\n#         db_session: AsyncSession = Depends(get_db),\n#     ):\n#         # 智能体的一些设置\n#         ...\n#\n#         # 使用SQLAlchemy记忆创建智能体\n#         agent = ReActAgent(\n#             # ...\n#             memory=AsyncSQLAlchemyMemory(\n#                 engine_or_session=db_session,\n#                 user_id=user_id,\n#                 session_id=session_id,\n#             ),\n#         )\n#\n#         # 处理与智能体的对话\n#         async for msg, _ in stream_printing_messages(\n#             agents=[agent],\n#             coroutine_task=agent(Msg(\"user\", input, \"user\")),\n#         ):\n#             # 将消息返回给客户端\n#             ...\n#\n#\n# NoSQL数据库记忆（NoSQL Database Memory）\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope还提供基于NoSQL数据库（如Redis）的记忆实现。\n# 它也支持用户和会话管理，以及生产环境中的连接池。\n#\n# 首先，我们可以按如下方式初始化Redis记忆：\n\n\nasync def redis_memory_example() -> None:\n    \"\"\"使用 RedisMemory 在 Redis 中存储消息的示例。\"\"\"\n    # 使用fakeredis进行内存测试，无需真实的 Redis 服务器\n    fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True)\n    # 创建 Redis 记忆\n    memory = RedisMemory(\n        # 使用fake redis进行演示\n        connection_pool=fake_redis.connection_pool,\n        # 也可以通过指定主机和端口连接到真实的Redis服务器\n        # host=\"localhost\",\n        # port=6379,\n        # 可选地指定 user_id 和 session_id\n        user_id=\"user_1\",\n        session_id=\"session_1\",\n    )\n\n    # 向记忆中添加消息\n    await memory.add(\n        Msg(\n            \"Alice\",\n            \"生成一份关于AgentScope的报告\",\n            \"user\",\n        ),\n    )\n\n    # 添加一条带有标记\"hint\"的提示消息\n    await memory.add(\n        Msg(\n            \"system\",\n            \"<system-hint>首先创建一个计划来收集信息，然后逐步生成报告。</system-hint>\",\n            \"system\",\n        ),\n        marks=\"hint\",\n    )\n\n    # 检索带有标记\"hint\"的消息\n    msgs = await memory.get_memory(mark=\"hint\")\n    print(\"带有标记'hint'的消息：\")\n    for msg in msgs:\n        print(f\"- {msg}\")\n\n\nasyncio.run(redis_memory_example())\n\n# %%\n# 同样，`RedisMemory` 也可以在生产环境中使用连接池，例如与FastAPI一起使用。\n#\n# .. code-block:: python\n#    :caption: FastAPI中使用连接池的Redis记忆\n#\n#     from fastapi import FastAPI, HTTPException\n#     from redis.asyncio import ConnectionPool\n#     from contextlib import asynccontextmanager\n#\n#     # 全局Redis连接池\n#     redis_pool: ConnectionPool | None = None\n#\n#\n#     # 使用lifespan事件管理Redis连接池\n#     @asynccontextmanager\n#     async def lifespan(app: FastAPI):\n#         global redis_pool\n#         redis_pool = ConnectionPool(\n#             host=\"localhost\",\n#             port=6379,\n#             db=0,\n#             password=None,\n#             decode_responses=True,\n#             max_connections=10,\n#             encoding=\"utf-8\",\n#         )\n#         print(\"✅ Redis连接已建立\")\n#\n#         yield\n#\n#         await redis_pool.disconnect()\n#         print(\"✅ Redis连接已关闭\")\n#\n#\n#     app = FastAPI(lifespan=lifespan)\n#\n#\n#     @app.post(\"/chat_endpoint\")\n#     async def chat_endpoint(\n#         user_id: str, session_id: str, input: str\n#     ):\n#         \"\"\"聊天端点\"\"\"\n#         global redis_pool\n#         if redis_pool is None:\n#             raise HTTPException(\n#                 status_code=500,\n#                 detail=\"Redis连接池未初始化。\",\n#             )\n#\n#         # 创建Redis记忆\n#         memory = RedisMemory(\n#             connection_pool=redis_pool,\n#             user_id=user_id,\n#             session_id=session_id,\n#         )\n#\n#         ...\n#\n#         # 完成后关闭Redis客户端连接\n#         client = memory.get_client()\n#         await client.aclose()\n#\n#\n#\n# 自定义记忆（Customizing Memory）\n# ~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# 要自定义您自己的记忆实现，只需从 ``MemoryBase`` 继承并实现以下方法：\n#\n# .. list-table::\n#     :header-rows: 1\n#\n#     * - 方法\n#       - 描述\n#     * - ``add``\n#       - 向记忆中添加 ``Msg`` 对象\n#     * - ``delete``\n#       - 从记忆中删除 ``Msg`` 对象\n#     * - ``delete_by_mark``\n#       - 通过标记从记忆中删除 ``Msg`` 对象\n#     * - ``size``\n#       - 记忆的大小\n#     * - ``clear``\n#       - 清空记忆内容\n#     * - ``get_memory``\n#       - 以 ``Msg`` 对象列表的形式获取记忆内容\n#     * - ``update_messages_mark``\n#       - 更新记忆中消息的标记\n#     * - ``state_dict``\n#       - 获取记忆的状态字典\n#     * - ``load_state_dict``\n#       - 加载记忆的状态字典\n#\n# 延伸阅读\n# ~~~~~~~~~~~~~~~~~~~~~~~~\n# - :ref:`agent`\n# - :ref:`long-term-memory`\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_middleware.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _middleware:\n\n中间件\n===========================\n\nAgentScope 提供了灵活的中间件系统，允许开发者拦截和修改各种操作的执行。\n目前，中间件支持已在 ``Toolkit`` 类中实现，用于**工具执行**。\n\n中间件系统遵循**洋葱模型**，每个中间件包裹在前一个中间件之外，形成层次结构。\n这使得开发者可以：\n\n- 在操作前进行**预处理**\n- 在执行过程中**拦截和修改**响应\n- 在操作完成后进行**后处理**\n- 根据条件**跳过**操作执行\n\n.. tip:: 未来版本的 AgentScope 将扩展中间件支持到其他组件，如智能体和模型。\n\n\"\"\"\nimport asyncio\nfrom typing import AsyncGenerator, Callable\n\nfrom agentscope.message import TextBlock, ToolUseBlock\nfrom agentscope.tool import ToolResponse, Toolkit\n\n\n# %%\n# 工具执行中间件\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# ``Toolkit`` 类通过 ``register_middleware`` 方法支持工具执行的中间件。\n# 每个中间件可以拦截工具调用并修改输入或输出。\n#\n# 中间件签名\n# ------------------------------\n#\n# 中间件函数应具有以下签名：\n#\n# .. code-block:: python\n#\n#     async def middleware(\n#         kwargs: dict,\n#         next_handler: Callable,\n#     ) -> AsyncGenerator[ToolResponse, None]:\n#         # 从 kwargs 访问参数\n#         tool_call = kwargs[\"tool_call\"]\n#\n#         # 预处理\n#         # ...\n#\n#         # 调用下一个中间件或工具函数\n#         async for response in await next_handler(**kwargs):\n#             # 后处理\n#             yield response\n#\n# .. list-table:: 中间件参数\n#    :header-rows: 1\n#\n#    * - 参数\n#      - 类型\n#      - 描述\n#    * - ``kwargs``\n#      - ``dict``\n#      - 上下文参数。当前包含 ``tool_call`` (ToolUseBlock)。未来版本可能包含更多参数。\n#    * - ``next_handler``\n#      - ``Callable``\n#      - 一个可调用对象，接受 kwargs dict 并返回产生 AsyncGenerator[ToolResponse] 的协程\n#    * - **返回值**\n#      - ``AsyncGenerator[ToolResponse, None]``\n#      - 产生 ToolResponse 对象的异步生成器\n#\n# 基本示例\n# ------------------------------\n#\n# 以下是一个记录工具调用的简单中间件：\n#\n\n\nasync def logging_middleware(\n    kwargs: dict,\n    next_handler: Callable,\n) -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"记录工具执行的中间件。\"\"\"\n    # 从 kwargs 访问工具调用\n    tool_call = kwargs[\"tool_call\"]\n\n    # 预处理：在工具执行前记录日志\n    print(f\"[中间件] 调用工具：{tool_call['name']}\")\n    print(f\"[中间件] 输入：{tool_call['input']}\")\n\n    # 调用下一个处理器（另一个中间件或实际工具）\n    async for response in await next_handler(**kwargs):\n        # 后处理：记录响应\n        print(f\"[中间件] 响应：{response.content[0]['text']}\")\n        yield response\n\n    # 在所有响应产生后执行\n    print(f\"[中间件] 工具 {tool_call['name']} 完成\")\n\n\n# %%\n# 让我们将这个中间件注册到工具包并测试它：\n#\n\n\nasync def search_tool(query: str) -> ToolResponse:\n    \"\"\"一个简单的搜索工具。\n\n    Args:\n        query (`str`):\n            搜索查询。\n\n    Returns:\n        `ToolResponse`:\n            搜索结果。\n    \"\"\"\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=f\"'{query}' 的搜索结果\",\n            ),\n        ],\n    )\n\n\nasync def example_logging_middleware() -> None:\n    \"\"\"使用日志中间件的示例。\"\"\"\n    # 创建工具包并注册工具\n    toolkit = Toolkit()\n    toolkit.register_tool_function(search_tool)\n\n    # 注册中间件\n    toolkit.register_middleware(logging_middleware)\n\n    # 调用工具\n    result = await toolkit.call_tool_function(\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"1\",\n            name=\"search_tool\",\n            input={\"query\": \"AgentScope\"},\n        ),\n    )\n\n    async for response in result:\n        print(f\"\\n[最终] {response.content[0]['text']}\\n\")\n\n\nprint(\"=\" * 60)\nprint(\"示例 1：日志中间件\")\nprint(\"=\" * 60)\nasyncio.run(example_logging_middleware())\n\n# %%\n# 修改输入和输出\n# ------------------------------\n#\n# 中间件还可以修改工具调用的输入和响应内容：\n#\n\n\nasync def transform_middleware(\n    kwargs: dict,\n    next_handler: Callable,\n) -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"转换输入和输出的中间件。\"\"\"\n    # 从 kwargs 访问工具调用\n    tool_call = kwargs[\"tool_call\"]\n\n    # 预处理：修改输入\n    original_query = tool_call[\"input\"][\"query\"]\n    tool_call[\"input\"][\"query\"] = f\"[已转换] {original_query}\"\n\n    async for response in await next_handler(**kwargs):\n        # 后处理：修改响应\n        original_text = response.content[0][\"text\"]\n        response.content[0][\"text\"] = f\"{original_text} [已修改]\"\n        yield response\n\n\nasync def example_transform_middleware() -> None:\n    \"\"\"转换中间件的示例。\"\"\"\n    toolkit = Toolkit()\n    toolkit.register_tool_function(search_tool)\n    toolkit.register_middleware(transform_middleware)\n\n    result = await toolkit.call_tool_function(\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"2\",\n            name=\"search_tool\",\n            input={\"query\": \"中间件\"},\n        ),\n    )\n\n    async for response in result:\n        print(f\"结果：{response.content[0]['text']}\")\n\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"示例 2：转换中间件\")\nprint(\"=\" * 60)\nasyncio.run(example_transform_middleware())\n\n# %%\n# 授权中间件\n# ------------------------------\n#\n# 可以使用中间件实现授权检查，如果未授权则跳过工具执行：\n#\n\n\nasync def authorization_middleware(\n    kwargs: dict,\n    next_handler: Callable,\n) -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"检查授权的中间件。\"\"\"\n    # 从 kwargs 访问工具调用\n    tool_call = kwargs[\"tool_call\"]\n\n    # 检查工具是否已授权（简单示例）\n    authorized_tools = {\"search_tool\"}\n\n    if tool_call[\"name\"] not in authorized_tools:\n        # 跳过执行并直接返回错误\n        print(f\"[授权] 工具 {tool_call['name']} 未授权\")\n        yield ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=f\"错误：工具 '{tool_call['name']}' 未授权\",\n                ),\n            ],\n        )\n        return\n\n    # 工具已授权，继续执行\n    print(f\"[授权] 工具 {tool_call['name']} 已授权\")\n    async for response in await next_handler(**kwargs):\n        yield response\n\n\nasync def unauthorized_tool(data: str) -> ToolResponse:\n    \"\"\"一个未授权的工具。\n\n    Args:\n        data (`str`):\n            一些数据。\n\n    Returns:\n        `ToolResponse`:\n            结果。\n    \"\"\"\n    return ToolResponse(\n        content=[TextBlock(type=\"text\", text=f\"处理 {data}\")],\n    )\n\n\nasync def example_authorization_middleware() -> None:\n    \"\"\"授权中间件的示例。\"\"\"\n    toolkit = Toolkit()\n    toolkit.register_tool_function(search_tool)\n    toolkit.register_tool_function(unauthorized_tool)\n    toolkit.register_middleware(authorization_middleware)\n\n    # 尝试授权的工具\n    print(\"\\n调用已授权的工具：\")\n    result = await toolkit.call_tool_function(\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"3\",\n            name=\"search_tool\",\n            input={\"query\": \"测试\"},\n        ),\n    )\n    async for response in result:\n        print(f\"结果：{response.content[0]['text']}\")\n\n    # 尝试未授权的工具\n    print(\"\\n调用未授权的工具：\")\n    result = await toolkit.call_tool_function(\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"4\",\n            name=\"unauthorized_tool\",\n            input={\"data\": \"测试\"},\n        ),\n    )\n    async for response in result:\n        print(f\"结果：{response.content[0]['text']}\")\n\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"示例 3：授权中间件\")\nprint(\"=\" * 60)\nasyncio.run(example_authorization_middleware())\n\n# %%\n# 多个中间件（洋葱模型）\n# ------------------------------\n#\n# 当注册多个中间件时，它们形成类似洋葱的结构。\n# 执行顺序遵循洋葱模型：\n#\n# - **预处理**：按照中间件注册的顺序执行\n# - **后处理**：按相反顺序执行（从内到外）\n#\n# 这是因为实际的工具响应对象会通过中间件链传递，\n# 每个中间件都会原地修改它。\n#\n\n\nasync def middleware_1(\n    kwargs: dict,\n    next_handler: Callable,\n) -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"第一个中间件。\"\"\"\n    # 从 kwargs 访问工具调用\n    tool_call = kwargs[\"tool_call\"]\n\n    # 预处理\n    print(\"[M1] 预处理\")\n    tool_call[\"input\"][\"query\"] += \" [M1]\"\n\n    async for response in await next_handler(**kwargs):\n        # 后处理\n        response.content[0][\"text\"] += \" [M1]\"\n        print(\"[M1] 后处理\")\n        yield response\n\n\nasync def middleware_2(\n    kwargs: dict,\n    next_handler: Callable,\n) -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"第二个中间件。\"\"\"\n    # 从 kwargs 访问工具调用\n    tool_call = kwargs[\"tool_call\"]\n\n    # 预处理\n    print(\"[M2] 预处理\")\n    tool_call[\"input\"][\"query\"] += \" [M2]\"\n\n    async for response in await next_handler(**kwargs):\n        # 后处理\n        response.content[0][\"text\"] += \" [M2]\"\n        print(\"[M2] 后处理\")\n        yield response\n\n\nasync def example_multiple_middleware() -> None:\n    \"\"\"多个中间件的示例。\"\"\"\n    toolkit = Toolkit()\n    toolkit.register_tool_function(search_tool)\n\n    # 按顺序注册中间件\n    toolkit.register_middleware(middleware_1)\n    toolkit.register_middleware(middleware_2)\n\n    result = await toolkit.call_tool_function(\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"5\",\n            name=\"search_tool\",\n            input={\"query\": \"测试\"},\n        ),\n    )\n\n    async for response in result:\n        print(f\"\\n最终结果：{response.content[0]['text']}\")\n\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"示例 4：多个中间件（洋葱模型）\")\nprint(\"=\" * 60)\nprint(\"\\n执行流程：\")\nprint(\"M1 预处理 → M2 预处理 → 工具 → M2 后处理 → M1 后处理\")\nprint()\nasyncio.run(example_multiple_middleware())\n\n# %%\n# 使用场景\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# 中间件系统适用于各种场景：\n#\n# - **日志和监控**：跟踪工具使用情况和性能\n# - **授权**：控制对特定工具的访问\n# - **速率限制**：限制工具调用的频率\n# - **缓存**：缓存重复调用的工具响应\n# - **错误处理**：添加重试逻辑或优雅降级\n# - **输入验证**：验证和清理工具输入\n# - **输出转换**：格式化或过滤工具输出\n# - **指标收集**：收集有关工具使用情况的统计信息\n#\n# .. note::\n#     - 中间件按注册顺序应用\n#     - 同一个 ``ToolResponse`` 对象通过中间件链传递并原地修改\n#     - 中间件可以通过不调用 ``next_handler`` 来完全跳过工具执行\n#     - 所有中间件必须是产生 ``ToolResponse`` 对象的异步生成器函数\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_model.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _model:\n\n模型\n====================\n\n在本教程中，我们介绍 AgentScope 中集成的模型 API、如何使用它们，以及如何集成新的模型 API。\nAgentScope 目前支持的模型 API 和模型提供商包括：\n\n.. list-table::\n    :header-rows: 1\n\n    * - API\n      - 类\n      - 兼容\n      - 流式\n      - 工具\n      - 视觉\n      - 推理\n    * - OpenAI\n      - ``OpenAIChatModel``\n      - vLLM, DeepSeek\n      - ✅\n      - ✅\n      - ✅\n      - ✅\n    * - DashScope\n      - ``DashScopeChatModel``\n      -\n      - ✅\n      - ✅\n      - ✅\n      - ✅\n    * - Anthropic\n      - ``AnthropicChatModel``\n      -\n      - ✅\n      - ✅\n      - ✅\n      - ✅\n    * - Gemini\n      - ``GeminiChatModel``\n      -\n      - ✅\n      - ✅\n      - ✅\n      - ✅\n    * - Ollama\n      - ``OllamaChatModel``\n      -\n      - ✅\n      - ✅\n      - ✅\n      - ✅\n\n.. note:: 当使用 vLLM 时，需要在部署时为不同模型配置相应的工具调用参数，例如 ``--enable-auto-tool-choice``、``--tool-call-parser`` 等参数。更多详情请参考 `vLLM 官方文档 <https://docs.vllm.ai/en/latest/features/tool_calling.html>`_。\n\n.. note:: 兼容 OpenAI API 的模型（例如 vLLM 部署的模型），推荐使用 ``OpenAIChatModel``，并通过 ``client_kwargs={\"base_url\": \"http://your-api-endpoint\"}`` 参数指定 API 端点。例如：\n\n    .. code-block:: python\n\n        OpenAIChatModel(client_kwargs={\"base_url\": \"http://localhost:8000/v1\"})\n\n.. note:: 模型的行为参数（如温度、最大长度等）可以通过 ``generate_kwargs`` 参数在构造函数中提前设定。例如：\n\n    .. code-block:: python\n\n        OpenAIChatModel(generate_kwargs={\"temperature\": 0.3, \"max_tokens\": 1000})\n\n为了提供统一的模型接口，上述所有类均被统一为：\n\n- ``__call__`` 函数的前三个参数是 ``messages``，``tools`` 和 ``tool_choice``，分别是输入消息，工具函数的 JSON schema，以及工具选择的模式。\n- 非流式返回时，返回类型是 ``ChatResponse`` 实例；流式返回时，返回的是 ``ChatResponse`` 的异步生成器。\n\n.. note:: 不同的模型 API 在输入消息格式上有所不同，AgentScope 通过 formatter 模块处理消息的转换，请参考 :ref:`format`。\n\n``ChatResponse`` 包含大模型生成的推理/文本/工具使用内容、身份、创建时间和使用信息。\n\"\"\"\nimport asyncio\nimport json\nimport os\n\nfrom agentscope.message import TextBlock, ToolUseBlock, ThinkingBlock, Msg\nfrom agentscope.model import ChatResponse, DashScopeChatModel\n\nresponse = ChatResponse(\n    content=[\n        ThinkingBlock(\n            type=\"thinking\",\n            thinking=\"我应该在 Google 上搜索 AgentScope。\",\n        ),\n        TextBlock(type=\"text\", text=\"我将在 Google 上搜索 AgentScope。\"),\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"642n298gjna\",\n            name=\"google_search\",\n            input={\"query\": \"AgentScope\"},\n        ),\n    ],\n)\n\nprint(response)\n\n# %%\n# 以 ``DashScopeChatModel`` 为例，调用和返回结果如下：\n\n\nasync def example_model_call() -> None:\n    \"\"\"使用 DashScopeChatModel 的示例。\"\"\"\n    model = DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        stream=False,\n    )\n\n    res = await model(\n        messages=[\n            {\"role\": \"user\", \"content\": \"你好！\"},\n        ],\n    )\n\n    # 您可以直接使用响应内容创建 ``Msg`` 对象\n    msg_res = Msg(\"Friday\", res.content, \"assistant\")\n\n    print(\"LLM 返回结果:\", res)\n    print(\"作为 Msg 的响应:\", msg_res)\n\n\nasyncio.run(example_model_call())\n\n# %%\n# 流式返回\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# 要启用流式返回，请在模型的构造函数中将 ``stream`` 参数设置为 ``True``。\n# 流式返回中，``__call__`` 方法将返回一个 **异步生成器**，该生成器迭代返回 ``ChatResponse`` 实例。\n#\n# .. note:: AgentScope 中的流式返回结果为 **累加式**，这意味着每个 chunk 中的内容包含所有之前的内容加上新生成的内容。\n#\n\n\nasync def example_streaming() -> None:\n    \"\"\"使用流式模型的示例。\"\"\"\n    model = DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        stream=True,\n    )\n\n    generator = await model(\n        messages=[\n            {\n                \"role\": \"user\",\n                \"content\": \"从 1 数到 20，只报告数字，不要任何其他信息。\",\n            },\n        ],\n    )\n    print(\"响应的类型:\", type(generator))\n\n    i = 0\n    async for chunk in generator:\n        print(f\"块 {i}\")\n        print(f\"\\t类型: {type(chunk.content)}\")\n        print(f\"\\t{chunk}\\n\")\n        i += 1\n\n\nasyncio.run(example_streaming())\n\n# %%\n# 推理模型\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope 通过提供 ``ThinkingBlock`` 来支持推理模型。\n#\n\n\nasync def example_reasoning() -> None:\n    \"\"\"使用推理模型的示例。\"\"\"\n    model = DashScopeChatModel(\n        model_name=\"qwen-turbo\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        enable_thinking=True,\n    )\n\n    res = await model(\n        messages=[\n            {\"role\": \"user\", \"content\": \"我是谁？\"},\n        ],\n    )\n\n    last_chunk = None\n    async for chunk in res:\n        last_chunk = chunk\n    print(\"最终响应:\")\n    print(last_chunk)\n\n\nasyncio.run(example_reasoning())\n\n# %%\n# 工具 API\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# 不同的模型提供商在工具 API 方面有所不同，例如工具 JSON schema、工具调用/响应格式。\n# 为了提供统一的接口，AgentScope 通过以下方式解决了这个问题：\n#\n# - 提供了统一的工具调用结构 block :ref:`ToolUseBlock <tool-block>` 和工具响应结构 :ref:`ToolResultBlock <tool-block>`。\n# - 在模型类的 ``__call__`` 方法中提供统一的工具接口 ``tools``，接受工具 JSON schema 列表，如下所示：\n#\n\njson_schemas = [\n    {\n        \"type\": \"function\",\n        \"function\": {\n            \"name\": \"google_search\",\n            \"description\": \"在 Google 上搜索查询。\",\n            \"parameters\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"query\": {\n                        \"type\": \"string\",\n                        \"description\": \"搜索查询。\",\n                    },\n                },\n                \"required\": [\"query\"],\n            },\n        },\n    },\n]\n\n# %%\n# 进一步阅读\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# - :ref:`message`\n# - :ref:`prompt`\n#\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_pipeline.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _pipeline:\n\n管道 (Pipeline)\n========================\n\n对于多智能体编排，AgentScope 提供了 ``agentscope.pipeline`` 模块\n作为将智能体链接在一起的语法糖，具体包括\n\n- **MsgHub**: 用于多个智能体之间消息的广播\n- **sequential_pipeline** 和 **SequentialPipeline**: 以顺序方式执行多个智能体的函数式和类式实现\n- **fanout_pipeline** 和 **FanoutPipeline**: 将相同输入分发给多个智能体的函数式和类式实现\n- **stream_printing_messages**: 将智能体在回复过程中，调用 ``self.print`` 打印的消息转换为一个异步生成器\n\n\"\"\"\n\nimport os, asyncio\n\nfrom agentscope.formatter import DashScopeMultiAgentFormatter\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.agent import ReActAgent\nfrom agentscope.pipeline import MsgHub, stream_printing_messages\n\n\n# %%\n# 使用 MsgHub 进行广播\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# ``MsgHub`` 类是一个 **异步上下文管理器**，它接收一个智能体列表作为其参与者。\n# 当一个参与者生成回复消息时，将通过调用所有其他参与者的 ``observe`` 方法广播该消息。\n# 这意味着在 ``MsgHub`` 上下文中，开发者无需手动将回复消息从一个智能体发送到另一个智能体。\n#\n# 这里我们创建四个智能体：Alice、Bob、Charlie 和 David。\n# 然后我们让 Alice、Bob 和 Charlie 通过自我介绍开始一个会议。需要注意的是 David 没有包含在这个会议中。\n#\n\n\ndef create_agent(name: str, age: int, career: str) -> ReActAgent:\n    \"\"\"根据给定信息创建智能体对象。\"\"\"\n    return ReActAgent(\n        name=name,\n        sys_prompt=f\"你是{name}，一个{age}岁的{career}\",\n        model=DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        ),\n        formatter=DashScopeMultiAgentFormatter(),\n    )\n\n\nalice = create_agent(\"Alice\", 50, \"老师\")\nbob = create_agent(\"Bob\", 35, \"工程师\")\ncharlie = create_agent(\"Charlie\", 28, \"设计师\")\ndavid = create_agent(\"David\", 30, \"开发者\")\n\n# %%\n# 然后我们创建一个 ``MsgHub`` 上下文，并让他们自我介绍:\n#\n# .. hint:: ``announcement`` 中的消息将在进入 ``MsgHub`` 上下文时广播给所有参与者。\n#\n\n\nasync def example_broadcast_message():\n    \"\"\"使用 MsgHub 广播消息的示例。\"\"\"\n\n    # 创建消息中心\n    async with MsgHub(\n        participants=[alice, bob, charlie],\n        announcement=Msg(\n            \"user\",\n            \"现在请简要介绍一下自己，包括你的姓名、年龄和职业。\",\n            \"user\",\n        ),\n    ) as hub:\n        # 无需手动消息传递的群聊\n        await alice()\n        await bob()\n        await charlie()\n\n\nasyncio.run(example_broadcast_message())\n\n# %%\n# 现在让我们检查 Bob、Charlie 和 David 是否收到了 Alice 的消息。\n#\n\n\nasync def check_broadcast_message():\n    \"\"\"检查消息是否正确广播。\"\"\"\n    user_msg = Msg(\n        \"user\",\n        \"你知道 Alice 是谁吗，她是做什么的？\",\n        \"user\",\n    )\n\n    await bob(user_msg)\n    await charlie(user_msg)\n    await david(user_msg)\n\n\nasyncio.run(check_broadcast_message())\n\n# %%\n# 现在我们观察到 Bob 和 Charlie 知道 Alice 和她的职业，而 David 对\n# Alice 一无所知，因为他没有包含在 ``MsgHub`` 上下文中。\n#\n#\n# 动态管理\n# ---------------------------\n# 此外，``MsgHub`` 支持通过以下方法动态管理参与者：\n#\n# - ``add``: 添加一个或多个智能体作为新参与者\n# - ``delete``: 从参与者中移除一个或多个智能体，他们将不再接收广播消息\n# - ``broadcast``: 向所有当前参与者广播消息\n#\n# .. note:: 新添加的参与者不会接收到之前的消息。\n#\n# .. code-block:: python\n#\n#       async with MsgHub(participants=[alice]) as hub:\n#           # 添加新参与者\n#           hub.add(david)\n#\n#           # 移除参与者\n#           hub.delete(alice)\n#\n#           # 向所有当前参与者广播\n#           await hub.broadcast(\n#               Msg(\"system\", \"现在我们开始...\", \"system\"),\n#           )\n#\n#\n# 管道\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# 管道是 AgentScope 中多智能体编排的一种语法糖。\n#\n# 目前，AgentScope 提供三种管道，用于减轻开发者的负担：\n#\n# 1. **顺序管道 (Sequential Pipeline)**: 按预定义顺序逐个执行智能体\n# 2. **扇出管道 (Fanout Pipeline)**: 将相同输入分发给多个智能体并收集它们的响应\n# 3. **流式获取打印消息 (stream printing messages)**: 将智能体在回复过程中，调用 ``self.print`` 打印的消息转换为一个异步生成器\n#\n# 顺序管道\n# ------------------------\n# 顺序管道逐个执行智能体，前一个智能体的输出成为下一个智能体的输入。\n#\n# 例如，以下两个代码片段是等价的：\n#\n# .. code-block:: python\n#     :caption: 代码片段 1: 手动消息传递\n#\n#     msg = None\n#     msg = await alice(msg)\n#     msg = await bob(msg)\n#     msg = await charlie(msg)\n#     msg = await david(msg)\n#\n#\n# .. code-block:: python\n#     :caption: 代码片段 2: 使用顺序管道\n#\n#     from agentscope.pipeline import sequential_pipeline\n#\n#     msg = await sequential_pipeline(\n#         # 按顺序执行的智能体列表\n#         agents=[alice, bob, charlie, david],\n#         # 第一个输入消息，可以是 None\n#         msg=None\n#     )\n#\n\n# %%\n# 扇出管道\n# ------------------------\n# 扇出管道将相同的输入消息同时分发给多个智能体并收集所有响应。当你想要收集对同一话题的不同观点或专业意见时，这非常有用。\n#\n# 例如，以下两个代码片段是等价的：\n#\n# .. code-block:: python\n#     :caption: 代码片段 3: 手动逐个调用智能体\n#\n#     from copy import deepcopy\n#\n#     msgs = []\n#     msg = None\n#     for agent in [alice, bob, charlie, david]:\n#         msgs.append(await agent(deepcopy(msg)))\n#\n#\n# .. code-block:: python\n#     :caption: 代码片段 4: 使用扇出管道\n#\n#     from agentscope.pipeline import fanout_pipeline\n#\n#     msgs = await fanout_pipeline(\n#         # 要执行的智能体列表\n#         agents=[alice, bob, charlie, david],\n#         # 输入消息，可以是 None\n#         msg=None,\n#         enable_gather=False,\n#     )\n#\n# .. note::\n#     ``enable_gather`` 参数控制扇出管道的执行模式：\n#\n#     - ``enable_gather=True`` (默认): 使用 ``asyncio.gather()`` **并发** 执行所有智能体。这为 I/O 密集型操作（如 API 调用）提供更好的性能，因为智能体并行运行。\n#     - ``enable_gather=False``: 逐个 **顺序** 执行智能体。当你需要确定性的执行顺序或想要避免并发请求压垮外部服务时，这很有用。\n#\n#     选择并发执行以获得更好的性能，或选择顺序执行以获得可预测的顺序和资源控制。\n#\n# .. tip::\n#     通过结合 ``MsgHub`` 和 ``sequential_pipeline`` 或 ``fanout_pipeline``，你可以非常容易地创建更复杂的工作流。\n\n# %%\n# 流式获取打印消息\n# ------------------------\n# ``stream_printing_messages`` 函数将智能体在回复过程中调用 ``self.print`` 打印的消息转换为一个异步生成器。\n# 可以帮助开发者快速以流式方式获取智能体的中间消息。\n#\n# 该函数接受一个或多个智能体和一个协程任务作为输入，并返回一个异步生成器。\n# 该异步生成器返回一个二元组，包含执行协程任务过程中通过 ``await self.print(...)`` 打印的消息，以及一个布尔值，表示该消息是否为一组流式消息中的最后一个。\n#\n# 需要注意的是，生成器返回的元组中，布尔值表示该消息是否为一组流式消息中的最后一个，而非此次智能体调用的最后一条消息。\n\n\nasync def run_example_pipeline() -> None:\n    \"\"\"运行流式打印消息的示例。\"\"\"\n    agent = create_agent(\"Alice\", 20, \"student\")\n\n    # 我们关闭agent的终端打印以避免输出混乱\n    agent.set_console_output_enabled(False)\n\n    async for msg, last in stream_printing_messages(\n        agents=[agent],\n        coroutine_task=agent(\n            Msg(\"user\", \"你好，你是谁？\", \"user\"),\n        ),\n    ):\n        print(msg, last)\n        if last:\n            print()\n\n\nasyncio.run(run_example_pipeline())\n\n\n# %%\n# 高级管道特性\n# ~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# 此外，为了可重用性，我们还提供了基于类的实现：\n#\n# .. code-block:: python\n#    :caption: 使用 SequentialPipeline 类\n#\n#     from agentscope.pipeline import SequentialPipeline\n#\n#     # 创建管道对象\n#     pipeline = SequentialPipeline(agents=[alice, bob, charlie, david])\n#\n#     # 调用管道\n#     msg = await pipeline(msg=None)\n#\n#     # 使用不同输入复用管道\n#     msg = await pipeline(msg=Msg(\"user\", \"你好！\", \"user\"))\n#\n#\n# .. code-block:: python\n#     :caption: 使用 FanoutPipeline 类\n#\n#     from agentscope.pipeline import FanoutPipeline\n#\n#     # 创建管道对象\n#     pipeline = FanoutPipeline(agents=[alice, bob, charlie, david])\n#\n#     # 调用管道\n#     msgs = await pipeline(msg=None)\n#\n#     # 使用不同输入复用管道\n#     msgs = await pipeline(msg=Msg(\"user\", \"你好！\", \"user\"))\n#\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_plan.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _plan:\n\n计划\n=========================\n\nAgentScope 中的计划（Plan）模块使智能体能够正式地将复杂任务分解为可管理的子任务并系统地执行它们。主要功能包括：\n\n- 支持 **手动计划规范**\n- 全面的计划管理功能：\n   - **创建、修改、放弃和恢复** 计划\n   - 在多个计划之间 **切换**\n   - 通过临时暂停计划来处理用户查询或紧急任务，**优雅地处理中断**\n- 计划执行的 **实时可视化和监控**\n\n.. note:: 当前计划模块仅支持子任务按照顺序执行。\n\n\n具体来说，计划模块的工作原理是\n\n- 提供计划管理的工具函数\n- 插入提示消息来指导ReAct智能体完成计划\n\n下图说明了计划模块如何与ReAct智能体协作：\n\n.. figure:: ../../_static/images/plan.png\n    :width: 90%\n    :alt: 计划模块\n    :class: bordered-image\n    :align: center\n\n    计划模块如何与ReAct智能体协作\n\n\"\"\"\nimport asyncio\nimport os\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.plan import PlanNotebook, Plan, SubTask\n\n# %%\n# PlanNotebook\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# ``PlanNotebook`` 类是计划模块的核心，负责提供\n#\n# - 管理计划，子任务的工具函数\n# - 提供用于“引导智能体正确完成任务”的提示消息（Hint message）\n#\n# ``PlanNotebook`` 类使用以下参数实例化：\n#\n# .. list-table:: ``PlanNotebook`` 构造函数的参数\n#   :header-rows: 1\n#\n#   * - 名称\n#     - 类型\n#     - 描述\n#   * - ``max_subtasks``\n#     - ``int | None``\n#     - 计划中允许的子任务最大数量，如果为 ``None`` 则无限制\n#   * - ``plan_to_hint``\n#     - ``Callable[[Plan | None], str | None] | None``\n#     - 基于当前计划的完成情况，生成对应提示消息的函数。如果未提供，将使用默认的 ``DefaultPlanToHint`` 对象。\n#   * - ``storage``\n#     - ``PlanStorageBase | None``\n#     - 计划的存储模块，用于恢复，保存历史计划。如果未提供，将使用默认的内存（In-memory）存储。\n#\n# ``plan_to_hint`` 参数是 ``PlanNotebook`` 类的核心参数，也是开发者进行提示工程的接口。\n# 作为可调用对象，接受当前计划作为输入，并返回一个字符串类型的提示消息。\n# AgentScope 构建了一个默认的 ``DefaultPlanToHint`` 类，可以直接使用，同时我们鼓励开发者提供自己的 ``plan_to_hint`` 函数以获得更好的性能。\n#\n# ``storage`` 用于存储历史计划，允许智能体检索和恢复历史计划。\n# 我们同样鼓励开发者通过继承 ``PlanStorageBase`` 类来实现自己的计划存储。如果未提供，将使用默认的内存存储。\n#\n# .. tip:: ``PlanStorageBase`` 类继承自 ``StateModule`` 类，因此 storage也会通过会话管理进行保存和加载。\n#\n# ``PlanNotebook`` 类的核心属性和方法总结如下：\n#\n# .. list-table:: ``PlanNotebook`` 类的核心属性和方法\n#    :header-rows: 1\n#\n#    * - 类型\n#      - 名称\n#      - 描述\n#    * - 属性\n#      - ``current_plan``\n#      - 智能体正在执行的当前计划\n#    * -\n#      - ``storage``\n#      - 历史计划的存储，用于检索和恢复历史计划\n#    * -\n#      - ``plan_to_hint``\n#      - 一个可调用对象，以当前计划为输入并生成提示消息来指导智能体完成计划\n#    * - 函数\n#      - ``list_tools``\n#      - 列出 ``PlanNotebook`` 类提供的所有工具函数\n#    * -\n#      - ``get_current_hint``\n#      - 获取当前计划的提示消息，将调用 ``plan_to_hint`` 函数\n#    * -\n#      - | ``create_plan``,\n#        | ``view_subtasks``,\n#        | ``revise_current_plan``,\n#        | ``update_subtask_state``,\n#        | ``finish_subtask``,\n#        | ``finish_plan``,\n#        | ``view_historical_plans``,\n#        | ``recover_historical_plan``\n#      - 允许智能体管理计划和子任务的工具函数\n#    * -\n#      - ``register_plan_change_hook``\n#      - 注册一个钩子函数，当计划发生变化时将被调用，用于计划可视化和监控\n#    * -\n#      - ``remove_plan_change_hook``\n#      - 移除已注册的钩子函数\n#\n# ``list_tools`` 方法是获取所有工具函数的快速方法，这样您就可以将它们注册到智能体的工具包中。\n\nplan_notebook = PlanNotebook()\n\n\nasync def list_tools() -> None:\n    \"\"\"列出PlanNotebook提供的工具函数。\"\"\"\n    print(\"PlanNotebook提供的工具：\")\n    for tool in plan_notebook.list_tools():\n        print(tool.__name__)\n\n\nasyncio.run(list_tools())\n\n\n# %%\n# 与ReActAgent协作\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope中的 ``ReActAgent`` 已通过构造函数中的 ``plan_notebook`` 参数集成了计划模块。\n# 一旦提供，智能体将\n#\n# - 配备计划管理工具函数，并且\n# - 在每个推理步骤开始时插入提示消息\n#\n# 有两种方式在 ``ReActAgent`` 中使用计划模块：\n#\n# - 开发者指定计划：开发者可以通过调用 ``create_plan`` 工具函数手动创建计划，并使用该计划来初始化 ``ReActAgent`` 。\n# - 智能体管理的计划执行：智能体将通过调用计划管理工具函数自己创建和管理计划。\n#\n# 手动计划规范\n# ---------------------------------\n# 通过调用 ``create_plan`` 工具函数手动创建计划非常简单。\n# 以下是手动创建计划以对LLM赋能的智能体进行全面研究的示例。\n#\nasync def manual_plan_specification() -> None:\n    \"\"\"手动计划规范示例。\"\"\"\n    await plan_notebook.create_plan(\n        name=\"智能体研究\",\n        description=\"对基于LLM的智能体进行全面研究\",\n        expected_outcome=\"一份Markdown格式的报告，回答三个问题：1. 什么是智能体？2. 智能体的当前技术水平是什么？3. 智能体的未来趋势是什么？\",\n        subtasks=[\n            SubTask(\n                name=\"搜索智能体相关调研论文\",\n                description=(\n                    \"在多个来源搜索调研论文，包括\"\n                    \"Google Scholar、arXiv和Semantic Scholar。必须\"\n                    \"在2021年后发表且引用数超过50。\"\n                ),\n                expected_outcome=\"Markdown格式的论文列表\",\n            ),\n            SubTask(\n                name=\"阅读和总结论文\",\n                description=\"阅读前一步找到的论文，并总结关键点，包括定义、分类、挑战和关键方向。\",\n                expected_outcome=\"Markdown格式的关键点总结\",\n            ),\n            SubTask(\n                name=\"研究大公司的最新进展\",\n                description=(\n                    \"研究大公司的最新进展，包括但不限于Google、Microsoft、OpenAI、\"\n                    \"Anthropic、阿里巴巴和Meta。查找官方博客或新闻文章。\"\n                ),\n                expected_outcome=\"大公司的最新进展\",\n            ),\n            SubTask(\n                name=\"撰写报告\",\n                description=\"基于前面的步骤撰写报告，并回答预期结果中的三个问题。\",\n                expected_outcome=(\n                    \"一份Markdown格式的报告，回答三个问题：1. \"\n                    \"什么是智能体？2. 智能体的当前技术水平\"\n                    \"是什么？3. 智能体的未来趋势是什么？\"\n                ),\n            ),\n        ],\n    )\n\n    print(\"当前提示消息：\\n\")\n    msg = await plan_notebook.get_current_hint()\n    print(f\"{msg.name}: {msg.content}\")\n\n\nasyncio.run(manual_plan_specification())\n\n# %%\n# 创建计划后，可以按如下方式使用计划笔记本初始化 ``ReActAgent`` ：\n\nagent = ReActAgent(\n    name=\"Friday\",\n    sys_prompt=\"你是一个有用的助手。\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n    ),\n    formatter=DashScopeChatFormatter(),\n    plan_notebook=plan_notebook,\n)\n\n# %%\n# 智能体自主管理\n# ---------------------------------\n# 智能体也可以通过调用计划管理工具函数自己创建和管理计划。\n# 我们只需要按如下方式使用计划笔记本初始化 ``ReActAgent`` ：\n#\n\nagent = ReActAgent(\n    name=\"Friday\",\n    sys_prompt=\"你是一个有用的助手。\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n    ),\n    formatter=DashScopeChatFormatter(),\n    plan_notebook=PlanNotebook(),\n)\n\n\n# %%\n# 之后，我们可以构建一个循环来与智能体交互，如下所示。\n# 一旦用户的任务复杂比较复杂，智能体将自己创建计划并逐步执行计划。\n#\n# .. code-block:: python\n#     :caption: 与计划智能体建立对话\n#\n#     async def interact_with_agent() -> None:\n#         \"\"\"与计划智能体交互。\"\"\"\n#         user = UserAgent(name=\"user\")\n#\n#         msg = None\n#         while True:\n#             msg = await user(msg)\n#             if msg.get_text_content() == \"exit\":\n#                 break\n#             msg = await agent(msg)\n#\n#     asyncio.run(interact_with_agent())\n#\n# 可视化和监控\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope 通过钩子函数支持计划执行的实时可视化和监控。\n#\n# 当前计划被工具函数改变时，钩子函数将被触发，开发者可以在这些钩子函数中将当前的计划转发到对应的前端进行可视化或其他处理。\n# 计划变化钩子函数的模板如下：\n#\n\n\ndef plan_change_hook_template(self: PlanNotebook, plan: Plan) -> None:\n    \"\"\"计划变化钩子函数的模板。\n\n    Args:\n        self (`PlanNotebook`):\n            PlanNotebook实例。\n        plan (`Plan`):\n            当前计划实例（变化后）。\n    \"\"\"\n    # 将计划转发到前端进行可视化或其他处理\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_prompt.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _prompt:\n\n提示词格式化\n=========================\n\nAgentScope 中的格式化器（formatter）模块负责\n\n- 将 Msg 对象转换为不同 LLM API 要求的格式，\n- （可选）截断消息以适应 max_token 的限制，\n- （可选）执行提示工程，例如对长对话进行总结。\n\n后两个功能是可选的，开发者也可以选择在记忆（memory）或智能体（agent）层面进行处理和实现。\n\n在 AgentScope 中，有两种类型的格式化器：\"ChatFormatter\" 和 \"MultiAgentFormatter\"，它们根据输入消息中的“身份实体”进行区分。\n\n- **ChatFormatter**：专为标准的用户-助手场景（聊天机器人）设计，使用 ``role`` 字段来识别用户和助手。\n- **MultiAgentFormatter**：专为多智能体场景设计，使用 ``name`` 字段来识别不同的实体，在格式化的过程中会将多智能体的对话历史合并为单个消息。\n\nAgentScope 内置的格式化器如下所列\n\n.. list-table:: AgentScope 中的内置格式化器\n    :header-rows: 1\n\n    * - API 提供商\n      - 用户-助手场景\n      - 多智能体场景\n    * - OpenAI\n      - ``OpenAIChatFormatter``\n      - ``OpenAIMultiAgentFormatter``\n    * - Anthropic\n      - ``AnthropicChatFormatter``\n      - ``AnthropicMultiAgentFormatter``\n    * - DashScope\n      - ``DashScopeChatFormatter``\n      - ``DashScopeMultiAgentFormatter``\n    * - Gemini\n      - ``GeminiChatFormatter``\n      - ``GeminiChatFormatter``\n    * - Ollama\n      - ``OllamaChatFormatter``\n      - ``OllamaMultiAgentFormatter``\n    * - DeepSeek\n      - ``DeepSeekChatFormatter``\n      - ``DeepSeekMultiAgentFormatter``\n    * - vLLM\n      - ``OpenAIFormatter``\n      - ``OpenAIFormatter``\n\n.. tip:: OpenAI API 支持 `name` 字段，因此 `OpenAIFormatter` 也可以用于多智能体场景。也可以使用 `OpenAIMultiAgentFormatter` 代替，它会将对话历史合并为单个用户消息。\n\n此外，内置格式化器对于不同的消息块（message blocks）的支持情况如下表所示：\n\n.. list-table:: 内置格式化器中支持的消息块\n    :header-rows: 1\n\n    * - 格式化器\n      - tool_use/result\n      - image\n      - audio\n      - video\n      - thinking\n    * - ``OpenAIChatFormatter``\n      - ✅\n      - ✅\n      - ✅\n      - ❌\n      -\n    * - ``DashScopeChatFormatter``\n      - ✅\n      - ✅\n      - ✅\n      - ❌\n      -\n    * - ``DashScopeMultiAgentFormatter``\n      - ✅\n      - ✅\n      - ✅\n      - ❌\n      -\n    * - ``AnthropicChatFormatter``\n      - ✅\n      - ✅\n      - ❌\n      - ❌\n      - ✅\n    * - ``AnthropicMultiAgentFormatter``\n      - ✅\n      - ✅\n      - ❌\n      - ❌\n      - ✅\n    * - ``GeminiChatFormatter``\n      - ✅\n      - ✅\n      - ✅\n      - ✅\n      -\n    * - ``GeminiMultiAgentFormatter``\n      - ✅\n      - ✅\n      - ✅\n      - ✅\n      -\n    * - ``OllamaChatFormatter``\n      - ✅\n      - ✅\n      - ❌\n      - ❌\n      -\n    * - ``OllamaMultiAgentFormatter``\n      - ✅\n      - ✅\n      - ❌\n      - ❌\n      -\n    * - ``DeepSeekChatFormatter``\n      - ✅\n      - ❌\n      - ❌\n      - ❌\n      -\n    * - ``DeepSeekMultiAgentFormatter``\n      - ✅\n      - ❌\n      - ❌\n      - ❌\n      -\n\n.. note:: 如 `官方文档 <https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#preserving-thinking-blocks>`_ 所述，只有 Anthropic 建议在输入的提示词中保留推理的部分（thinking blocks）。对于其它格式化器，我们忽略输入消息中包含的 ``ThinkingBlock``。\n\n面向 ReAct 的格式化\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n内置的 formatter 均面向 ReAct 智能体进行设计，其中输入消息由交替的 **对话历史** 和 **工具调用序列** 组成。\n\n在用户-助手场景中，对话历史是用户和助手的消息，我们直接将它们转换为所期望的格式。\n然而，在多智能体场景中，对话历史是来自不同智能体的消息列表，如下所示：\n\n.. figure:: ../../_static/images/multiagent_msgs.png\n    :alt: 多智能体消息示例\n    :width: 85%\n    :align: center\n\n    *多智能体消息示例*\n\n\n因此，我们必须将对话历史合并为带有标签 \"<history>\" 和 \"</history>\" 的单个用户消息。\n以 DashScope 为例，格式化后的消息将如下所示：\n\"\"\"\n\nfrom agentscope.token import HuggingFaceTokenCounter\nfrom agentscope.formatter import DashScopeMultiAgentFormatter\nfrom agentscope.message import Msg, ToolResultBlock, ToolUseBlock, TextBlock\nimport asyncio, json\n\n\ninput_msgs = [\n    # 系统提示\n    Msg(\"system\", \"你是一个名为 Friday 的有用助手\", \"system\"),\n    # 对话历史\n    Msg(\"Bob\", \"你好，Alice，你知道最近的图书馆在哪里吗？\", \"assistant\"),\n    Msg(\n        \"Alice\",\n        \"抱歉，我不知道。Charlie，你有什么想法吗？\",\n        \"assistant\",\n    ),\n    Msg(\n        \"Charlie\",\n        \"没有，我们问问 Friday 吧。Friday，帮我找到最近的图书馆。\",\n        \"assistant\",\n    ),\n    # 工具序列\n    Msg(\n        \"Friday\",\n        [\n            ToolUseBlock(\n                type=\"tool_use\",\n                name=\"get_current_location\",\n                id=\"1\",\n                input={},\n            ),\n        ],\n        \"assistant\",\n    ),\n    Msg(\n        \"system\",\n        [\n            ToolResultBlock(\n                type=\"tool_result\",\n                name=\"get_current_location\",\n                id=\"1\",\n                output=[TextBlock(type=\"text\", text=\"104.48, 36.30\")],\n            ),\n        ],\n        \"system\",\n    ),\n    Msg(\n        \"Friday\",\n        [\n            ToolUseBlock(\n                type=\"tool_use\",\n                name=\"search_around\",\n                id=\"2\",\n                input={\"location\": [104.48, 36.30], \"keyword\": \"library\"},\n            ),\n        ],\n        \"assistant\",\n    ),\n    Msg(\n        \"system\",\n        [\n            ToolResultBlock(\n                type=\"tool_result\",\n                name=\"search_around\",\n                id=\"2\",\n                output=[TextBlock(type=\"text\", text=\"[...]\")],\n            ),\n        ],\n        \"system\",\n    ),\n    # 对话历史继续\n    Msg(\"Friday\", \"最近的图书馆是...\", \"assistant\"),\n    Msg(\"Bob\", \"谢谢，Friday！\", \"assistant\"),\n    Msg(\"Alice\", \"我们一起去吧。\", \"assistant\"),\n]\n\n\nasync def run_formatter_example() -> list[dict]:\n    \"\"\"多智能体消息格式化示例。\"\"\"\n    formatter = DashScopeMultiAgentFormatter()\n    formatted_message = await formatter.format(input_msgs)\n    print(\"格式化后的消息：\")\n    print(json.dumps(formatted_message, indent=4, ensure_ascii=False))\n    return formatted_message\n\n\nformatted_message = asyncio.run(run_formatter_example())\n\n# %%\n# 具体来说，对话历史被格式化为：\n#\nprint(\"第一段对话历史：\")\nprint(formatted_message[1][\"content\"])\n\nprint(\"\\n第二段对话历史：\")\nprint(formatted_message[-1][\"content\"])\n\n# %%\n# 基于截断的格式化\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# 通过 AgentScope 中的 token 模块，内置格式化器支持通过 **删除最旧的消息**（除了系统提示消息）在 token 超过限制时截断输入消息。\n#\n# 以 OpenAIFormatter 为例，我们首先计算输入消息的总 token 数。\n#\n\n\nasync def run_token_counter() -> int:\n    \"\"\"计算输入消息的 token 数量。\"\"\"\n    # 我们使用 huggingface token 计数器用于 dashscope 模型。\n    token_counter = HuggingFaceTokenCounter(\n        \"Qwen/Qwen2.5-VL-3B-Instruct\",\n        use_mirror=False,\n    )\n\n    return await token_counter.count(formatted_message)\n\n\n# %%\n# 然后我们将最大 token 限制设置为比总 token 数少 20 个，并运行格式化器。\n#\n\n\nasync def run_truncated_formatter() -> None:\n    \"\"\"带截断的消息格式化示例。\"\"\"\n    token_counter = HuggingFaceTokenCounter(\n        pretrained_model_name_or_path=\"Qwen/Qwen2.5-VL-3B-Instruct\",\n        use_mirror=False,\n    )\n    formatter = DashScopeMultiAgentFormatter(\n        token_counter=token_counter,\n        max_tokens=n_tokens - 20,\n    )\n    truncated_formatted_message = await formatter.format(input_msgs)\n    n_truncated_tokens = await token_counter.count(truncated_formatted_message)\n    print(\"截断后的 token 数量：\", n_truncated_tokens)\n\n    print(\"\\n截断后的对话历史：\")\n    print(truncated_formatted_message[1][\"content\"])\n\n\n# %%\n# 我们可以看到来自 Bob 和 Alice 的前两条消息被删除以适应 ``max_tokens`` 的限制。\n#\n#\n# 自定义格式化器\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope 提供了两个基类 ``FormatterBase`` 和其子类 ``TruncatedFormatterBase``。\n# 其中 ``TruncatedFormatterBase`` 类提供了 FIFO（First In First Out）截断策略，所有内置格式化器都继承自它。\n#\n# .. list-table:: AgentScope 中格式化器的基类\n#   :header-rows: 1\n#\n#   * - 类\n#     - 抽象方法\n#     - 描述\n#   * - ``FormatterBase``\n#     - ``format``\n#     - 将输入的 ``Msg`` 对象格式化为目标 API 所期望的格式\n#   * - ``TruncatedFormatterBase``\n#     - ``_format_agent_message``\n#     - 格式化智能体消息，在多智能体场景中可能包含多个身份\n#   * -\n#     - ``_format_tool_sequence``\n#     - 将工具使用和结果序列格式化为所期望的格式\n#   * -\n#     - ``_format`` (可选)\n#     - 将输入的 ``Msg`` 对象格式化为目标 API 所期望的格式\n#\n# .. tip:: ``TruncatedFormatterBase`` 中的 ``_format`` 将输入消息分组为智能体消息和工具序列，然后分别通过调用 ``_format_agent_message`` 和 ``_format_tool_sequence`` 来格式化它们。开发者可以重写两个函数来实现自己的格式化策略。\n#\n# .. tip:: 可选地，开发者可以重写 ``TruncatedFormatterBase`` 中的 ``_truncate`` 方法来实现自己的截断策略。\n#\n# 进一步阅读\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# - :ref:`token`\n# - :ref:`model`\n#\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_rag.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _rag:\n\nRAG\n==========================\n\nAgentScope 提供了内置的 RAG（Retrieval-Augmented Generation) 实现。本节将详细介绍\n\n- 如何使用 AgentScope 中的 RAG 模块，\n- 如何实现 **多模态** RAG，\n- 如何在 ``ReActAgent`` 中以两种不同的方式集成 RAG 模块：\n    - 智能体自主控制（Agentic manner）\n    - 通用方式（Generic manner）\n\n我们在下列表格中总结了两种模式的优缺点：\n\n.. list-table:: RAG 集成方式比较\n    :header-rows: 1\n\n    * - 集成方式\n      - 描述\n      - 优点\n      - 缺点\n    * - 智能体自主控制\n      - 以工具调用方式让智能体自主决定何时进行查询，查询什么关键字\n      - - 与 ReAct 算法契合，灵活性高\n        - 智能体能够根据当前的上下文改写查询关键词\n        - 避免在不必要时发生查询\n      - 对 LLM 模型能力要求较高\n    * - 通用方式\n      - 每次在 ``reply`` 函数开始时固定进行查询，并将检索结果整合到提示（prompt）中\n      - - 实现简单\n        - 对 LLM 模型能力要求低\n      - - 每次都会运行查询，因此会引入过多不必要的查询检索\n        - 查询数据库较大时，回复延迟较高\n\n.. note:: 作为开源框架，AgentScope 的目标是让开发过程更简单也更有趣。因此，AgentScope 的设计中并不强制要求使用内置的 RAG 实现，同时支持、鼓励开发者集成现有的 RAG 实现或第三方 RAG 框架。\n\n\"\"\"\nimport asyncio\nimport json\nimport os\n\nfrom matplotlib import pyplot as plt\n\nimport agentscope\nfrom agentscope.agent import ReActAgent\nfrom agentscope.embedding import (\n    DashScopeTextEmbedding,\n    DashScopeMultiModalEmbedding,\n)\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.rag import (\n    TextReader,\n    SimpleKnowledge,\n    QdrantStore,\n    Document,\n    ImageReader,\n)\nfrom agentscope.tool import Toolkit\n\n# %%\n# 使用 RAG 模块\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope 中的 RAG 模块由以下三个核心组件构成：\n#\n# - **Reader**：负责从数据源读取数据，并进行分块（chunking）\n# - **Knowledge**：负责知识库查询检索和数据存储逻辑的算法实现\n# - **Store**：负责与向量数据库交互的逻辑实现\n#\n# .. note:: 我们将持续在 AgentScope 中集成新的向量数据库和数据读取模块。详情请查看我们的 `开发路线图 <https://github.com/orgs/agentscope-ai/projects/2>`_，同时也欢迎贡献代码！\n#\n# 当前 AgentScope 中内置支持的 reader 包括：\n#\n\nfor _ in agentscope.rag.__all__:\n    if _.endswith(\"Reader\"):\n        print(f\"- {_}\")\n\n# %%\n# 这些 reader 的作用是读取数据，将数据分块并包装成 ``agentscope.rag.Document`` 对象。 ``Document`` 对象包含以下字段：\n#\n# - ``metadata``：数据块的元信息，包含数据内容（``content``）、数据 ID（``doc_id``）、块 ID（``chunk_id``）和总块数（``total_chunks``）\n# - ``embedding``: 数据块的向量表示，默认为 ``None``，在将数据块添加到知识库时会被填充\n# - ``score``: 数据块的相关性分数，默认为 ``None``，在从知识库检索数据块时会被填充\n#\n# 以 ``TextReader`` 为例，通过如下代码读取文本字符串，并将文本分块为 ``Document`` 对象：\n#\n\n\nasync def example_text_reader(print_docs: bool) -> list[Document]:\n    \"\"\"使用 TextReader 读取文本字符串，并将文本分块为 Document 对象。\"\"\"\n    # 创建 TextReader 对象\n    reader = TextReader(chunk_size=512, split_by=\"paragraph\")\n\n    # 读取文本字符串\n    documents = await reader(\n        text=(\n            # 我们准备一些文本数据用于演示 RAG 功能。\n            \"我的名字是李明，今年28岁。\\n\"\n            \"我居住在中国杭州，是一名算法工程师。我喜欢打篮球和玩游戏。\\n\"\n            \"我父亲的名字是李强，是一名医生，我的母亲是陈芳芳，是一名教师，她总是指导我学习。\\n\"\n            \"我现在在北京大学攻读博士学位，研究方向是人工智能。\\n\"\n            \"我最好的朋友是王伟，我们从小一起长大，现在他是一名律师。\"\n        ),\n    )\n\n    if print_docs:\n        print(f\"文本被分块为 {len(documents)} 个 Document 对象：\")\n        for idx, doc in enumerate(documents):\n            print(f\"Document {idx}:\")\n            print(\"\\tScore: \", doc.score)\n            print(\n                \"\\tMetadata: \",\n                json.dumps(doc.metadata, indent=2, ensure_ascii=False),\n                \"\\n\",\n            )\n\n    return documents\n\n\ndocs = asyncio.run(example_text_reader(print_docs=True))\n\n# %%\n# 由于并不存在一个 “one-for-all” 的数据读取和分块方法，特别像是 PDF 和 Word 这类复杂格式的文档。\n# 因此，AgentScope 鼓励开发者根据自己的数据格式实现自定义的 reader。\n# 只需要继承 ``BaseReader`` 类，并实现 ``__call__`` 方法即可。\n#\n# 在数据分块后，接下来需要将数据块添加到知识库中。\n# 在 AgentScope 中，知识库的初始化需要提供 **嵌入模型** 和 **向量存储** （即向量数据库） 的对象。\n# AgentScope 目前内置支持基于 `Qdrant <https://qdrant.tech/>`_ 实现的向量存储，以及一个知识库的基础实现 ``SimpleKnowledge``。\n# 具体使用方式如下：\n#\n# .. note::\n#\n#  - 我们正在 AgentScope 中集成新的向量数据库，详情请查看我们的 `开发路线图 <https://github.com/orgs/agentscope-ai/projects/2>`_。欢迎贡献代码！\n#  - Qdrant 的实现通过 ``location`` 参数支持多种不同的部署方式，包括内存模式，本地模式和云端模式。详情请参考 `Qdrant 文档 <https://qdrant.tech/>`_。\n#\n\n\nasync def build_knowledge_base() -> SimpleKnowledge:\n    \"\"\"构建知识库。\"\"\"\n    # 读取 documents 数据\n    documents = await example_text_reader(print_docs=False)\n\n    # 创建一个内存中的 Qdrant 向量存储，以及使用 DashScopeTextEmbedding 作为嵌入模型，初始化知识库\n    knowledge = SimpleKnowledge(\n        # 提供一个 embedding 模型用于将文本转换为向量\n        embedding_model=DashScopeTextEmbedding(\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            model_name=\"text-embedding-v4\",\n            dimensions=1024,\n        ),\n        # 选择 Qdrant 作为向量存储\n        embedding_store=QdrantStore(\n            location=\":memory:\",  # 使用内存模式\n            collection_name=\"test_collection\",\n            dimensions=1024,  # 嵌入向量的维度必须与嵌入模型输出的维度一致\n        ),\n    )\n\n    # 将 documents 添加到知识库中\n    await knowledge.add_documents(documents)\n\n    # 从知识库中检索数据\n    docs = await knowledge.retrieve(\n        query=\"李明的父亲是谁？\",\n        limit=3,\n        score_threshold=0.5,\n    )\n\n    print(\"检索到的 Document 对象：\")\n    for doc in docs:\n        print(doc, \"\\n\")\n\n    return knowledge\n\n\nknowledge = asyncio.run(build_knowledge_base())\n\n# %%\n# AgentScope 中的知识库类提供两个核心方法：``add_documents`` 和 ``retrieve``，分别用于添加数据块和搜索检索数据块。\n#\n# 此外，AgentScope 提供了 ``retrieve_knowledge`` 方法，它将 ``retrieve`` 方法封装成一个智能体能够直接调用的工具函数。开发者可以直接使用该工具函数装备智能体。\n#\n# 自定义 RAG 组件\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope 支持并鼓励开发者自定义 RAG 组件，包括 reader、知识库和向量数据库。\n# 具体来说，我们提供了以下基类用于自定义：\n#\n# .. list-table:: RAG 基类\n#     :header-rows: 1\n#\n#     * - 基类\n#       - 描述\n#       - 抽象方法\n#     * - ``ReaderBase``\n#       - 所有 reader 的基类\n#       - ``__call__``\n#     * - ``VDBStoreBase``\n#       - 向量数据库的基类\n#       - | ``add``\n#         | ``search``\n#         | ``get_client`` (可选)\n#         | ``delete`` (可选)\n#     * - ``KnowledgeBase``\n#       - 知识库的基类\n#       - | ``retrieve``\n#         | ``add_documents``\n#\n# ``VDBStoreBase`` 中的 ``get_client`` 方法允许开发者访问底层向量数据库的完整功能。\n# 这样，他们就可以基于向量数据库实现更高级的功能，例如建立索引、高级搜索等。\n#\n# 与 ReActAgent 集成\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# 接下来，我们将演示如何以智能体自主控制（agentic）和通用（generic）两种方式将 RAG 模块与 AgentScope 中的 ``ReActAgent`` 集成。\n#\n# 智能体自主控制\n# --------------------------------\n# 在智能体自主控制的方式中，ReAct 智能体可以自主决定何时检索知识以及检索的查询内容。\n# 将 RAG 模块与 AgentScope 中的 ``ReActAgent`` 集成非常简单，只需将知识库的 ``retrieve_knowledge`` 方法注册为工具，并为该工具提供适当的描述即可。\n\n\nasync def example_agentic_manner() -> None:\n    \"\"\"以智能体自主控制方式将 RAG 模块与 ReActAgent 集成的示例。\"\"\"\n    # 创建一个 ReAct 智能体\n    toolkit = Toolkit()\n\n    # 使用 DashScope 作为模型创建 ReAct 智能体\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"You're a helpful assistant named Friday.\",\n        model=DashScopeChatModel(\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            model_name=\"qwen-max\",\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n    )\n\n    print(\"第一次回复: \")\n    # 第一次我们进行一些交流，提供“李明”这个名字作为上下文内容\n    await agent(\n        Msg(\n            \"user\",\n            \"李明是我最好的朋友。\",\n            \"user\",\n        ),\n    )\n\n    # 将 retrieve_knowledge 方法注册为工具箱中的工具函数\n    toolkit.register_tool_function(\n        knowledge.retrieve_knowledge,\n        func_description=(  # 为工具提供清晰的描述\n            \"用于检索与给定查询相关的文档的工具。\" \"当你需要查找有关李明的信息时使用此工具。\"\n        ),\n    )\n\n    print(\"\\n\\n第二次回复: \")\n    # 第二次回复中，我们希望智能体能够将查询中“他父亲”改写得更具体，例如\n    # “李明的父亲是谁？”或“李明的父亲”\n    await agent(\n        Msg(\n            \"user\",\n            \"你知道他父亲是谁吗？\",\n            \"user\",\n        ),\n    )\n\n\nasyncio.run(example_agentic_manner())\n\n# %%\n# 在上面的例子中，我们模拟了正常与智能体交流过程。第一次的交流我们提供了“李明”的名字作为上下文内容。\n# 第二次提问时，我们的问题是“你知道他父亲是谁吗？”，\n# 我们希望智能体能够利用上下文历史信息改写查询，使其更具体，更好的进行检索，例如改写为“李明的父亲是谁？”或“李明的父亲”。\n#\n# 更进一步，结合 :ref:`plan` 模块，我们可以让智能体实现更加复杂的查询改写和多轮检索。\n#\n# 通用方式\n# --------------------------------\n# ``ReActAgent`` 还以一种更加通用的方式集成了 RAG 模块，\n# 它在每次 ``reply`` 函数开始执行时检索知识，并将检索到的知识附加到用户消息的提示中。\n#\n# 只需设置 ``ReActAgent`` 的 ``knowledge`` 参数，智能体就会在每次回复开始时自动检索知识。\n#\n\n\nasync def example_generic_manner() -> None:\n    \"\"\"以通用方式将 RAG 模块与 ReActAgent 集成的示例。\"\"\"\n    # 创建一个 ReAct 智能体\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"You're a helpful assistant named Friday.\",\n        model=DashScopeChatModel(\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            model_name=\"qwen-max\",\n        ),\n        formatter=DashScopeChatFormatter(),\n        # 将知识库传递给智能体\n        knowledge=knowledge,\n    )\n\n    await agent(\n        Msg(\n            \"user\",\n            \"你知道李明的父亲是谁吗？\",\n            \"user\",\n        ),\n    )\n\n    print(\"\\n查看智能体记忆中检索信息如何插入：\")\n    content = (await agent.memory.get_memory())[1].content\n    print(json.dumps(content, indent=2, ensure_ascii=False))\n\n\nasyncio.run(example_generic_manner())\n\n\n# %%\n# 多模态 RAG\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope 中的 RAG 模块原生支持多模态，因为\n#\n# - AgentScope 支持多模态嵌入 API，例如 ``DashScopeMultimodalEmbedding``。\n# - ``Document`` 类的 ``metadata`` 中，``content`` 字段的类型是 ``TextBlock | ImageBlock | VideoBlock``，因此可以存储文本、图片和视频等多模态数据。\n#\n# 因此，我们可以直接使用多模态 reader 和嵌入模型来构建多模态知识库，如下所示。\n#\n# 首先，我们准备一张本地的图片，这张图片上包含了文本“My name is Ming Li”。\n\n# 准备一张包含文本“My name is Ming Li”的图片。\npath_image = \"./example.png\"\nplt.figure(figsize=(8, 3))\nplt.text(0.5, 0.5, \"My name is Ming Li\", ha=\"center\", va=\"center\", fontsize=30)\nplt.axis(\"off\")\nplt.savefig(path_image, bbox_inches=\"tight\", pad_inches=0.1)\nplt.close()\n\n# %%\n# 然后我们可以构建一个多模态知识库，构建过程与纯文本知识库类似。只是将 reader 和嵌入模型替换为多模态版本即可。\n# 在下面的例子中，我们使用了 ``ImageReader`` 和 ``DashScopeMultiModalEmbedding``。\n# 同时，这里我们使用多模态模型 ``qwen3-vl-plus`` 作为智能体的语言模型。\n#\n\n\nasync def example_multimodal_rag() -> None:\n    \"\"\"使用多模态 RAG 的示例。\"\"\"\n    # 使用 ImageReader 读取图片\n    reader = ImageReader()\n    docs = await reader(image_url=path_image)\n\n    # 创建一个知识库\n    knowledge = SimpleKnowledge(\n        embedding_model=DashScopeMultiModalEmbedding(\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            model_name=\"multimodal-embedding-v1\",\n            dimensions=1024,\n        ),\n        embedding_store=QdrantStore(\n            location=\":memory:\",\n            collection_name=\"test_collection\",\n            dimensions=1024,\n        ),\n    )\n\n    await knowledge.add_documents(docs)\n\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"You're a helpful assistant named Friday.\",\n        model=DashScopeChatModel(\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            model_name=\"qwen3-vl-plus\",\n        ),\n        formatter=DashScopeChatFormatter(),\n        knowledge=knowledge,\n    )\n\n    await agent(\n        Msg(\n            \"user\",\n            \"你知道我的名字吗？\",\n            \"user\",\n        ),\n    )\n\n    # 让我们看看检索到的图片数据是否已经加入了智能体的记忆中\n    print(\"\\n查看智能体记忆中检索信息如何插入：\")\n    content = (await agent.memory.get_memory())[1].content\n    print(json.dumps(content, indent=2, ensure_ascii=False))\n\n\nasyncio.run(example_multimodal_rag())\n\n# %%\n# 进一步阅读\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# - :ref:`embedding`\n# - :ref:`plan`\n#\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_realtime.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _realtime:\n\n实时智能体\n====================\n\n**实时智能体（Realtime Agent）** 用于处理实时交互场景，例如语音对话或实时聊天会话。\nAgentScope 中的实时智能体具有以下特性：\n\n- 集成 OpenAI、DashScope、Gemini 等实时模型 API\n- 统一的事件接口，简化与不同实时模型的交互\n- 支持工具调用能力\n- 支持多智能体交互\n\n.. note:: 实时智能体目前处于活跃开发阶段，欢迎社区贡献、讨论和反馈！如果开发者对实时智能体感兴趣，欢迎加入讨论和开发。\n\n\"\"\"\n\nimport asyncio\nimport os\nfrom agentscope.agent import RealtimeAgent\nfrom agentscope.realtime import (\n    DashScopeRealtimeModel,\n    OpenAIRealtimeModel,\n    GeminiRealtimeModel,\n)\n\n# %%\n# 创建实时模型\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# AgentScope 目前支持以下实时模型 API：\n#\n# .. list-table::\n#    :header-rows: 1\n#    :widths: 15 25 25 15 20\n#\n#    * - 提供商\n#      - 类名\n#      - 支持的模型\n#      - 输入模态\n#      - 工具支持\n#    * - DashScope\n#      - ``DashScopeRealtimeModel``\n#      - ``qwen3-omni-flash-realtime``\n#      - 文本、音频、图像\n#      - 否\n#    * - OpenAI\n#      - ``OpenAIRealtimeModel``\n#      - ``gpt-4o-realtime-preview``\n#      - 文本、音频\n#      - 是\n#    * - Gemini\n#      - ``GeminiRealtimeModel``\n#      - ``gemini-2.5-flash-native-audio-preview-09-2025``\n#      - 文本、音频、图像\n#      - 是\n#\n#\n# 以下是初始化不同实时模型的示例：\n#\n# .. code-block:: python\n#     :caption: 初始化不同实时模型的示例\n#\n#     # DashScope 实时模型\n#     dashscope_model = DashScopeRealtimeModel(\n#         model_name=\"qwen3-omni-flash-realtime\",\n#         api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n#         voice=\"Cherry\",  # 可选项: \"Cherry\", \"Serena\", \"Ethan\", \"Chelsie\"\n#         enable_input_audio_transcription=True,\n#     )\n#\n#     # OpenAI 实时模型\n#     openai_model = OpenAIRealtimeModel(\n#         model_name=\"gpt-4o-realtime-preview\",\n#         api_key=os.getenv(\"OPENAI_API_KEY\"),\n#         voice=\"alloy\",  # 可选项: \"alloy\", \"echo\", \"marin\", \"cedar\"\n#         enable_input_audio_transcription=True,\n#     )\n#\n#     # Gemini 实时模型\n#     gemini_model = GeminiRealtimeModel(\n#         model_name=\"gemini-2.5-flash-native-audio-preview-09-2025\",\n#         api_key=os.getenv(\"GEMINI_API_KEY\"),\n#         voice=\"Puck\",  # 可选项: \"Puck\", \"Charon\", \"Kore\", \"Fenrir\"\n#         enable_input_audio_transcription=True,\n#     )\n#\n#\n#\n# 实时模型提供以下核心方法：\n#\n# .. list-table::\n#    :header-rows: 1\n#    :widths: 30 70\n#\n#    * - 方法\n#      - 描述\n#    * - ``connect(outgoing_queue, instructions, tools)``\n#      - 建立与实时模型 API 的 WebSocket 连接\n#    * - ``disconnect()``\n#      - 关闭 WebSocket 连接\n#    * - ``send(data)``\n#      - 向实时模型发送音频/文本/图像数据进行处理\n#\n# ``connect()`` 方法中的 ``outgoing_queue`` 参数是一个 asyncio 队列，\n# 用于将实时模型的事件转发到外部（例如智能体或前端）。\n#\n#\n# 模型事件接口\n# -----------------------\n#\n# AgentScope 提供统一的 ``agentscope.realtime.ModelEvents`` 接口，\n# 简化与不同实时模型的交互。支持以下事件：\n#\n# .. note:: ModelEvents 中的 \"session\" 指的是实时模型与模型 API 之间的\n#     WebSocket 连接会话，而非前端与后端之间的会话。\n#\n# .. list-table::\n#    :header-rows: 1\n#    :widths: 40 60\n#\n#    * - 事件\n#      - 描述\n#    * - ``ModelEvents.ModelSessionCreatedEvent``\n#      - 会话创建成功\n#    * - ``ModelEvents.ModelSessionEndedEvent``\n#      - 会话已结束\n#    * - ``ModelEvents.ModelResponseCreatedEvent``\n#      - 模型开始生成响应\n#    * - ``ModelEvents.ModelResponseDoneEvent``\n#      - 模型完成响应生成\n#    * - ``ModelEvents.ModelResponseAudioDeltaEvent``\n#      - 流式音频数据块\n#    * - ``ModelEvents.ModelResponseAudioDoneEvent``\n#      - 音频响应完成\n#    * - ``ModelEvents.ModelResponseAudioTranscriptDeltaEvent``\n#      - 流式音频转录文本块\n#    * - ``ModelEvents.ModelResponseAudioTranscriptDoneEvent``\n#      - 音频转录完成\n#    * - ``ModelEvents.ModelResponseToolUseDeltaEvent``\n#      - 流式工具调用参数\n#    * - ``ModelEvents.ModelResponseToolUseDoneEvent``\n#      - 工具调用参数完成\n#    * - ``ModelEvents.ModelInputTranscriptionDeltaEvent``\n#      - 流式用户输入转录文本块\n#    * - ``ModelEvents.ModelInputTranscriptionDoneEvent``\n#      - 用户输入转录完成\n#    * - ``ModelEvents.ModelInputStartedEvent``\n#      - 检测到用户音频输入开始（VAD）\n#    * - ``ModelEvents.ModelInputDoneEvent``\n#      - 检测到用户音频输入结束（VAD）\n#    * - ``ModelEvents.ModelErrorEvent``\n#      - 发生错误\n#\n#\n#\n# 创建实时智能体\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# ``RealtimeAgent`` 作为桥接层，负责：\n#\n# - 将实时模型的 ``ModelEvents`` 转换为 ``ServerEvents``，发送给前端和其他智能体\n# - 接收来自前端或其他智能体的 ``ClientEvents``，并转发给实时模型 API\n# - 管理智能体的生命周期和事件队列\n#\n# 服务端和客户端事件\n# -------------------------\n#\n# AgentScope 提供统一的 ``ServerEvents`` 和 ``ClientEvents``，\n# 用于后端与前端之间的通信：\n#\n# **ServerEvents**（后端 → 前端）：\n#\n# .. list-table::\n#    :header-rows: 1\n#    :widths: 40 60\n#\n#    * - 事件\n#      - 描述\n#    * - ``ServerEvents.ServerSessionCreatedEvent``\n#      - 后端创建会话\n#    * - ``ServerEvents.ServerSessionUpdatedEvent``\n#      - 后端更新会话\n#    * - ``ServerEvents.ServerSessionEndedEvent``\n#      - 后端结束会话\n#    * - ``ServerEvents.AgentReadyEvent``\n#      - 智能体准备接收输入\n#    * - ``ServerEvents.AgentEndedEvent``\n#      - 智能体已结束\n#    * - ``ServerEvents.AgentResponseCreatedEvent``\n#      - 智能体开始生成响应\n#    * - ``ServerEvents.AgentResponseDoneEvent``\n#      - 智能体完成响应生成\n#    * - ``ServerEvents.AgentResponseAudioDeltaEvent``\n#      - 智能体流式音频块\n#    * - ``ServerEvents.AgentResponseAudioDoneEvent``\n#      - 音频响应完成\n#    * - ``ServerEvents.AgentResponseAudioTranscriptDeltaEvent``\n#      - 智能体响应的流式转录\n#    * - ``ServerEvents.AgentResponseAudioTranscriptDoneEvent``\n#      - 转录完成\n#    * - ``ServerEvents.AgentResponseToolUseDeltaEvent``\n#      - 流式工具调用数据\n#    * - ``ServerEvents.AgentResponseToolUseDoneEvent``\n#      - 工具调用完成\n#    * - ``ServerEvents.AgentResponseToolResultEvent``\n#      - 工具执行结果\n#    * - ``ServerEvents.AgentInputTranscriptionDeltaEvent``\n#      - 用户输入的流式转录\n#    * - ``ServerEvents.AgentInputTranscriptionDoneEvent``\n#      - 输入转录完成\n#    * - ``ServerEvents.AgentInputStartedEvent``\n#      - 用户音频输入开始\n#    * - ``ServerEvents.AgentInputDoneEvent``\n#      - 用户音频输入结束\n#    * - ``ServerEvents.AgentErrorEvent``\n#      - 发生错误\n#\n# **ClientEvents**（前端 → 后端）：\n#\n# .. list-table::\n#    :header-rows: 1\n#    :widths: 40 60\n#\n#    * - 事件\n#      - 描述\n#    * - ``ClientEvents.ClientSessionCreateEvent``\n#      - 创建指定配置的新会话\n#    * - ``ClientEvents.ClientSessionEndEvent``\n#      - 结束当前会话\n#    * - ``ClientEvents.ClientResponseCreateEvent``\n#      - 请求智能体立即生成响应\n#    * - ``ClientEvents.ClientResponseCancelEvent``\n#      - 中断智能体的当前响应\n#    * - ``ClientEvents.ClientTextAppendEvent``\n#      - 追加文本输入\n#    * - ``ClientEvents.ClientAudioAppendEvent``\n#      - 追加音频输入\n#    * - ``ClientEvents.ClientAudioCommitEvent``\n#      - 提交音频输入（标志输入结束）\n#    * - ``ClientEvents.ClientImageAppendEvent``\n#      - 追加图像输入\n#    * - ``ClientEvents.ClientToolResultEvent``\n#      - 发送工具执行结果\n#\n# 初始化实时智能体\n# ------------------------------\n#\n# 以下是创建和使用实时智能体的示例：\n\n\nasync def example_realtime_agent() -> None:\n    \"\"\"创建和使用实时智能体的示例。\"\"\"\n    agent = RealtimeAgent(\n        name=\"Friday\",\n        sys_prompt=\"你是一个名为 Friday 的助手。\",\n        model=DashScopeRealtimeModel(\n            model_name=\"qwen3-omni-flash-realtime\",\n            api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n        ),\n    )\n\n    # 创建队列接收来自智能体的消息\n    outgoing_queue = asyncio.Queue()\n\n    # 智能体现在已准备好处理输入\n    # 在独立任务中处理输出消息\n    async def handle_agent_messages():\n        while True:\n            event = await outgoing_queue.get()\n            # 处理事件（例如通过 WebSocket 发送到前端）\n            print(f\"智能体事件: {event.type}\")\n\n    # 启动消息处理任务\n    asyncio.create_task(handle_agent_messages())\n\n    # 启动智能体（建立连接）\n    await agent.start(outgoing_queue)\n\n    # 完成后停止智能体\n    await agent.stop()\n\n\n# %%\n# 启动实时对话\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# 下面演示如何在用户和实时智能体之间建立实时对话。\n#\n# 这里以 FastAPI 为例，展示如何搭建实时对话的后端框架。\n#\n# **后端设置（服务端）：**\n#\n# 后端需要：\n#\n# 1. 创建 WebSocket 端点接受前端连接\n# 2. 在会话开始时创建 ``RealtimeAgent``\n# 3. 将前端的 ``ClientEvents`` 转发给智能体\n# 4. 将智能体的 ``ServerEvents`` 转发给前端\n#\n# .. code-block:: python\n#\n#     from fastapi import FastAPI, WebSocket\n#     from agentscope.agent import RealtimeAgent\n#     from agentscope.realtime import (\n#         DashScopeRealtimeModel,\n#         ClientEvents,\n#         ServerEvents,\n#     )\n#\n#     app = FastAPI()\n#\n#     @app.websocket(\"/ws/{user_id}/{session_id}\")\n#     async def websocket_endpoint(\n#         websocket: WebSocket,\n#         user_id: str,\n#         session_id: str,\n#     ):\n#         await websocket.accept()\n#\n#         # 创建智能体消息队列\n#         frontend_queue = asyncio.Queue()\n#\n#         # 创建智能体\n#         agent = RealtimeAgent(\n#             name=\"Assistant\",\n#             sys_prompt=\"你是一个有用的助手。\",\n#             model=DashScopeRealtimeModel(\n#                 model_name=\"qwen3-omni-flash-realtime\",\n#                 api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n#             ),\n#         )\n#\n#         # 启动智能体\n#         await agent.start(frontend_queue)\n#\n#         # 将智能体消息转发到前端\n#         async def send_to_frontend():\n#             while True:\n#                 msg = await frontend_queue.get()\n#                 await websocket.send_json(msg.model_dump())\n#\n#         asyncio.create_task(send_to_frontend())\n#\n#         # 接收前端消息并转发给智能体\n#         while True:\n#             data = await websocket.receive_json()\n#             client_event = ClientEvents.from_json(data)\n#             await agent.handle_input(client_event)\n#\n# **前端设置（客户端）：**\n#\n# 前端需要：\n#\n# 1. 建立与后端的 WebSocket 连接\n# 2. 发送 ``CLIENT_SESSION_CREATE`` 事件初始化会话\n# 3. 捕获麦克风音频，通过 ``CLIENT_AUDIO_APPEND`` 事件发送\n# 4. 接收并处理 ``ServerEvents``（例如播放音频、显示转录文本）\n#\n# .. code-block:: javascript\n#\n#     // 连接 WebSocket\n#     const ws = new WebSocket('ws://localhost:8000/ws/user1/session1');\n#\n#     ws.onopen = () => {\n#         // 创建会话\n#         ws.send(JSON.stringify({\n#             type: 'client_session_create',\n#             config: {\n#                 instructions: '你是一个有用的助手。',\n#                 user_name: 'User1'\n#             }\n#         }));\n#     };\n#\n#     // 处理来自后端的消息\n#     ws.onmessage = (event) => {\n#         const data = JSON.parse(event.data);\n#         if (data.type === 'response_audio_delta') {\n#             // 播放音频块\n#             playAudio(data.delta);\n#         }\n#     };\n#\n#     // 发送音频数据\n#     function sendAudioChunk(audioData) {\n#         ws.send(JSON.stringify({\n#             type: 'client_audio_append',\n#             session_id: 'session1',\n#             audio: audioData,  // base64 编码\n#             format: { encoding: 'pcm16', sample_rate: 16000 }\n#         }));\n#     }\n#\n# 完整的工作示例请参见 AgentScope 仓库中的\n# ``examples/agent/realtime_voice_agent/``。\n\n# %%\n# 多智能体实时对话\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# AgentScope 通过 ``ChatRoom`` 类支持多智能体实时交互。\n#\n# 请注意目前大多数实时模型 API 仅支持单用户交互，但 AgentScope 的架构设计支持多智能体和多用户，\n# 当 API 能力扩展时即可应用到多智能体场景。\n#\n# 实时聊天室\n# ----------------------------\n#\n# AgentScope 引入 ``ChatRoom`` 类来管理共享对话空间中的多个实时智能体。\n# ChatRoom 提供：\n#\n# - 集中管理多个 ``RealtimeAgent`` 实例\n# - 智能体之间的自动消息广播\n# - 统一的前端通信消息队列\n# - 房间内所有智能体的生命周期管理\n#\n# 使用 ChatRoom\n# --------------\n#\n# ``ChatRoom`` 的用法与 ``RealtimeAgent`` 类似：\n#\n\n\nasync def example_chat_room() -> None:\n    \"\"\"使用 ChatRoom 和多个实时智能体的示例。\"\"\"\n    from agentscope.pipeline import ChatRoom\n    from agentscope.agent import RealtimeAgent\n    from agentscope.realtime import DashScopeRealtimeModel\n\n    # 创建多个智能体\n    agent1 = RealtimeAgent(\n        name=\"Agent1\",\n        sys_prompt=\"你是 Agent1，一个有用的助手。\",\n        model=DashScopeRealtimeModel(\n            model_name=\"qwen3-omni-flash-realtime\",\n            api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n        ),\n    )\n\n    agent2 = RealtimeAgent(\n        name=\"Agent2\",\n        sys_prompt=\"你是 Agent2，一个有用的助手。\",\n        model=DashScopeRealtimeModel(\n            model_name=\"qwen3-omni-flash-realtime\",\n            api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n        ),\n    )\n\n    # 创建包含多个智能体的聊天室\n    chat_room = ChatRoom(agents=[agent1, agent2])\n\n    # 创建队列接收来自所有智能体的消息\n    outgoing_queue = asyncio.Queue()\n\n    # 启动聊天室\n    await chat_room.start(outgoing_queue)\n\n    # 处理来自前端的输入\n    # 聊天室会广播给所有智能体\n    from agentscope.realtime import ClientEvents\n\n    client_event = ClientEvents.ClientTextAppendEvent(\n        session_id=\"session1\",\n        text=\"大家好！\",\n    )\n    await chat_room.handle_input(client_event)\n\n    # 完成后停止聊天室\n    await chat_room.stop()\n\n\n# %%\n# 发展规划\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# 实时智能体功能目前为实验性质，正在积极开发中。未来计划包括：\n#\n# - 支持更多实时模型 API\n# - 增强对话历史的记忆管理\n# - 多用户语音交互支持\n# - 改进 VAD（语音活动检测）配置\n# - 更好的错误处理和恢复机制\n#\n#\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_state.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _state:\n\n状态/会话管理\n=================================\n\n在 AgentScope 中，**\"状态\"** 是指智能体在运行中某一时刻的数据快照，包括其当前的系统提示、记忆、上下文、装备的工具以及其他 **随时间变化** 的信息。\n\n为了管理应用程序的状态，AgentScope 设计实现了 **自动状态注册** 和 **会话级状态管理**，具有以下特性：\n\n- 支持所有继承自 ``StateModule`` 的变量的 **自动状态注册**\n- 支持使用自定义序列化/反序列化方法的 **手动状态注册**\n- 支持 **会话/应用程序级别管理**\n\"\"\"\nimport asyncio\nimport json\nimport os\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.module import StateModule\nfrom agentscope.session import JSONSession\nfrom agentscope.tool import Toolkit\n\n# %%\n# 状态模块\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# ``StateModule`` 类是 AgentScope 中状态管理的基础，提供三个基本函数：\n#\n# .. list-table:: ``StateModule`` 的方法\n#     :header-rows: 1\n#\n#     * - 方法\n#       - 参数\n#       - 描述\n#     * - ``register_state``\n#       - | ``attr_name``,\n#         | ``custom_to_json`` （可选）,\n#         | ``custom_from_json`` （可选）\n#       - 将属性注册为其状态，带有可选的序列化/反序列化函数。\n#     * - ``state_dict``\n#       -\n#       - 获取当前对象的状态字典\n#     * - ``load_state_dict``\n#       - | ``state_dict``,\n#         | ``strict`` （可选）\n#       - 将状态字典加载到当前对象\n#\n# 在 ``StateModule`` 的对象中，以下所有属性都将被视为其状态的一部分：\n#\n# - 继承自 ``StateModule`` 的 **属性**\n# - 通过 ``register_state`` 方法注册的 **属性**\n#\n# 注意 ``StateModule`` 支持 **嵌套** 序列化和反序列化，例如下面的示例中，``ClassB`` 中包含一个 ``ClassA`` 的实例：\n#\n\n\nclass ClassA(StateModule):\n    def __init__(self) -> None:\n        super().__init__()\n        self.cnt = 123\n        # 将 cnt 属性注册为状态\n        self.register_state(\"cnt\")\n\n\nclass ClassB(StateModule):\n    def __init__(self) -> None:\n        super().__init__()\n\n        # 属性 \"a\" 继承自 StateModule，将自动视为状态的一部分\n        self.a = ClassA()\n\n        # 手动将属性 \"c\" 注册为状态\n        self.c = \"Hello, world!\"\n        self.register_state(\"c\")\n\n\nobj_b = ClassB()\n\nprint(\"obj_b.a 的状态：\")\nprint(obj_b.a.state_dict())\n\nprint(\"\\nobj_b 的状态：\")\nprint(json.dumps(obj_b.state_dict(), indent=4))\n\n# %%\n# 我们可以观察到 ``obj_b`` 的状态自动包含了其属性 ``a`` 的状态。\n#\n# 在 AgentScope 中，``AgentBase``、``MemoryBase``、``LongTermMemoryBase`` 和 ``Toolkit`` 类都继承自 ``StateModule``，因此支持自动和嵌套状态管理。\n#\n\n# 创建一个智能体\nagent = ReActAgent(\n    name=\"Friday\",\n    sys_prompt=\"你是一个名为 Friday 的助手。\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n    ),\n    formatter=DashScopeChatFormatter(),\n    memory=InMemoryMemory(),\n    toolkit=Toolkit(),\n)\n\ninitial_state = agent.state_dict()\n\nprint(\"智能体的初始状态：\")\nprint(json.dumps(initial_state, indent=4, ensure_ascii=False))\n\n# %%\n# 然后我们通过生成回复消息来改变其状态：\n#\n\n\nasync def example_agent_state() -> None:\n    \"\"\"智能体状态管理示例。\"\"\"\n    await agent(Msg(\"user\", \"你好，智能体！\", \"user\"))\n\n    print(\"生成回复后智能体的状态：\")\n    print(json.dumps(agent.state_dict(), indent=4, ensure_ascii=False))\n\n\nasyncio.run(example_agent_state())\n\n# %%\n# 现在我们将智能体的状态恢复到初始状态：\n#\n\nagent.load_state_dict(initial_state)\n\nprint(\"加载初始状态后：\")\nprint(json.dumps(agent.state_dict(), indent=4, ensure_ascii=False))\n\n# %%\n# 会话管理\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# 会话（Session）是应用程序中状态的集合，例如多个智能体。\n#\n# AgentScope 提供了 ``SessionBase`` 类，包含两个用于会话管理的抽象方法：``save_session_state`` 和 ``load_session_state``。\n# 开发者可以继承该类来实现自己的状态保存方案，例如对接到自己的数据库或文件系统。\n#\n# 在 AgentScope 中，我们提供了基于 JSON 和文件系统的的会话类 ``JSONSession``，\n# 它会将状态保存到会化 ID 命名的 JSON 文件中，也可以从中加载状态。\n#\n# 保存会话状态\n# -----------------------------------------\n#\n\n# 通过生成回复消息改变智能体状态\nasyncio.run(example_agent_state())\n\nprint(\"\\n智能体的状态：\")\nprint(json.dumps(agent.state_dict(), indent=4, ensure_ascii=False))\n\n# %%\n# 然后我们将其保存到会话文件中：\n\nsession = JSONSession(\n    save_dir=\"./\",  # 保存所有session文件的目录\n)\n\n\nasync def example_session() -> None:\n    \"\"\"会话管理示例。\"\"\"\n\n    # 可以保存多个状态，只需要输入的对象为 `StateModule` 的子类。\n    await session.save_session_state(\n        session_id=\"user_bob\",\n        agent=agent,\n    )\n\n    print(\"保存的状态：\")\n    with open(\"./user_bob.json\", \"r\", encoding=\"utf-8\") as f:\n        print(json.dumps(json.load(f), indent=4, ensure_ascii=False))\n\n\nasyncio.run(example_session())\n\n# %%\n# 加载会话状态\n# -----------------------------------------\n# 然后我们可以从会话文件中加载状态：\n#\n\n\nasync def example_load_session() -> None:\n    \"\"\"加载会话状态示例。\"\"\"\n\n    # 清空智能体状态\n    await agent.memory.clear()\n\n    print(\"当前智能体状态：\")\n    print(json.dumps(agent.state_dict(), indent=4, ensure_ascii=False))\n\n    # 从会话文件中加载状态\n    await session.load_session_state(\n        session_id=\"user_bob\",\n        # 这里使用的关键词参数必须与 `save_session_state` 中的参数一致\n        agent=agent,\n    )\n    print(\"加载会话状态后智能体的状态：\")\n    print(json.dumps(agent.state_dict(), indent=4, ensure_ascii=False))\n\n\nasyncio.run(example_load_session())\n\n# %%\n# 此时我们可以观察到智能体的状态已经恢复到之前保存的状态。\n#\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_studio.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _studio:\n\nAgentScope Studio\n=========================\n\nAgentScope Studio 是一个本地部署的 Web 应用程序，它\n\n- 为智能体应用程序的开发提供 **项目管理**\n- 为运行中的应用程序提供 **可视化** 追踪\n- 内置一个为 \"Friday\" 的 **智能体**，支持用户二次开发\n\n.. note:: Studio 正在快速开发中，更多功能即将推出！\n\n.. figure:: ../../_static/images/studio_home.webp\n    :width: 100%\n    :alt: AgentScope Studio 主页\n    :class: bordered-image\n    :align: center\n\n    AgentScope Studio 主页\n\n快速开始\n~~~~~~~~~~~~~~~~~~~~~~~~\n\nAgentScope Studio 通过 ``npm`` 安装：\n\n.. code-block:: bash\n\n    npm install -g @agentscope/studio\n\n\n使用以下命令启动 Studio：\n\n.. code-block:: bash\n\n    as_studio\n\n要将应用程序连接到 Studio，请在 ``agentscope.init`` 函数中使用 ``studio_url`` 参数：\n\n.. code-block:: python\n\n    import agentscope\n\n    agentscope.init(studio_url=\"http://localhost:3000\")\n\n    # 应用程序代码\n    ...\n\n然后可以在 Studio 中看到该应用程序，如下所示：\n\n.. figure:: ../../_static/images/studio_project.webp\n    :width: 100%\n    :alt: 项目管理\n    :class: bordered-image\n    :align: center\n\n    AgentScope Studio 中的项目管理\n\n关于应用程序的详细信息，例如 token 使用情况、模型调用和追踪信息，都可以在 Studio 中查看。\n\n.. figure:: ../../_static/images/studio_run.webp\n    :width: 100%\n    :alt: AgentScope Studio 运行页面\n    :class: bordered-image\n    :align: center\n\n    AgentScope Studio 中的应用程序可视化\n\n\nFriday 智能体\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nFriday 是由 AgentScope 构建的实验性本地部署智能体，旨在\n\n- 回答关于 AgentScope 开发的问题，\n- 为开发者提供便捷的二次开发环境，\n- 集成 AgentScope 中所有可用功能以构建更强大的智能体，以及\n- 持续测试和集成 AgentScope 中的高级功能。\n\n.. note:: 我们非常欢迎开源社区贡献并改进 Friday！欢迎在我们的 `GitHub 仓库 <https://github.com/agentscope-ai/agentscope>`_ 上提出问题或拉取请求。\n\n我们正在持续改进 Friday，目前它集成了 AgentScope 中的以下功能：\n\n.. list-table::\n    :header-rows: 1\n\n    * - 功能\n      - 状态\n      - 进一步阅读\n      - 描述\n    * - 元工具\n      - ✅\n      - :ref:`tool`\n      - 分组工具管理，允许智能体自己更改装备的工具。\n    * - 智能体钩子\n      - ✅\n      - :ref:`hook`\n      - 使用钩子将打印消息转发到前端。\n    * - 智能体中断\n      - ✅\n      - :ref:`agent`\n      - 允许用户通过后处理中断智能体的回复过程。\n    * - 截断提示\n      - ✅\n      - :ref:`prompt`\n      - 支持使用预设的最大 token 限制截断提示。\n    * - 状态和会话管理\n      - ✅\n      - :ref:`state`\n      - 智能体的自动状态管理和会话管理，在不同运行之间维护状态。\n    * - 长期记忆\n      - 🚧\n      - :ref:`memory`\n      - 支持长期记忆管理。\n\n\n\"\"\"\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_token.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _token:\n\nToken 计数\n=========================\n\nAgentScope 在 ``agentscope.token`` 模块下提供了 token 计数功能，用于计算给定消息中\n的 token 数量，允许开发者在调用 LLM API 前预估 token 数量。\n\n具体而言，可用的 token 计数器如下：\n\n.. list-table::\n    :header-rows: 1\n\n    * - LLM API\n      - 类\n      - 实现方式\n      - 支持图像数据\n      - 支持工具\n    * - Anthropic\n      - ``AnthropicTokenCounter``\n      - 官方 API\n      - ✅\n      - ✅\n    * - OpenAI\n      - ``OpenAITokenCounter``\n      - 本地计算\n      - ✅\n      - ✅\n    * - Gemini\n      - ``GeminiTokenCounter``\n      - 官方 API\n      - ✅\n      - ✅\n    * - HuggingFace\n      - ``HuggingFaceTokenCounter``\n      - 基于Tokenizer计算\n      - 取决于模型\n      - 取决于模型\n\n.. tip:: 格式化器模块已集成了 token 计数器以支持提示截断。更多详细信息请参考 :ref:`prompt` 部分。\n\n.. note::\n - 对于 DashScope 模型，目前 dashscope 库不提供 token 计数 API。因此我们建议使用 HuggingFace token 计数器代替。\n - 对于 OpenAI 模型，由于官方未提供 token 计数 API，因此可能存在与官方计算结果不一致的情况。\n\n下面展示使用 OpenAI token 计数器计算 token 数量的示例：\n\"\"\"\n\nimport asyncio\nfrom agentscope.token import OpenAITokenCounter\n\n\nasync def example_token_counting():\n    # 示例消息\n    messages = [\n        {\"role\": \"user\", \"content\": \"Hello!\"},\n        {\"role\": \"assistant\", \"content\": \"Hi, how can I help you?\"},\n    ]\n\n    # OpenAI token 计数\n    openai_counter = OpenAITokenCounter(model_name=\"gpt-4.1\")\n    n_tokens = await openai_counter.count(messages)\n\n    print(f\"Token 数量: {n_tokens}\")\n\n\nasyncio.run(example_token_counting())\n\n\n# %%\n# 进一步阅读\n# ------------------------------\n#\n# - :ref:`prompt`\n#\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_tool.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _tool:\n\n工具\n=========================\n\n为了确保准确可靠的工具解析，AgentScope 全面支持工具 API 的使用，具有以下特性：\n\n- 支持从文档字符串 **自动** 解析工具函数\n- 支持 **同步和异步** 工具函数\n- 支持 **流式** 工具响应（同步或异步生成器）\n- 支持对工具 JSON Schema 的 **动态扩展**\n- 支持用户实时 **中断** 工具的执行\n- 支持智能体的 **自主工具管理**\n\n所有上述功能都由 AgentScope 中的 ``Toolkit`` 类实现，该类负责管理工具函数及其执行。\n\n.. tip:: MCP（模型上下文协议）的支持请参考 :ref:`mcp` 部分。\n\"\"\"\nimport asyncio\nimport inspect\nimport json\nfrom typing import Any, AsyncGenerator\n\nfrom pydantic import BaseModel, Field\n\nimport agentscope\nfrom agentscope.message import TextBlock, ToolUseBlock\nfrom agentscope.tool import ToolResponse, Toolkit, execute_python_code\n\n\n# %%\n# 工具函数\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# 在 AgentScope 中，工具函数是一个 Python 的可调用对象，它\n#\n# - 返回一个 ``ToolResponse`` 对象或产生 ``ToolResponse`` 对象的生成器（可以是异步或同步）\n# - 具有描述工具功能和参数的文档字符串\n#\n# 工具函数的模板如下：\n\n\ndef tool_function(a: int, b: str) -> ToolResponse:\n    \"\"\"{函数描述}\n\n    Args:\n        a (int):\n            {第一个参数的描述}\n        b (str):\n            {第二个参数的描述}\n    \"\"\"\n\n\n# %%\n# .. tip:: 实例方法和类方法也可以用作工具函数，``Toolkit`` 中将自动忽略 ``self`` 和 ``cls`` 参数。\n#\n# AgentScope 在 ``agentscope.tool`` 模块下提供了几个内置工具函数，如 ``execute_python_code``、``execute_shell_command`` 和文本文件读写函数。\n#\n\nprint(\"内置工具函数：\")\nfor _ in agentscope.tool.__all__:\n    if _ not in [\"Toolkit\", \"ToolResponse\"]:\n        print(_)\n\n# %%\n# 工具模块（Toolkit）\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# ``Toolkit`` 类设计用于管理工具函数，从文档字符串中提取它们的 JSON Schema，并为工具执行提供统一接口。\n#\n# 基本用法\n# ------------------------------\n# ``Toolkit`` 类的基本功能是注册工具函数并执行它们。\n#\n\n\n# 准备一个自定义工具函数\nasync def my_search(query: str, api_key: str) -> ToolResponse:\n    \"\"\"一个简单的示例工具函数。\n\n    Args:\n        query (str):\n            搜索查询。\n        api_key (str):\n            用于身份验证的 API 密钥。\n    \"\"\"\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=f\"正在使用 API 密钥 '{api_key}' 搜索 '{query}'\",\n            ),\n        ],\n    )\n\n\n# 在工具模块中注册工具函数\ntoolkit = Toolkit()\ntoolkit.register_tool_function(my_search)\n\n# %%\n# 注册工具函数后，可以通过调用 ``get_json_schemas`` 方法获取其 JSON Schema。\n#\n\nprint(\"工具 JSON Schemas：\")\nprint(json.dumps(toolkit.get_json_schemas(), indent=4, ensure_ascii=False))\n\n# %%\n# ``Toolkit`` 还允许开发者为工具函数预设参数，这对于 API 密钥或其他敏感信息特别有用。\n#\n\n# 先清空工具模块\ntoolkit.clear()\n\n# 使用预设关键字参数注册工具函数\ntoolkit.register_tool_function(my_search, preset_kwargs={\"api_key\": \"xxx\"})\n\nprint(\"带预设参数的工具 JSON Schemas：\")\nprint(json.dumps(toolkit.get_json_schemas(), indent=4, ensure_ascii=False))\n\n# %%\n# 预设参数后，该参数将从 JSON schema 中被移除，并在工具调用时自动传递给该工具函数。\n#\n# 在 ``Toolkit`` 中，``call_tool_function`` 方法以 ``ToolUseBlock`` 作为输入执行指定的工具函数，统一返回一个 **异步生成器**，该生成器产生 ``ToolResponse`` 对象。\n#\n# .. note:: AgentScope 中，流式返回的工具函数应该是 **“累积的”**，即当前块的内容应包含之前所有块的内容。\n#\n\n\nasync def example_tool_execution() -> None:\n    \"\"\"工具调用执行示例。\"\"\"\n    res = await toolkit.call_tool_function(\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"123\",\n            name=\"my_search\",\n            input={\"query\": \"AgentScope\"},\n        ),\n    )\n\n    # 非流式返回的工具函数只有一个 ToolResponse 返回\n    print(\"工具响应：\")\n    async for tool_response in res:\n        print(tool_response)\n\n\nasyncio.run(example_tool_execution())\n\n# %%\n# 动态扩展 JSON Schema\n# --------------------------------------\n#\n# Toolkit 允许通过调用 ``set_extended_model`` 方法动态扩展工具函数的 JSON schemas。\n# 这种功能允许开发者在不修改工具函数原始定义的情况下，向工具函数添加更多参数。\n#\n# .. tip:: 相关场景包括动态 :ref:`structured-output` 和 CoT（思维链）推理\n#\n# .. note:: 要扩展的工具函数应该接受可变关键字参数（``**kwargs``），以便附加字段可以传递给它。\n#\n# 以 CoT 推理为例，我们可以用 ``thinking`` 字段扩展所有工具函数，允许智能体总结当前状态然后决定下一步做什么。\n#\n\n\n# 示例工具函数\ndef tool_function(**kwargs: Any) -> ToolResponse:\n    \"\"\"一个工具函数\"\"\"\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=f\"接收到的参数：{kwargs}\",\n            ),\n        ],\n    )\n\n\n# 添加一个思考字段，以便智能体在给出其他参数之前可以思考。\nclass ThinkingModel(BaseModel):\n    \"\"\"用于附加字段的 Pydantic 模型。\"\"\"\n\n    thinking: str = Field(\n        description=\"总结当前状态并决定下一步做什么。\",\n    )\n\n\n# 注册\ntoolkit.set_extended_model(\"my_search\", ThinkingModel)\n\nprint(\"扩展后的 JSON Schema：\")\nprint(json.dumps(toolkit.get_json_schemas(), indent=4, ensure_ascii=False))\n\n# %%\n# 中断工具执行\n# ------------------------------\n# ``Toolkit`` 类支持 **异步工具函数** 的 **执行中断**，并提供 **面向智能体的后处理机制**。\n# 这种中断基于 asyncio 取消机制实现，其后处理过程根据工具函数的返回类型而有所不同。\n#\n# .. note:: 对于同步（工具）函数，它们的执行无法通过 asyncio 取消来中断。因此其中断在智能体内而不是工具模块内处理。\n#  有关更多信息，请参考 :ref:`agent` 部分。\n#\n# 具体来说，如果工具函数返回 ``ToolResponse`` 对象，将产生一个带有中断消息的 ``ToolResponse`` 对象。\n# 这样智能体可以观察到这一中断并相应地处理它。\n# 此外，该 ``ToolResponse`` 对象中的 ``is_interrupted`` 将设置为 ``True``，外部调用者可以决定是否将 ``CancelledError`` 异常抛出到外层。\n#\n# 可以被中断的异步工具函数示例如下：\n#\n\n\nasync def non_streaming_function() -> ToolResponse:\n    \"\"\"一个可以被中断的非流式工具函数。\"\"\"\n    await asyncio.sleep(1)  # 模拟长时间运行的任务\n\n    # 为演示目的模拟中断\n    raise asyncio.CancelledError()\n\n    # 由于取消，以下代码不会被执行\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=\"运行成功！\",\n            ),\n        ],\n    )\n\n\nasync def example_tool_interruption() -> None:\n    \"\"\"工具中断示例。\"\"\"\n    toolkit = Toolkit()\n    toolkit.register_tool_function(non_streaming_function)\n    res = await toolkit.call_tool_function(\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"123\",\n            name=\"non_streaming_function\",\n            input={},\n        ),\n    )\n\n    async for tool_response in res:\n        print(\"工具响应：\")\n        print(tool_response)\n        print(\"中断标志：\")\n        print(tool_response.is_interrupted)\n\n\nasyncio.run(example_tool_interruption())\n\n# %%\n# 对于流式工具函数，``Toolkit`` 将把中断消息附加到中断发生时的 ``ToolResponse`` 上。\n# 通过这种方式，智能体可以观察到工具在中断前返回的内容。\n#\n# 中断流式工具函数的示例如下：\n#\n\n\nasync def streaming_function() -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"一个可以被中断的流式工具函数。\"\"\"\n    # 模拟一块响应\n    yield ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=\"1234\",\n            ),\n        ],\n        stream=True,\n    )\n\n    # 模拟中断\n    raise asyncio.CancelledError()\n\n    # 由于取消，以下代码不会被执行\n    yield ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=\"123456789\",\n            ),\n        ],\n    )\n\n\nasync def example_streaming_tool_interruption() -> None:\n    \"\"\"流式工具中断示例。\"\"\"\n    toolkit = Toolkit()\n    toolkit.register_tool_function(streaming_function)\n\n    res = await toolkit.call_tool_function(\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"xxx\",\n            name=\"streaming_function\",\n            input={},\n        ),\n    )\n\n    i = 0\n    async for tool_response in res:\n        print(f\"块 {i}：\")\n        print(tool_response)\n        print(\"中断标志：\", tool_response.is_interrupted, \"\\n\")\n        i += 1\n\n\nasyncio.run(example_streaming_tool_interruption())\n\n# %%\n# 自动工具管理\n# -------------------------------------\n# .. image:: https://img.alicdn.com/imgextra/i3/O1CN013cvRpO27MfesMsTeh_!!6000000007783-2-tps-840-521.png\n#     :width: 100%\n#     :align: center\n#     :alt: 自动工具管理\n#\n#\n# ``Toolkit`` 类通过引入 **工具组** （Group） 的概念，以及名为 ``reset_equipped_tools`` 的 **元工具函数** （Meta Tool） 来支持 **自动工具管理** 。\n#\n# 工具组是一组相关工具函数的集合，例如浏览器使用工具、地图服务工具等，它们将被一起管理。工具组有激活和非激活两种状态，\n# 只有工具组被激活，其中的工具函数才对智能体可见，即可以通过 ``toolkit.get_json_schemas()`` 方法访问。\n#\n# 注意有一个名为 ``basic`` 的特殊组，它始终处于激活状态，注册工具时如果未指定组名，则工具函数将默认添加到此组。\n#\n# .. tip:: ``basic`` 组确保开发者不需要“组管理”的功能时，工具的基本使用不会受到影响。\n#\n# 现在我们尝试创建一个名为 ``browser_use`` 的工具组，其中包含一些网页浏览工具。\n#\n\n\n# 我们创建一些浏览器操作相关的工具\ndef navigate(url: str) -> ToolResponse:\n    \"\"\"导航到网页。\n\n    Args:\n        url (str):\n            要导航到的网页的 URL。\n    \"\"\"\n    pass\n\n\ndef click_element(element_id: str) -> ToolResponse:\n    \"\"\"点击网页上的元素。\n\n    Args:\n        element_id (str):\n            要点击的元素的 ID。\n    \"\"\"\n    pass\n\n\ntoolkit = Toolkit()\n\n# 创建一个名为 browser_use 的工具组\ntoolkit.create_tool_group(\n    group_name=\"browser_use\",\n    description=\"用于网页浏览的工具函数。\",\n    active=False,\n    # 使用这些工具时的注意事项\n    notes=\"\"\"1. 使用 ``navigate`` 打开网页。\n2. 当需要用户身份验证时，请向用户询问凭据\n3. ...\"\"\",\n)\n\ntoolkit.register_tool_function(navigate, group_name=\"browser_use\")\ntoolkit.register_tool_function(click_element, group_name=\"browser_use\")\n\n# 我们也可以注册一些基本工具\ntoolkit.register_tool_function(execute_python_code)\n\n# %%\n# 此时 ``browser_use`` 未被激活，如果我们检查工具 JSON schema，只能看到 ``execute_python_code`` 工具：\n\nprint(\"此时对智能体可见的工具函数 JSON Schemas：\")\nprint(json.dumps(toolkit.get_json_schemas(), indent=4, ensure_ascii=False))\n\n# %%\n# 使用 ``update_tool_groups`` 方法激活或停用工具组：\n\ntoolkit.update_tool_groups(group_names=[\"browser_use\"], active=True)\n\nprint(\"激活后对智能体可见的工具函数 JSON Schemas：\")\nprint(json.dumps(toolkit.get_json_schemas(), indent=4, ensure_ascii=False))\n\n# %%\n# 此外，``Toolkit`` 提供了一个名为 ``reset_equipped_tools`` 的元工具函数，它会将所有组名（除了 \"basic\"）作为一个 bool 型的参数，\n# 让智能体调用该工具来决定要激活哪些工具组：\n#\n# .. note:: 在 ``ReActAgent`` 类的实现中，只需要在构造函数中将 ``enable_meta_tool`` 设置为 ``True`` 即可启用元工具函数。\n#\n\n# 注册元工具函数\ntoolkit.register_tool_function(toolkit.reset_equipped_tools)\n\nreset_equipped = next(\n    tool\n    for tool in toolkit.get_json_schemas()\n    if tool[\"function\"][\"name\"] == \"reset_equipped_tools\"\n)\nprint(\"``reset_equipped_tools`` 函数的 JSON schema：\")\nprint(\n    json.dumps(\n        reset_equipped,\n        indent=4,\n        ensure_ascii=False,\n    ),\n)\n\n# %%\n# 当智能体调用 ``reset_equipped_tools`` 时，对应工具组将被激活，同时返回的结果中将包含工具的使用注意事项。\n#\n\n\nasync def mock_agent_reset_tools() -> None:\n    \"\"\"模拟智能体调用 reset_equipped_tools 函数。\"\"\"\n    res = await toolkit.call_tool_function(\n        ToolUseBlock(\n            type=\"tool_use\",\n            id=\"456\",\n            name=\"reset_equipped_tools\",\n            input={\n                \"browser_use\": True,  # 激活浏览器使用工具组\n            },\n        ),\n    )\n\n    async for tool_response in res:\n        print(\"工具响应中的文字返回：\")\n        print(tool_response.content[0][\"text\"])\n\n\nasyncio.run(mock_agent_reset_tools())\n\n# %%\n# 此外，``Toolkit`` 还通过 ``get_activated_notes`` 函数提供已经被激活了的工具组的 notes，开发者也可以将其组装到智能体的系统提示中，从而达到动态管理工具的作用。\n#\n# .. tip:: 自动工具管理功能已在 ``ReActAgent`` 类中实现，有关更多详细信息，请参考 :ref:`agent` 部分。\n#\n\n# 再创建一个工具组\ntoolkit.create_tool_group(\n    group_name=\"map_service\",\n    description=\"谷歌地图服务工具。\",\n    active=True,\n    notes=\"\"\"1. 使用 ``get_location`` 获取地点的位置。\n2. ...\"\"\",\n)\n\nprint(\"激活工具组的汇总注意事项：\")\nprint(toolkit.get_activated_notes())\n\n# %%\n# 进一步阅读\n# ---------------------\n# - :ref:`agent`\n# - :ref:`state`\n# - :ref:`mcp`\n#\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_tracing.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _tracing:\n\n追踪\n==============================\n\nAgentScope 实现了基于 OpenTelemetry 的追踪来监控和调试\n智能体应用程序的执行，具有以下特性\n\n- 为 LLM、工具、智能体、格式化器等提供内置追踪\n- 支持错误和异常追踪\n- 在 AgentScope Studio 中提供原生追踪 **可视化**\n- 支持连接到 **第三方平台**，如阿里云云监控、`Arize-Phoenix <https://github.com/Arize-ai/phoenix>`_、`Langfuse <https://langfuse.com/>`_ 等\n\n设置\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n.. note:: 连接到 :ref:`studio` 或第三方平台应该在应用程序开始时通过调用 ``agentscope.init`` 函数完成。\n\nAgentScope Studio\n---------------------------------------\n\n.. figure:: ../../_static/images/studio_tracing.webp\n    :width: 100%\n    :alt: AgentScope Studio 追踪页面\n    :class: bordered-image\n    :align: center\n\n    *AgentScope Studio 中的追踪页面*\n\n\n当连接到 AgentScope Studio 时，只需在 ``agentscope.init`` 函数中提供 ``studio_url`` 参数。\n\n.. code-block:: python\n\n    import agentscope\n\n    agentscope.init(studio_url=\"http://xxx:port\")\n\n\n第三方平台\n---------------------------------------\n\n要连接到第三方追踪平台，请在 ``agentscope.init`` 函数中设置 ``tracing_url`` 参数。\n``tracing_url`` 是您的 OpenTelemetry 收集器或任何支持 OTLP（OpenTelemetry 协议）的服务器 URL。\n\n.. code-block:: python\n\n    import agentscope\n\n    # 连接到 OpenTelemetry 兼容的后端\n    agentscope.init(tracing_url=\"https://your-tracing-backend:port/traces\")\n\n以阿里云云监控、Arize-Phoenix 和 Langfuse 为例：\n\n**阿里云云监控（Alibaba Cloud CloudMonitor）**：全托管可观测平台。\n\n.. code-block:: python\n    :caption: 连接到阿里云云监控\n\n    agentscope.init(tracing_url=\"https://tracing-cn-hangzhou.arms.aliyuncs.com/adapt_xxx/api/otlp/traces\")\n\n.. tip::\n    **获取 Endpoint：** 在 `ARMS 控制台 <https://arms.console.aliyun.com/>`_ 的 **接入中心** > **OpenTelemetry** 中，\n    根据实际部署地域选择对应的 **公网接入点**。可通过环境变量 ``OTEL_SERVICE_NAME`` 自定义应用名称。\n    阿里云云监控可通过 `LoongSuite <https://github.com/alibaba/loongsuite-python-agent>`_ 探针提供零侵入的自动化接入。\n    更多信息请参考 `云监控文档 <https://help.aliyun.com/zh/cms/cloudmonitor-2-0/user-guide/model-application>`_。\n\n**Arize-Phoenix**：需要在环境变量中设置 ``PHOENIX_API_KEY``。\n\n.. code-block:: python\n    :caption: 连接到 Arize Phoenix\n\n    # Arize Phoenix 集成\n    import os\n\n    PHOENIX_API_KEY = os.environ.get(\"PHOENIX_API_KEY\")\n    os.environ[\"OTEL_EXPORTER_OTLP_HEADERS\"] = f\"api_key={PHOENIX_API_KEY}\"\n\n    agentscope.init(tracing_url=\"https://app.phoenix.arize.com/v1/traces\")\n\n**LangFuse**：需要在环境变量中设置 ``LANGFUSE_PUBLIC_KEY`` 和 ``LANGFUSE_SECRET_KEY``。\n授权头是使用这些密钥构建的。\n\n.. code-block:: python\n    :caption: 连接到 LangFuse\n\n    import os, base64\n\n    LANGFUSE_PUBLIC_KEY = os.environ[\"LANGFUSE_PUBLIC_KEY\"]\n    LANGFUSE_SECRET_KEY = os.environ[\"LANGFUSE_SECRET_KEY\"]\n    LANGFUSE_AUTH_STRING = f\"{LANGFUSE_PUBLIC_KEY}:{LANGFUSE_SECRET_KEY}\"\n\n    LANGFUSE_AUTH = base64.b64encode(LANGFUSE_AUTH_STRING.encode(\"utf-8\")).decode(\"ascii\")\n    os.environ[\"OTEL_EXPORTER_OTLP_HEADERS\"] = f\"Authorization=Basic {LANGFUSE_AUTH}\"\n\n    # 欧盟数据区域\n    agentscope.init(tracing_url=\"https://cloud.langfuse.com/api/public/otel/v1/traces\")\n    # 美国数据区域\n    # agentscope.init(tracing_url=\"https://us.cloud.langfuse.com/api/public/otel/v1/traces\")\n\n\n自定义追踪\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n如前所述，AgentScope 中的追踪功能是基于 OpenTelemetry 实现的。\n这意味着 AgentScope 中的追踪与开发者基于 OpenTelemetry SDK 自己实现的的追踪代码**完全兼容**。\n\n此外，AgentScope 内置了以下装饰器来追踪相应的模块，它们对不同类的特殊属性，以及返回值做了相应的特殊处理：\n\n- ``@trace_llm``：追踪 ``ChatModelBase`` 子类的 ``__call__`` 函数\n- ``@trace_reply``：追踪 ``AgentBase`` 子类的 ``reply`` 函数\n- ``@trace_format``：追踪 ``FormatterBase`` 子类的 ``format`` 函数\n- ``@trace``：追踪一般函数\n\n\n追踪大语言模型\n----------------------------------------\n\n``@trace_llm`` 装饰器用于追踪 ``ChatModelBase`` 类的 ``__call__`` 函数。\n\n.. code-block:: python\n    :caption: 追踪新的 ChatModel 类\n\n    class ExampleChatModel(ChatModelBase):\n        \\\"\\\"\\\"示例模型\\\"\\\"\\\"\n\n        ...\n\n        @trace_llm\n        async def __call__(\n            self,\n            *args: Any,\n            **kwargs: Any,\n        ) -> AsyncGenerator[ChatResponse, None] | ChatResponse:\n            \\\"\\\"\\\"LLM 调用\\\"\\\"\\\"\n            ...\n\n\n追踪智能体\n----------------------------------------\n\n``@trace_reply`` 装饰器用于追踪智能体的 `reply` 函数。\n\n.. code-block:: python\n    :caption: 追踪新的 Agent 类\n\n    class ExampleAgent(AgentBase):\n        \\\"\\\"\\\"示例智能体类\\\"\\\"\\\"\n\n        @trace_reply\n        async def reply(self, *args: Any, **kwargs: Any) -> Msg:\n            \\\"\\\"\\\"回复消息。\\\"\\\"\\\"\n            ...\n\n\n追踪格式化器\n----------------------------------------\n``@trace_format`` 装饰器用于格式化器实现并追踪 `format` 函数。\n\n.. code-block:: python\n    :caption: 追踪新的 Formatter 类\n\n    class ExampleFormatter(FormatterBase):\n        \\\"\\\"\\\"简单的示例格式化器类\\\"\\\"\\\"\n\n        @trace_format\n        async def format(self, *args: Any, **kwargs: Any) -> list[dict]:\n            \\\"\\\"\\\"示例格式化\\\"\\\"\\\"\n\n\n一般函数追踪\n----------------------------------------\n\n``@trace`` 装饰器与上述装饰器不同，它是一个通用的追踪装饰器，可以应用于任何函数。\n它需要一个 `name` 参数来标识被追踪的函数，并且可以追踪各种类型的函数，包括：\n\n- 同步函数\n- 同步生成器函数\n- 异步函数\n- 异步生成器函数\n\n.. code-block:: python\n    :caption: 一般追踪示例\n\n    # 1. 同步函数\n    @trace(name='simple_function')\n    def simple_function(name: str, age: int) -> str:\n        \\\"\\\"\\\"带有自动追踪的简单函数。\\\"\\\"\\\"\n        return f\"你好, {name}! 你的年龄是 {age} 岁。\"\n\n    # 2. 同步生成器函数\n    @trace(name='number_generator')\n    def number_generator(n: int) -> Generator[int, None, None]:\n        \\\"\\\"\\\"生成从 0 到 n-1 的数字。\\\"\\\"\\\"\n        for i in range(n):\n            yield i\n\n    # 3. 异步函数\n    @trace(name='async_function')\n    async def async_function(data: dict) -> dict:\n        \\\"\\\"\\\"异步处理数据。\\\"\\\"\\\"\n        return {\"processed\": data}\n\n    # 4. 异步生成器函数\n    @trace(name='async_stream')\n    async def async_stream(n: int) -> AsyncGenerator[str, None]:\n        \\\"\\\"\\\"异步生成数据流。\\\"\\\"\\\"\n        for i in range(n):\n            yield f\"data_{i}\"\n\n\"\"\"\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_tts.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _tts:\n\nTTS\n====================\n\nAgentScope 为多个 API 提供商的文本转语音（TTS）模型提供了统一接口。\n本章节演示如何在 AgentScope 中使用 TTS 模型。\n\nAgentScope 支持以下 TTS API：\n\n.. list-table:: 内置 TTS 模型\n    :header-rows: 1\n\n    * - API\n      - 类\n      - 流式输入\n      - 非流式输入\n      - 流式输出\n      - 非流式输出\n    * - DashScope 实时 API\n      - ``DashScopeRealtimeTTSModel``\n      - ✅\n      - ✅\n      - ✅\n      - ✅\n    * - DashScope CosyVoice 实时 API\n      - ``DashScopeCosyVoiceRealtimeTTSModel``\n      - ✅\n      - ✅\n      - ✅\n      - ✅\n    * - DashScope API\n      - ``DashScopeTTSModel``\n      - ❌\n      - ✅\n      - ✅\n      - ✅\n    * - DashScope CosyVoice API\n      - ``DashScopeCosyVoiceTTSModel``\n      - ❌\n      - ✅\n      - ✅\n      - ✅\n    * - OpenAI API\n      - ``OpenAITTSModel``\n      - ❌\n      - ✅\n      - ✅\n      - ✅\n    * - Gemini API\n      - ``GeminiTTSModel``\n      - ❌\n      - ✅\n      - ✅\n      - ✅\n\n.. note:: AgentScope TTS 模型中的流式输入和输出都是累积式的。\n\n**选择合适的模型：**\n\n- **使用非实时 TTS**：当已有完整文本时（例如预先编写的响应、完整的 LLM 输出）\n- **使用实时 TTS**：当文本是逐步生成时（例如 LLM 的流式返回），以获得更低的延迟\n\n\"\"\"\n\nimport asyncio\nimport os\n\nfrom agentscope.agent import ReActAgent, UserAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tts import (\n    DashScopeRealtimeTTSModel,\n    DashScopeTTSModel,\n)\n\n# %%\n# 非实时 TTS\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# 非实时 TTS 模型处理完整的文本输入，使用起来最简单，可以直接调用它们的 ``synthesize()`` 方法。\n#\n# 以 DashScope TTS 模型为例：\n\n\nasync def example_non_realtime_tts() -> None:\n    \"\"\"使用非实时 TTS 模型的基本示例。\"\"\"\n    # DashScope TTS 示例\n    tts_model = DashScopeTTSModel(\n        api_key=os.environ.get(\"DASHSCOPE_API_KEY\", \"\"),\n        model_name=\"qwen3-tts-flash\",\n        voice=\"Cherry\",\n        stream=False,  # 非流式输出\n    )\n\n    msg = Msg(\n        name=\"assistant\",\n        content=\"你好，这是 DashScope TTS。\",\n        role=\"assistant\",\n    )\n\n    tts_response = await tts_model.synthesize(msg)\n\n    # tts_response.content 包含一个带有 base64 编码音频数据的音频块\n    print(\"音频数据长度：\", len(tts_response.content[\"source\"][\"data\"]))\n\n\nasyncio.run(example_non_realtime_tts())\n\n# %%\n# **流式输出以降低延迟：**\n#\n# 当 ``stream=True`` 时，模型会逐步返回音频块，允许\n# 您在合成完成前开始播放。这减少了感知延迟。\n#\n\n\nasync def example_non_realtime_tts_streaming() -> None:\n    \"\"\"使用带流式输出的非实时 TTS 模型的示例。\"\"\"\n    # 使用流式输出的 DashScope TTS 示例\n    tts_model = DashScopeTTSModel(\n        api_key=os.environ.get(\"DASHSCOPE_API_KEY\", \"\"),\n        model_name=\"qwen3-tts-flash\",\n        voice=\"Cherry\",\n        stream=True,  # 启用流式输出\n    )\n\n    msg = Msg(\n        name=\"assistant\",\n        content=\"你好，这是带流式输出的 DashScope TTS。\",\n        role=\"assistant\",\n    )\n\n    # 合成并接收用于流式输出的异步生成器\n    async for tts_response in await tts_model.synthesize(msg):\n        # 处理到达的每个音频块\n        print(\"接收到的音频块长度：\", len(tts_response.content[\"source\"][\"data\"]))\n\n\nasyncio.run(example_non_realtime_tts_streaming())\n\n\n# %%\n# 实时 TTS\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# 实时 TTS 模型专为文本增量生成的场景设计，\n# 例如流式 LLM 响应。这通过在完整文本准备好之前\n# 开始音频合成，实现尽可能低的延迟。\n#\n# **核心概念：**\n#\n# - **有状态处理**：实时 TTS 为单个流式会话维护状态，\n#   由 ``msg.id`` 标识。一次只能有一个流式会话处于活动状态。\n# - **两种方法**：\n#\n#   - ``push(msg)``：非阻塞方法，提交文本块并立即返回。\n#     如果有可用的部分音频，可能会返回。\n#   - ``synthesize(msg)``：阻塞方法，完成会话并返回\n#     所有剩余的音频。当 ``stream=True`` 时，返回异步生成器。\n#\n# .. code-block:: python\n#\n#     async def example_realtime_tts_streaming():\n#         tts_model = DashScopeRealtimeTTSModel(\n#             api_key=os.environ.get(\"DASHSCOPE_API_KEY\", \"\"),\n#             model_name=\"qwen3-tts-flash-realtime\",\n#             voice=\"Cherry\",\n#             stream=False,\n#         )\n#\n#         # 实时 tts 模型接收累积的文本块\n#         res = await tts_model.push(msg_chunk_1)  # 非阻塞\n#         res = await tts_model.push(msg_chunk_2)  # 非阻塞\n#         ...\n#         res = await tts_model.synthesize(final_msg)  # 阻塞，获取所有剩余音频\n#\n# 在初始化时设置 ``stream=True`` 时，``synthesize()`` 方法返回 ``TTSResponse`` 对象的异步生成器，允许您在音频块到达时处理它们。\n#\n#\n# 与 ReActAgent 集成\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# AgentScope 智能体在提供 TTS 模型时，可以自动将其响应合成为语音。\n# 这与实时和非实时 TTS 模型都能无缝协作。\n#\n# **工作原理：**\n#\n# 1. 智能体生成文本响应（可能从 LLM 流式传输）\n# 2. TTS 模型自动将文本合成为音频\n# 3. 合成的音频附加到 ``Msg`` 对象的 ``speech`` 字段\n# 4. 音频在智能体的 ``self.print()`` 方法期间播放\n#\n\n\nasync def example_agent_with_tts() -> None:\n    \"\"\"使用带 TTS 的 ReActAgent 的示例。\"\"\"\n    # 创建启用了 TTS 的智能体\n    agent = ReActAgent(\n        name=\"Assistant\",\n        sys_prompt=\"你是一个有用的助手。\",\n        model=DashScopeChatModel(\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            model_name=\"qwen-max\",\n            stream=True,\n        ),\n        formatter=DashScopeChatFormatter(),\n        # 启用 TTS\n        tts_model=DashScopeRealtimeTTSModel(\n            api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n            model_name=\"qwen3-tts-flash-realtime\",\n            voice=\"Cherry\",\n        ),\n    )\n    user = UserAgent(\"User\")\n\n    # 像正常情况一样构建对话\n    msg = None\n    while True:\n        msg = await agent(msg)\n        msg = await user(msg)\n        if msg.get_text_content() == \"exit\":\n            break\n\n\n# %%\n# 自定义 TTS 模型\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# 可以通过继承 ``TTSModelBase`` 来创建自定义 TTS 实现。\n# 基类为实时和非实时 TTS 模型提供了灵活的接口。\n# 我们使用属性 ``supports_streaming_input`` 来指示 TTS 模型是否为实时模型。\n#\n# 对于实时 TTS 模型，需要实现 ``connect``、``close``、``push`` 和 ``synthesize`` 方法来处理 API 的生命周期和流式输入。\n#\n# 而对于非实时 TTS 模型，只需实现 ``synthesize`` 方法。\n#\n# 进一步阅读\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# - :ref:`agent` - 了解更多关于 AgentScope 中的智能体\n# - :ref:`message` - 理解 AgentScope 中的消息格式\n# - API 参考：:class:`agentscope.tts.TTSModelBase`\n#\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/task_tuner.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _tuner:\n\nTuner\n=================\n\nAgentScope 提供了 ``tuner`` 模块，用于通过强化学习（RL）训练智能体应用。\n本教程将带你系统了解如何利用 ``tuner`` 提升智能体在特定任务上的表现，包括：\n\n- 介绍 ``tuner`` 的核心组件\n- 演示调优流程所需的关键代码实现\n- 展示调优流程的配置与运行方法\n\n主要组件\n~~~~~~~~~~~~~~~~~~~\n``tuner`` 模块为智能体训练工作流引入了三大核心组件：\n\n- **任务数据集**：用于训练和评估智能体的任务集合。\n- **工作流函数**：封装被调优智能体应用的决策逻辑。\n- **评判函数**：评估智能体在特定任务上的表现，并为调优过程提供奖励信号。\n\n此外，``tuner`` 还提供了若干用于自定义调优流程的配置类，包括：\n\n- **TunerModelConfig**：用于指定被调优模型的相关配置。\n- **AlgorithmConfig**：用于指定强化学习算法（如 GRPO、PPO 等）及其参数。\n\n实现流程\n~~~~~~~~~~~~~~~~~~~\n本节以一个简单的数学智能体为例，演示如何用 ``tuner`` 进行训练。\n\n任务数据集\n--------------------\n任务数据集包含用于训练和评估的任务集合。\n\n``tuner`` 的任务数据集采用 Huggingface `datasets <https://huggingface.co/docs/datasets/quickstart>`_ 格式，并通过 ``datasets.load_dataset`` 加载。例如：\n\n.. code-block:: text\n\n    my_dataset/\n        ├── train.jsonl  # 训练样本\n        └── test.jsonl   # 测试样本\n\n假设 `train.jsonl` 内容如下：\n\n.. code-block:: json\n\n    {\"question\": \"2 + 2 等于多少？\", \"answer\": \"4\"}\n    {\"question\": \"4 + 4 等于多少？\", \"answer\": \"8\"}\n\n在开始调优前，你可以用如下方法来确定你的数据集能够被正确加载：\n\n.. code-block:: python\n\n    from agentscope.tuner import DatasetConfig\n\n    dataset = DatasetConfig(path=\"my_dataset\", split=\"train\")\n    dataset.preview(n=2)\n    # 输出前两个样本以验证数据集加载正确\n    # [\n    #   {\n    #     \"question\": \"2 + 2 等于多少？\",\n    #     \"answer\": \"4\"\n    #   },\n    #   {\n    #     \"question\": \"4 + 4 等于多少？\",\n    #     \"answer\": \"8\"\n    #   }\n    # ]\n\n工作流函数\n--------------------\n工作流函数定义了智能体与环境的交互方式和决策过程。所有工作流函数需遵循 ``agentscope.tuner.WorkflowType`` 的输入/输出签名。\n\n以下是一个用 ReAct 智能体回答数学问题的简单工作流函数示例：\n\"\"\"\n\nfrom typing import Dict, Optional\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import OpenAIChatFormatter\nfrom agentscope.message import Msg\nfrom agentscope.model import ChatModelBase\nfrom agentscope.tuner import WorkflowOutput\n\n\nasync def example_workflow_function(\n    task: Dict,\n    model: ChatModelBase,\n    auxiliary_models: Optional[Dict[str, ChatModelBase]] = None,\n) -> WorkflowOutput:\n    \"\"\"一个用于调优的工作流函数示例。\n\n    Args:\n        task (`Dict`): 任务信息。\n        model (`ChatModelBase`): 智能体使用的对话模型。\n        auxiliary_models (`Optional[Dict[str, ChatModelBase]]`):\n            用于辅助的额外对话模型，一般用于多智能体场景下模拟其他非训练智能体的行为。\n\n    Returns:\n        `WorkflowOutput`: 工作流生成的输出。\n    \"\"\"\n    agent = ReActAgent(\n        name=\"react_agent\",\n        sys_prompt=\"你是一个善于解决数学问题的智能体。\",\n        model=model,\n        formatter=OpenAIChatFormatter(),\n    )\n\n    response = await agent.reply(\n        msg=Msg(\n            \"user\",\n            task[\"question\"],\n            role=\"user\",\n        ),  # 从任务中提取问题\n    )\n\n    return WorkflowOutput(  # 返回响应结果\n        response=response,\n    )\n\n\n# %%\n# 你可以直接用任务字典和日常调试使用的 ``DashScopeChatModel`` / ``OpenAIChatModel`` 运行此工作流函数，从而在正式训练前测试其流程的正确性。例如：\n\nimport asyncio\nimport os\nfrom agentscope.model import DashScopeChatModel\n\ntask = {\"question\": \"123 加 456 等于多少？\", \"answer\": \"579\"}\nmodel = DashScopeChatModel(\n    model_name=\"qwen-max\",\n    api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n)\nworkflow_output = asyncio.run(example_workflow_function(task, model))\nassert isinstance(\n    workflow_output.response,\n    Msg,\n), \"在此示例中，响应应为 Msg 实例。\"\nprint(\"\\n工作流响应:\", workflow_output.response.get_text_content())\n\n# %%\n#\n# 评判函数\n# --------------------\n# 评判函数用于评估智能体在特定任务上的表现，并为调优过程提供奖励信号。\n# 所有评判函数需遵循 ``agentscope.tuner.JudgeType`` 的输入/输出签名。\n# 下面是一个简单的评判函数示例，通过比较智能体响应与标准答案给出奖励：\n\nfrom typing import Any\nfrom agentscope.tuner import JudgeOutput\n\n\nasync def example_judge_function(\n    task: Dict,\n    response: Any,\n    auxiliary_models: Optional[Dict[str, ChatModelBase]] = None,\n) -> JudgeOutput:\n    \"\"\"仅用于演示的简单评判函数。\n\n    Args:\n        task (`Dict`): 任务信息。\n        response (`Any`): WorkflowOutput 的响应字段。\n        auxiliary_models (`Optional[Dict[str, ChatModelBase]]`):\n            用于 LLM-as-a-Judge 的辅助模型。\n    Returns:\n        `JudgeOutput`: 评判函数分配的奖励。\n    \"\"\"\n    ground_truth = task[\"answer\"]\n    reward = 1.0 if ground_truth in response.get_text_content() else 0.0\n    return JudgeOutput(reward=reward)\n\n\n# 本地测试函数的正确性：\njudge_output = asyncio.run(\n    example_judge_function(\n        task,\n        workflow_output.response,\n    ),\n)\nprint(f\"评判奖励: {judge_output.reward}\")\n\n# %%\n# 评判函数同样可以按照上述案例中展示的方式在正式训练前进行本地测试，以确保其逻辑正确。\n#\n# .. tip::\n#    你可以在评判函数中利用已有的 `MetricBase <https://github.com/agentscope-ai/agentscope/blob/main/src/agentscope/evaluate/_metric_base.py>`_ 实现，计算更复杂的指标，并将其组合为复合奖励。\n#\n# 配置并运行\n# ~~~~~~~~~~~~~~~\n# 最后，你可以用 ``tuner`` 模块配置并运行调优流程。\n# 在开始调优前，请确保环境已安装 `Trinity-RFT <https://github.com/agentscope-ai/Trinity-RFT>`_，这是 ``tuner`` 的依赖。\n#\n# 下面是调优流程的配置与启动示例：\n#\n# .. note::\n#    此示例仅供演示。完整可运行示例请参考 `Tune ReActAgent <https://github.com/agentscope-ai/agentscope/tree/main/examples/tuner/react_agent>`_\n#\n# .. code-block:: python\n#\n#        from agentscope.tuner import tune, AlgorithmConfig, DatasetConfig, TunerModelConfig\n#        # 你的工作流 / 评判函数 ...\n#\n#        if __name__ == \"__main__\":\n#            dataset = DatasetConfig(path=\"my_dataset\", split=\"train\")\n#            model = TunerModelConfig(model_path=\"Qwen/Qwen3-0.6B\", max_model_len=16384)\n#            algorithm = AlgorithmConfig(\n#                algorithm_type=\"multi_step_grpo\",\n#                group_size=8,\n#                batch_size=32,\n#                learning_rate=1e-6,\n#            )\n#            tune(\n#                workflow_func=example_workflow_function,\n#                judge_func=example_judge_function,\n#                model=model,\n#                train_dataset=dataset,\n#                algorithm=algorithm,\n#            )\n#\n# 这里用 ``DatasetConfig`` 配置训练数据集，用 ``TunerModelConfig`` 配置可训练模型相关参数，用 ``AlgorithmConfig`` 指定强化学习算法及其超参数。\n#\n# .. tip::\n#    ``tune`` 函数基于 `Trinity-RFT <https://github.com/agentscope-ai/Trinity-RFT>`_ 实现，内部会将输入参数转换为 YAML 配置。\n#    高级用户可忽略 ``model``、``train_dataset``、``algorithm`` 参数，直接通过 ``config_path`` 指定 YAML 配置文件。\n#    推荐使用配置文件方式以便更细粒度地控制训练过程，充分利用 Trinity-RFT 的高级功能。\n#    你可参考 Trinity-RFT 的 `配置指南 <https://agentscope-ai.github.io/Trinity-RFT/en/main/tutorial/trinity_configs.html>`_ 了解更多配置选项。\n#\n# 你可以将上述代码保存为 ``main.py``，并用如下命令运行：\n#\n# .. code-block:: bash\n#\n#        ray start --head\n#        python main.py\n#\n# 检查点和日志会自动保存到当前工作目录下的 ``checkpoints/AgentScope`` 目录，每次运行会以时间戳为后缀保存到子目录。\n# tensorboard 日志可在检查点目录下的 ``monitor/tensorboard`` 中找到。\n#\n# .. code-block:: text\n#\n#        your_workspace/\n#            └── checkpoints/\n#                └──AgentScope/\n#                    └── Experiment-20260104185355/  # 每次运行以时间戳保存\n#                        ├── monitor/\n#                        │   └── tensorboard/  # tensorboard 日志\n#                        └── global_step_x/    # 第 x 步保存的模型检查点\n#\n# .. tip::\n#    更多调优样例请参考 AgentScope-Samples 库中的 `tuner 目录 <https://github.com/agentscope-ai/agentscope-samples/tree/main/tuner>`_\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/workflow_concurrent_agents.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n\nConcurrent Agents\n===================================\n在异步编程的帮助下，多智能体并发可以通过 Python 中的 ``asyncio.gather`` 执行。\n\n下面展示了一个简单的示例，其中创建了两个智能体并并发执行。\n\"\"\"\nimport asyncio\nfrom datetime import datetime\nfrom typing import Any\n\nfrom agentscope.agent import AgentBase\n\n\nclass ExampleAgent(AgentBase):\n    \"\"\"用于并发执行的示例智能体。\"\"\"\n\n    def __init__(self, name: str) -> None:\n        \"\"\"使用智能体名称初始化智能体。\"\"\"\n        super().__init__()\n        self.name = name\n\n    async def reply(self, *args: Any, **kwargs: Any) -> None:\n        \"\"\"回复消息。\"\"\"\n        start_time = datetime.now().strftime(\"%H:%M:%S.%f\")[:-3]\n        print(f\"{self.name} 开始于 {start_time}\")\n        await asyncio.sleep(3)  # 模拟长时间运行的任务\n        end_time = datetime.now().strftime(\"%H:%M:%S.%f\")[:-3]\n        print(f\"{self.name} 结束于 {end_time}\")\n\n\nasync def run_concurrent_agents() -> None:\n    \"\"\"运行并发智能体。\"\"\"\n    agent1 = ExampleAgent(\"智能体 1\")\n    agent2 = ExampleAgent(\"智能体 2\")\n\n    await asyncio.gather(agent1(), agent2())\n\n\nasyncio.run(run_concurrent_agents())\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/workflow_conversation.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _conversation:\n\nConversation\n======================\n\nConversation 是一种智能体间交换和共享信息的设计模式，常见于游戏、聊天机器人和多智能体讨论场景。\n\n在 AgentScope 中，conversation 的构建在 **显式的消息传递** 基础上。在本章中，我们将演示如何构建：\n\n- User-assistant 之间的对话（聊天机器人）\n- 多实体对话（游戏、讨论等）\n\n它们的主要区别在于\n\n- **提示的构建方式**，以及\n- 信息在智能体之间的 **传播/共享** 方式。\n\"\"\"\nimport asyncio\nimport json\nimport os\n\nfrom agentscope.agent import ReActAgent, UserAgent\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.formatter import (\n    DashScopeChatFormatter,\n    DashScopeMultiAgentFormatter,\n)\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.message import Msg\nfrom agentscope.pipeline import MsgHub\nfrom agentscope.tool import Toolkit\n\n# %%\n# User-Assistant 对话\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# User-assistant 对话，也称为聊天机器人（chatbot），是最常见的智能体应用，也是当前大多数 LLM API 的设计模式。\n# 在这种对话只有两个参与者：用户（user）和智能体（assistant）。\n#\n# 在 AgentScope 中，名称中带有 **\"Chat\"** 的格式化器专为 user-assistant 对话设计，\n# 如 ``DashScopeChatFormatter``、``AnthropicChatFormatter`` 等。\n# 它们使用消息中的 ``role`` 字段来区分用户和智能体，并相应地格式化消息。\n#\n# 这里我们构建智能体 ``Friday`` 和用户之间的简单对话。\n#\n# .. tip:: AgentScope 提供了内置的 ``UserAgent`` 类，用于人机交互（HITL）。更多详细信息请参考 :ref:`user-agent`。\n#\n\nfriday = ReActAgent(\n    name=\"Friday\",\n    sys_prompt=\"你是一个名为 Friday 的有用助手\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n    ),\n    formatter=DashScopeChatFormatter(),  # 用于 user-assistant 对话的格式化器\n    memory=InMemoryMemory(),\n    toolkit=Toolkit(),\n)\n\n# 创建用户智能体\nuser = UserAgent(name=\"User\")\n\n# %%\n# 现在，我们可以通过在这两个智能体之间交换消息来开始对话，直到用户输入\"exit\"结束对话。\n#\n# .. code-block:: python\n#\n#     async def run_conversation() -> None:\n#         \"\"\"运行 Friday 和用户之间的简单对话。\"\"\"\n#         msg = None\n#         while True:\n#             msg = await friday(msg)\n#             msg = await user(msg)\n#             if msg.get_text_content() == \"exit\":\n#                 break\n#\n#     asyncio.run(run_conversation())\n#\n\n# %%\n# 多实体对话\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# 如开头所述，我们演示如何在 **提示构建** 和 **信息共享** 方面构建多智能体对话。\n#\n# 构建提示\n# -------------------------------\n# 在 AgentScope 中，我们为多智能体对话提供了内置格式化器，其名称中带有 **\"MultiAgent\"**，\n# 如 ``DashScopeMultiAgentFormatter``、``AnthropicMultiAgentFormatter`` 等。\n#\n# 具体而言，它们使用消息中的 ``name`` 字段来区分不同的实体，并将对话历史格式化为单个用户消息。\n# 以 ``DashScopeMultiAgentFormatter`` 为例：\n#\n# .. tip:: 有关格式化器的更多详细信息可以在 :ref:`prompt` 中找到。\n#\n\n\nasync def example_multi_agent_prompt() -> None:\n    msgs = [\n        Msg(\"system\", \"你是一个名为 Bob 的有用助手。\", \"system\"),\n        Msg(\"Alice\", \"嗨！\", \"user\"),\n        Msg(\"Bob\", \"嗨！很高兴见到大家。\", \"assistant\"),\n        Msg(\"Charlie\", \"我也是！顺便说一下，我是 Charlie。\", \"assistant\"),\n    ]\n\n    formatter = DashScopeMultiAgentFormatter()\n    prompt = await formatter.format(msgs)\n\n    print(\"格式化的提示：\")\n    print(json.dumps(prompt, indent=4, ensure_ascii=False))\n\n    # 我们在这里打印组合用户消息的内容以便更好地理解：\n    print(\"-------------\")\n    print(\"组合消息\")\n    print(prompt[1][\"content\"])\n\n\nasyncio.run(example_multi_agent_prompt())\n\n\n# %%\n# 消息共享\n# -------------------------------\n# 在多智能体对话中，显式交换消息可能不够高效和便利，\n# 特别是在多个智能体之间广播消息时。\n#\n# 因此，AgentScope 提供了一个名为 ``MsgHub`` 的异步上下文管理器来简化消息广播。\n# 具体而言，同一个 ``MsgHub`` 中的智能体将自动接收其它参与者通过 ``reply`` 函数返回的消息。\n#\n# 下面我们构建一个多人聊天的场景，多个智能体扮演不同的角色：\n#\n\nmodel = DashScopeChatModel(\n    model_name=\"qwen-max\",\n    api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n)\nformatter = DashScopeMultiAgentFormatter()\n\nalice = ReActAgent(\n    name=\"Alice\",\n    sys_prompt=\"你是一个名为 Alice 的学生。\",\n    model=model,\n    formatter=formatter,\n)\n\nbob = ReActAgent(\n    name=\"Bob\",\n    sys_prompt=\"你是一个名为 Bob 的学生。\",\n    model=model,\n    formatter=formatter,\n)\n\ncharlie = ReActAgent(\n    name=\"Charlie\",\n    sys_prompt=\"你是一个名为 Charlie 的学生。\",\n    model=model,\n    formatter=formatter,\n)\n\n\nasync def example_msghub() -> None:\n    \"\"\"使用 MsgHub 进行多智能体对话的示例。\"\"\"\n    async with MsgHub(\n        [alice, bob, charlie],\n        # 进入 MsgHub 时的公告消息\n        announcement=Msg(\n            \"system\",\n            \"现在大家互相认识一下，简单自我介绍。\",\n            \"system\",\n        ),\n    ):\n        await alice()\n        await bob()\n        await charlie()\n\n\nasyncio.run(example_msghub())\n\n# %%\n# 现在我们打印 Alice 的记忆，检查她的记忆是否正确更新。\n#\n\n\nasync def example_memory() -> None:\n    \"\"\"打印 Alice 的记忆。\"\"\"\n    print(\"Alice 的记忆：\")\n    for msg in await alice.memory.get_memory():\n        print(\n            f\"{msg.name}: {json.dumps(msg.content, indent=4, ensure_ascii=False)}\",\n        )\n\n\nasyncio.run(example_memory())\n\n# %%\n# 进一步阅读\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# - :ref:`prompt`\n# - :ref:`pipeline`\n#\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/workflow_handoffs.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _handoffs:\n\nHandoffs\n========================================\n\nHandoffs 是由 OpenAI 提出的工作流模式，通过调用子智能体的方式来完成目标任务。\n在 AgentScope 中通过工具调用的方式实现 handoffs 非常简单。首先，我们创建一个函数来允许协调者动态创建子智能体。\n\n.. figure:: ../../_static/images/handoffs.png\n   :width: 80%\n   :align: center\n   :alt: 协调者-子智能体工作流\n\n   *Handoffs 示例*\n\n\"\"\"\n\nimport asyncio\nimport os\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import (\n    ToolResponse,\n    Toolkit,\n    execute_python_code,\n)\n\n\n# 创建子智能体的工具函数\nasync def create_worker(\n    task_description: str,\n) -> ToolResponse:\n    \"\"\"创建一个子智能体来完成给定的任务。子智能体配备了 Python 执行工具。\n\n    Args:\n        task_description (``str``):\n            子智能体要完成的任务描述。\n    \"\"\"\n    # 为子智能体智能体配备一些工具\n    toolkit = Toolkit()\n    toolkit.register_tool_function(execute_python_code)\n\n    # 创建子智能体智能体\n    worker = ReActAgent(\n        name=\"Worker\",\n        sys_prompt=\"你是一个智能体。你的目标是完成给定的任务。\",\n        model=DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            stream=False,\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n    )\n    # 让子智能体完成任务\n    res = await worker(Msg(\"user\", task_description, \"user\"))\n    return ToolResponse(\n        content=res.get_content_blocks(\"text\"),\n    )\n\n\nasync def run_handoffs() -> None:\n    \"\"\"交接工作流示例。\"\"\"\n    # 初始化协调者智能体\n    toolkit = Toolkit()\n    toolkit.register_tool_function(create_worker)\n\n    orchestrator = ReActAgent(\n        name=\"Orchestrator\",\n        sys_prompt=\"你是一个协调者智能体。你的目标是通过将任务分解为更小的任务并创建子智能体来完成它们，从而完成给定的任务。\",\n        model=DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            stream=False,\n        ),\n        memory=InMemoryMemory(),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n    )\n\n    # 任务描述\n    task_description = \"在 Python 中执行 hello world\"\n\n    # 创建子智能体来完成任务\n    await orchestrator(Msg(\"user\", task_description, \"user\"))\n\n\nasyncio.run(run_handoffs())\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/workflow_multiagent_debate.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _multiagent-debate:\n\nMulti-Agent Debate\n========================\n\nMulti-Agent debate 模拟不同智能体之间的多轮讨论场景，通常包括几个 solver 和一个 aggregator。\n典型情况下，solver 生成并交换他们的答案，而 aggregator 收集并总结答案。\n\n我们实现了 `EMNLP 2024`_ 中的示例，其中两个 solver 智能体将按固定顺序讨论一个话题，根据先前的辩论历史表达他们的论点。\n在每一轮中，主持人智能体将决定是否可以在当前轮获得最终的正确答案。\n\"\"\"\nimport asyncio\nimport os\n\nfrom pydantic import Field, BaseModel\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import (\n    DashScopeMultiAgentFormatter,\n)\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.pipeline import MsgHub\n\n# 准备一个话题\ntopic = \"两个圆外切且没有相对滑动。圆A的半径是圆B半径的1/3。圆A绕圆B滚动一圈回到起点。圆A总共会旋转多少次？\"\n\n\n# 创建两个辩论者智能体，Alice 和 Bob，他们将讨论这个话题。\ndef create_solver_agent(name: str) -> ReActAgent:\n    \"\"\"获取一个解决者智能体。\"\"\"\n    return ReActAgent(\n        name=name,\n        sys_prompt=f\"你是一个名为 {name} 的辩论者。你好，欢迎来到\"\n        \"辩论比赛。我们的目标是找到正确答案，因此你没有必要完全同意对方\"\n        f\"的观点。辩论话题如下所述：{topic}\",\n        model=DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            stream=False,\n        ),\n        formatter=DashScopeMultiAgentFormatter(),\n    )\n\n\nalice, bob = [create_solver_agent(name) for name in [\"Alice\", \"Bob\"]]\n\n# 创建主持人智能体\nmoderator = ReActAgent(\n    name=\"Aggregator\",\n    sys_prompt=f\"\"\"你是一个主持人。将有两个辩论者参与辩论比赛。他们将就以下话题提出观点并进行讨论：\n``````\n{topic}\n``````\n在每轮讨论结束时，你将评估辩论是否结束，以及话题正确的答案。\"\"\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        stream=False,\n    ),\n    # 使用多智能体格式化器，因为主持人将接收来自多于用户和助手的消息\n    formatter=DashScopeMultiAgentFormatter(),\n)\n\n\n# 主持人的结构化输出模型\nclass JudgeModel(BaseModel):\n    \"\"\"主持人的结构化输出模型。\"\"\"\n\n    finished: bool = Field(description=\"辩论是否结束。\")\n    correct_answer: str | None = Field(\n        description=\"辩论话题的正确答案，仅当辩论结束时提供该字段。否则保留为 None。\",\n        default=None,\n    )\n\n\nasync def run_multiagent_debate() -> None:\n    \"\"\"运行多智能体辩论工作流。\"\"\"\n    while True:\n        # MsgHub 中参与者的回复消息将广播给所有参与者。\n        async with MsgHub(participants=[alice, bob, moderator]):\n            await alice(\n                Msg(\n                    \"user\",\n                    \"你是正方，请表达你的观点。\",\n                    \"user\",\n                ),\n            )\n            await bob(\n                Msg(\n                    \"user\",\n                    \"你是反方。你不同意正方的观点。请表达你的观点和理由。\",\n                    \"user\",\n                ),\n            )\n\n        # Alice 和 Bob 不需要知道主持人的消息，所以主持人在 MsgHub 外部调用。\n        msg_judge = await moderator(\n            Msg(\n                \"user\",\n                \"现在你已经听到了他们的辩论，现在判断辩论是否结束，以及你能得到正确答案吗？\",\n                \"user\",\n            ),\n            structured_model=JudgeModel,\n        )\n\n        if msg_judge.metadata.get(\"finished\"):\n            print(\n                \"\\n辩论结束，正确答案是：\",\n                msg_judge.metadata.get(\"correct_answer\"),\n            )\n            break\n\n\nasyncio.run(run_multiagent_debate())\n\n\n# %%\n# 进一步阅读\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n#\n# - :ref:`pipeline`\n#\n# .. _EMNLP 2024:\n# Encouraging Divergent Thinking in Large Language Models through Multi-Agent Debate. EMNLP 2024.\n#\n"
  },
  {
    "path": "docs/tutorial/zh_CN/src/workflow_routing.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n.. _routing:\n\nRouting\n==========================\n在 AgentScope 中有两种实现 Routing 的方法，都简单易实现：\n\n- 使用结构化输出的显式 routing\n- 使用工具调用的隐式 routing\n\n.. tip:: 考虑到智能体 routing 没有统一的标准/定义，我们遵循 `Building effective agents <https://www.anthropic.com/engineering/building-effective-agents>`_ 中的设置\n\n显式 Routing\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n在显式 routing 中，我们可以直接使用智能体的结构化输出来确定将消息路由到哪个智能体。\n\n初始化 routing 智能体\n\"\"\"\nimport asyncio\nimport json\nimport os\nfrom typing import Literal\n\nfrom pydantic import BaseModel, Field\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import Toolkit, ToolResponse\n\nrouter = ReActAgent(\n    name=\"Router\",\n    sys_prompt=\"你是一个路由智能体。你的目标是将用户查询路由到正确的后续任务，注意你不需要回答用户的问题。\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        stream=False,\n    ),\n    formatter=DashScopeChatFormatter(),\n)\n\n\n# 使用结构化输出指定路由任务\nclass RoutingChoice(BaseModel):\n    your_choice: Literal[\n        \"Content Generation\",\n        \"Programming\",\n        \"Information Retrieval\",\n        None,\n    ] = Field(\n        description=\"选择正确的后续任务，如果任务太简单或没有合适的任务，则选择 ``None``\",\n    )\n    task_description: str | None = Field(\n        description=\"任务描述\",\n        default=None,\n    )\n\n\nasync def example_router_explicit() -> None:\n    \"\"\"使用结构化输出进行显式路由的示例。\"\"\"\n    msg_user = Msg(\n        \"user\",\n        \"帮我写一首诗\",\n        \"user\",\n    )\n\n    # 路由查询\n    msg_res = await router(\n        msg_user,\n        structured_model=RoutingChoice,\n    )\n\n    # 结构化输出存储在 metadata 字段中\n    print(\"结构化输出：\")\n    print(json.dumps(msg_res.metadata, indent=4, ensure_ascii=False))\n\n\nasyncio.run(example_router_explicit())\n\n# %%\n# 隐式 Routing\n# ~~~~~~~~~~~~~~~~~~~~~~~~~\n# 另一种方法是将下游智能体包装成工具函数，这样路由智能体就可以根据用户查询决定调用哪个工具。\n#\n# 我们首先定义几个工具函数：\n#\n\n\nasync def generate_python(demand: str) -> ToolResponse:\n    \"\"\"根据需求生成 Python 代码。\n\n    Args:\n        demand (``str``):\n            对 Python 代码的需求。\n    \"\"\"\n    # 示例需求智能体\n    python_agent = ReActAgent(\n        name=\"PythonAgent\",\n        sys_prompt=\"你是一个 Python 专家，你的目标是根据需求生成 Python 代码。\",\n        model=DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            stream=False,\n        ),\n        memory=InMemoryMemory(),\n        formatter=DashScopeChatFormatter(),\n        toolkit=Toolkit(),\n    )\n    msg_res = await python_agent(Msg(\"user\", demand, \"user\"))\n\n    return ToolResponse(\n        content=msg_res.get_content_blocks(\"text\"),\n    )\n\n\n# 为演示目的模拟一些其他工具函数\nasync def generate_poem(demand: str) -> ToolResponse:\n    \"\"\"根据需求生成诗歌。\n\n    Args:\n        demand (``str``):\n            对诗歌的需求。\n    \"\"\"\n    pass\n\n\nasync def web_search(query: str) -> ToolResponse:\n    \"\"\"在网络上搜索查询。\n\n    Args:\n        query (``str``):\n            要搜索的查询。\n    \"\"\"\n    pass\n\n\n# %%\n# 之后，我们定义一个路由智能体并为其配备上述工具函数。\n#\n\ntoolkit = Toolkit()\ntoolkit.register_tool_function(generate_python)\ntoolkit.register_tool_function(generate_poem)\ntoolkit.register_tool_function(web_search)\n\n# 使用工具模块初始化路由智能体\nrouter_implicit = ReActAgent(\n    name=\"Router\",\n    sys_prompt=\"你是一个路由智能体。你的目标是将用户查询路由到正确的后续任务。\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        stream=False,\n    ),\n    formatter=DashScopeChatFormatter(),\n    toolkit=toolkit,\n    memory=InMemoryMemory(),\n)\n\n\nasync def example_router_implicit() -> None:\n    \"\"\"使用工具调用进行隐式路由的示例。\"\"\"\n    msg_user = Msg(\n        \"user\",\n        \"帮我在 Python 中生成一个快速排序函数\",\n        \"user\",\n    )\n\n    # 路由查询\n    await router_implicit(msg_user)\n\n\nasyncio.run(example_router_implicit())\n"
  },
  {
    "path": "examples/agent/a2a_agent/README.md",
    "content": "# Agent-to-Agent Protocol Example\n\nThe `A2AAgent` in AgentScope is an A2A client that connects to an external agent server via the Agent-to-Agent (A2A) protocol.\nThis example demonstrates how to set up and use the `A2AAgent` to interact with an agent hosted on an A2A server.\n\nNote the A2A feature is experimental and subject to change, and due to the limitations of A2A protocol, the `A2AAgent`\ncurrently\n\n1. only supports chatbot scenarios, where only a user and an agent are involved\n2. does not support realtime steering/interruption during the conversation\n3. does not support agentic structured outputs\n4. stores the observed messages locally and send them together with the input message(s) of the `reply` function\n\n## Files\n\nThe example contains the following files:\n\n```\nexamples/agent/a2a_agent\n├── main.py                  # The main script to run the A2A agent example\n├── setup_a2a_server.py      # The script to set up a simple A2A server\n├── agent_card.py            # The agent card definition for the A2A agent\n└── README.md                # This README file\n```\n\n## Setup\n\nThis example provides a simple setup to demonstrate how to use the `A2AAgent` in AgentScope.\nFirst you need to install the required dependencies:\n\n```bash\nuv pip install a2a-sdk[http-server] agentscope[a2a]\n#  or\npip install a2a-sdk[http-server] agentscope[a2a]\n```\n\nThen we first set up a simple A2A server that hosts a ReAct agent:\n```bash\nuvicorn setup_a2a_server:app --host 0.0.0.0 --port 8000\n```\nThis will start an A2A server locally on port 8000.\n\nAfter that, you can run the A2A agent example to run a chatbot conversation with the agent hosted on the A2A server:\n```bash\npython main.py\n```\n\n"
  },
  {
    "path": "examples/agent/a2a_agent/agent_card.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The agent card definition for the A2A agent.\"\"\"\nfrom a2a.types import AgentCard, AgentCapabilities, AgentSkill\n\nagent_card = AgentCard(\n    name=\"Friday\",\n    description=\"A simple ReAct agent that handles input queries\",\n    url=\"http://localhost:8000\",\n    version=\"1.0.0\",\n    capabilities=AgentCapabilities(\n        push_notifications=False,\n        state_transition_history=True,\n        streaming=True,\n    ),\n    default_input_modes=[\"text/plain\"],\n    default_output_modes=[\"text/plain\"],\n    skills=[\n        AgentSkill(\n            name=\"execute_python_code\",\n            id=\"execute_python_code\",\n            description=\"Execute Python code snippets.\",\n            tags=[\"code_execution\"],\n        ),\n        AgentSkill(\n            name=\"execute_shell_command\",\n            id=\"execute_shell_command\",\n            description=\"Execute shell commands on the server.\",\n            tags=[\"code_execution\"],\n        ),\n        AgentSkill(\n            name=\"view_text_file\",\n            id=\"view_text_file\",\n            description=\"View the content of a text file on the server.\",\n            tags=[\"file_viewing\"],\n        ),\n    ],\n)\n"
  },
  {
    "path": "examples/agent/a2a_agent/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The main entry for the A2A agent example.\"\"\"\nimport asyncio\n\nfrom agent_card import agent_card\n\nfrom agentscope.agent import UserAgent, A2AAgent\n\n\nasync def main() -> None:\n    \"\"\"The main entry for the example, where we build a simple conversation\n    between the A2A agent and the user.\"\"\"\n\n    user = UserAgent(\"user\")\n\n    agent = A2AAgent(\n        agent_card=agent_card,\n    )\n\n    msg = None\n    while True:\n        msg = await user(msg)\n        if msg.get_text_content() == \"exit\":\n            break\n        msg = await agent(msg)\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/agent/a2a_agent/setup_a2a_server.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Set up an A2A server with a ReAct agent to handle the input query\"\"\"\nimport os\nimport uuid\nfrom typing import AsyncGenerator, Any\n\nfrom agent_card import agent_card\n\nfrom a2a.server.events import Event\nfrom a2a.types import (\n    TaskStatus,\n    TaskState,\n    MessageSendParams,\n    TaskStatusUpdateEvent,\n)\nfrom a2a.server.apps import A2AStarletteApplication\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter, A2AChatFormatter\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.pipeline import stream_printing_messages\nfrom agentscope.session import JSONSession\nfrom agentscope.tool import (\n    Toolkit,\n    execute_python_code,\n    execute_shell_command,\n    view_text_file,\n)\n\n\nclass SimpleStreamHandler:\n    \"\"\"A simple request handler that handles the input query by an\n    ReAct agent.\"\"\"\n\n    async def on_message_send_stream(\n        self,  # pylint: disable=unused-argument\n        params: MessageSendParams,\n        *args: Any,\n        **kwargs: Any,\n    ) -> AsyncGenerator[Event, None]:\n        \"\"\"Handles the message_send method by the agent\n\n        Args:\n            params (`MessageSendParams`):\n                The parameters for sending the message.\n\n        Returns:\n            `AsyncGenerator[Event, None]`:\n                An asynchronous generator that yields task status update\n                events.\n        \"\"\"\n        task_id = params.message.task_id or uuid.uuid4().hex\n        context_id = params.message.context_id or \"default-context\"\n        # ============ Agent Logic ============\n\n        # Register the tool functions\n        toolkit = Toolkit()\n        toolkit.register_tool_function(execute_python_code)\n        toolkit.register_tool_function(execute_shell_command)\n        toolkit.register_tool_function(view_text_file)\n\n        # Create the agent instance\n        agent = ReActAgent(\n            name=\"Friday\",\n            sys_prompt=\"You're a helpful assistant named Friday.\",\n            model=DashScopeChatModel(\n                model_name=\"qwen-max\",\n                api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n            ),\n            formatter=DashScopeChatFormatter(),\n            toolkit=toolkit,\n        )\n\n        session = JSONSession(save_dir=\"./sessions\")\n        await session.load_session_state(\n            session_id=\"test-a2a-agent\",\n            agent=agent,\n        )\n\n        # Convert the A2A message to AgentScope Msg objects\n        formatter = A2AChatFormatter()\n        as_msg = await formatter.format_a2a_message(\n            name=\"Friday\",\n            message=params.message,\n        )\n\n        yield TaskStatusUpdateEvent(\n            task_id=task_id,\n            context_id=context_id,\n            status=TaskStatus(state=TaskState.working),\n            final=False,\n        )\n\n        async for msg, last in stream_printing_messages(\n            agents=[agent],\n            coroutine_task=agent(as_msg),\n        ):\n            # The A2A streaming response is one complete Message object rather\n            # than accumulated or incremental text\n            if last:\n                a2a_message = await formatter.format([msg])\n\n                yield TaskStatusUpdateEvent(\n                    task_id=task_id,\n                    context_id=context_id,\n                    status=TaskStatus(\n                        state=TaskState.working,\n                        message=a2a_message,\n                    ),\n                    final=False,\n                )\n\n        # Finish the task\n        yield TaskStatusUpdateEvent(\n            task_id=task_id,\n            context_id=context_id,\n            status=TaskStatus(state=TaskState.completed),\n            final=True,\n        )\n\n        await session.save_session_state(\n            session_id=\"test-a2a-agent\",\n            agent=agent,\n        )\n\n\nhandler = SimpleStreamHandler()\napp_instance = A2AStarletteApplication(\n    agent_card,\n    handler,\n)\napp = app_instance.build()\n"
  },
  {
    "path": "examples/agent/a2ui_agent/README.md",
    "content": "# A2UI in AgentScope\n\n[A2UI (Agent-to-Agent UI)](https://github.com/google/A2UI) is a protocol for agents to send\nstreaming, interactive user interfaces to clients. It enables LLMs to generate platform-agnostic,\ndeclarative UI definitions that clients can render progressively using native widget sets.\n\nIn this example, we demonstrate how to integrate A2UI into a ReAct agent in AgentScope. This\nimplementation is based on the official A2UI agent samples, adapted to use AgentScope's agent\nframework.\n\nSpecifically, we have:\n\n1. **Reimplemented the agent with AgentScope**: The agent part of the official A2UI samples has\n   been reimplemented using AgentScope's `ReActAgent`, providing a more familiar and integrated\n   development experience for AgentScope users.\n\n2. **Progressive schema and template exposure via skills**: To help the agent learn and generate\n   A2UI-compliant interfaces, we use AgentScope's skill system to progressively expose the A2UI\n   schema and UI templates. The agent can dynamically load these resources through the\n   `A2UI_response_generator` skill, enabling it to understand component definitions and learn from\n   example UI structures.\n\n## Note on External Dependencies\n\nThe following directories in this example contain content sourced from the [Google A2UI repository](https://github.com/google/A2UI):\n\n- **`samples/client/`**: A2UI client sample applications\n\n**NPM Package Status**: As of now, the A2UI client libraries (`@a2ui/lit` and `@a2ui/angular`) are **not yet published to NPM**. According to the [official A2UI client setup guide](https://a2ui.org/guides/client-setup/#renderers): \"The Lit client library is not yet published to NPM. Check back in the coming days.\"\n\nTherefore, these dependencies are currently included in this example repository using local file paths (e.g., `\"@a2ui/lit\": \"file:../../../../renderers/lit\"` in `package.json` files). This mirrors the approach used in the [official A2UI repository](https://github.com/google/A2UI), where the renderers and samples also use local file paths to reference each other. Additionally, the `copy-spec` task in `renderers/lit/package.json` copies files from the local `specification/` directory during the build process.\n\n**Future Plans**: Once those libraries are published to NPM, we plan to gradually migrate to using the official NPM packages and remove these locally included directories.\n\n## Quick Start\n\nDownload the a2ui and agentscope package to the same directory\n\n```bash\ngit clone https://github.com/google/A2UI.git\ngit clone -b main https://github.com/agentscope-ai/agentscope.git\n# copy the renders and specification directory to AgentScope/examples/agent/a2ui_agent\ncp -r A2UI/renderers AgentScope/examples/agent/a2ui_agent\ncp -r A2UI/specification AgentScope/examples/agent/a2ui_agent\n```\n\n\nThen, navigate to the client directory and run the restaurant finder demo:\n\n```bash\ncd AgentScope/examples/agent/a2ui_agent/samples/client/lit\nnpm run demo:restaurant\n```\n\nThis command will:\n- Install dependencies and build the A2UI renderer\n- Start the A2A server (AgentScope agent) for the restaurant finder\n- Launch the client application in your browser\n\n> Note:\n> - The example is built with DashScope chat model. Make sure to set your `DASHSCOPE_API_KEY`\n>   environment variable before running the demo.\n> - If you are using Qwen series models, we recommend using `qwen3-max` for better performance in\n>   generating A2UI-compliant JSON responses.\n> - Generating UI JSON responses may take some time, typically 1-2 minutes, as the agent needs to\n>   process the schema, examples, and generate complex UI structures.\n> - The demo uses the standard A2UI catalog. Custom catalog and inline catalog support are under\n>   development.\n\n## Roadmap\n\nAgentScope's main focus going forward will be on improving **How Agents Work** with A2UI. The\nworkflow we're working towards is:\n\n```\nUser Input → Agent Logic → LLM → A2UI JSON\n```\n\nOur optimization efforts will focus on:\n\n- **Agent Logic**: Improving how agents process inputs and orchestrate the generation of A2UI JSON\n  messages\n\n\n- **Handle user interactions from the client**: Enabling agents to properly process and respond to\n  user interactions from the client (such as button clicks, form submissions), treating them as new\n  user input to create a continuous interactive loop\n\n**Current approach**: The skill-based method we've implemented in this example is our first step\ntowards this goal. By using AgentScope's skill system to progressively expose the A2UI schema and\ntemplates, agents can learn to generate compliant UI structures. Future improvements will focus on\nstreamlining this process and making it more intuitive for developers to build A2UI-capable agents.\n\n**Next steps for Agent Logic improvement**\n\n- **Agent skills improvements**:\n  - Support flexible schema addition: Enable developers to easily add and customize schemas without\n    modifying core skill code\n  - Separate schema and examples into dedicated folders: Organize schema definitions and example\n    templates into distinct directories for better maintainability and clearer structure\n\n- **Context management in Memory for A2UI long context**:\n  - Currently, A2UI messages are extremely long, which makes multi-turn interactions inefficient\n    and degrades the quality of agent responses. We plan to implement better context management\n    strategies to handle these long-form messages and improve the quality of multi-turn conversations.\n\n- **Keep up with A2UI protocol updates**:\n  - We will follow A2UI protocol updates and make corresponding adjustments. For example, we plan to\n    support streaming UI JSON introduced in A2UI v0.9.\n\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/a2a_client.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"A2A client example demonstrating agent card fetching and message sending.\"\"\"\nimport logging\n\nfrom typing import Any\nfrom uuid import uuid4\n\nimport httpx\n\nfrom a2a.client import A2ACardResolver, A2AClient\nfrom a2a.types import (\n    AgentCard,\n    MessageSendParams,\n    SendMessageRequest,\n)\nfrom a2a.utils.constants import (\n    AGENT_CARD_WELL_KNOWN_PATH,\n    EXTENDED_AGENT_CARD_PATH,\n)\n\n\nasync def main() -> None:\n    \"\"\"Main function demonstrating A2A client usage.\"\"\"\n    # Configure logging to show INFO level messages\n    logging.basicConfig(level=logging.INFO)\n    logger = logging.getLogger(__name__)  # Get a logger instance\n\n    # --8<-- [start:A2ACardResolver]\n\n    base_url = \"http://localhost:10002\"\n\n    async with httpx.AsyncClient(\n        timeout=httpx.Timeout(timeout=300.0),\n    ) as httpx_client:\n        # Initialize A2ACardResolver\n        resolver = A2ACardResolver(\n            httpx_client=httpx_client,\n            base_url=base_url,\n            # agent_card_path uses default,\n            # extended_agent_card_path also uses default\n        )\n        # --8<-- [end:A2ACardResolver]\n\n        # Fetch Public Agent Card and Initialize Client\n        final_agent_card_to_use: AgentCard | None = None\n\n        try:\n            logger.info(\n                \"Attempting to fetch public agent card from: %s%s\",\n                base_url,\n                AGENT_CARD_WELL_KNOWN_PATH,\n            )\n            _public_card = (\n                await resolver.get_agent_card()\n            )  # Fetches from default public path\n            logger.info(\"Successfully fetched public agent card:\")\n            logger.info(\n                _public_card.model_dump_json(indent=2, exclude_none=True),\n            )\n            final_agent_card_to_use = _public_card\n            logger.info(\n                \"\\nUsing PUBLIC agent card for client initialization \"\n                \"(default).\",\n            )\n\n            if _public_card.supports_authenticated_extended_card:\n                try:\n                    logger.info(\n                        \"\\nPublic card supports authenticated extended card. \"\n                        \"Attempting to fetch from: %s%s\",\n                        base_url,\n                        EXTENDED_AGENT_CARD_PATH,\n                    )\n                    auth_headers_dict = {\n                        \"Authorization\": (\n                            \"Bearer dummy-token-for-extended-card\"\n                        ),\n                    }\n                    _extended_card = await resolver.get_agent_card(\n                        relative_card_path=EXTENDED_AGENT_CARD_PATH,\n                        http_kwargs={\"headers\": auth_headers_dict},\n                    )\n                    logger.info(\n                        \"Successfully fetched authenticated extended \"\n                        \"agent card:\",\n                    )\n                    logger.info(\n                        _extended_card.model_dump_json(\n                            indent=2,\n                            exclude_none=True,\n                        ),\n                    )\n                    final_agent_card_to_use = (\n                        _extended_card  # Update to use the extended card\n                    )\n                    logger.info(\n                        \"\\nUsing AUTHENTICATED EXTENDED agent card \"\n                        \"for client initialization.\",\n                    )\n                except Exception as e_extended:\n                    logger.warning(\n                        \"Failed to fetch extended agent card: %s. \"\n                        \"Will proceed with public card.\",\n                        e_extended,\n                        exc_info=True,\n                    )\n            elif (\n                _public_card\n            ):  # supports_authenticated_extended_card is False or None\n                logger.info(\n                    \"\\nPublic card does not indicate support for an \"\n                    \"extended card. Using public card.\",\n                )\n\n        except Exception as e:\n            logger.error(\n                \"Critical error fetching public agent card: %s\",\n                e,\n                exc_info=True,\n            )\n            raise RuntimeError(\n                \"Failed to fetch the public agent card. Cannot continue.\",\n            ) from e\n\n        # --8<-- [start:send_message]\n        client = A2AClient(\n            httpx_client=httpx_client,\n            agent_card=final_agent_card_to_use,\n        )\n        logger.info(\"A2AClient initialized.\")\n\n        send_message_payload: dict[str, Any] = {\n            \"message\": {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"kind\": \"text\",\n                        \"text\": \"find top 5 chinese restaurants in new york\",\n                    },\n                ],\n                \"messageId\": uuid4().hex,\n            },\n        }\n        request = SendMessageRequest(\n            id=str(uuid4()),\n            params=MessageSendParams(**send_message_payload),\n        )\n\n        response = await client.send_message(request)\n        print(response.model_dump(mode=\"json\", exclude_none=True))\n\n\nif __name__ == \"__main__\":\n    import asyncio\n\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/README.md",
    "content": "# A2UI Generator\n\nThis is a UI to generate and visualize A2UI responses.\n\n## Prerequisites\n\n1. [nodejs](https://nodejs.org/en)\n\n## Running\n\nThis sample depends on the Lit renderer. Before running this sample, you need to build the renderer.\n\n1. **Build the renderer:**\n   ```bash\n   cd ../../../renderers/lit\n   npm install\n   npm run build\n   ```\n\n2. **Run this sample:**\n   ```bash\n   cd - # back to the sample directory\n   npm install\n   ```\n\n3. **Run the servers:**\n   - Run the [A2A server](../../../agent/adk/contact_lookup/)\n   - Run the dev server: `npm run dev`\n\nAfter starting the dev server, you can open http://localhost:5173/ to view the sample."
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/client.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { v0_8 } from \"@a2ui/lit\";\nimport { registerContactComponents } from \"./ui/custom-components/register-components.js\";\ntype A2TextPayload = {\n  kind: \"text\";\n  text: string;\n};\n\ntype A2DataPayload = {\n  kind: \"data\";\n  data: v0_8.Types.ServerToClientMessage;\n};\n\ntype A2AServerPayload =\n  | Array<A2DataPayload | A2TextPayload>\n  | { error: string };\n\nexport class A2UIClient {\n  #ready: Promise<void> = Promise.resolve();\n  get ready() {\n    return this.#ready;\n  }\n\n  async send(\n    message: v0_8.Types.A2UIClientEventMessage\n  ): Promise<v0_8.Types.ServerToClientMessage[]> {\n    const response = await fetch(\"/a2a\", {\n      body: JSON.stringify(message),\n      method: \"POST\",\n    });\n\n    if (response.ok) {\n      const data = (await response.json()) as A2AServerPayload;\n      const messages: v0_8.Types.ServerToClientMessage[] = [];\n      if (\"error\" in data) {\n        throw new Error(data.error);\n      } else {\n        for (const item of data) {\n          if (item.kind === \"text\") continue;\n          messages.push(item.data);\n        }\n      }\n      return messages;\n    }\n\n    const error = (await response.json()) as { error: string };\n    throw new Error(error.error);\n  }\n}\nregisterContactComponents();\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/contact.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { SignalWatcher } from \"@lit-labs/signals\";\nimport { provide } from \"@lit/context\";\nimport {\n  LitElement,\n  html,\n  css,\n  nothing,\n  HTMLTemplateResult,\n  unsafeCSS,\n} from \"lit\";\nimport { customElement, state } from \"lit/decorators.js\";\nimport { theme as uiTheme } from \"./theme/theme.js\";\nimport { A2UIClient } from \"./client.js\";\nimport {\n  SnackbarAction,\n  SnackbarMessage,\n  SnackbarUUID,\n  SnackType,\n} from \"./types/types.js\";\nimport { type Snackbar } from \"./ui/snackbar.js\";\nimport { repeat } from \"lit/directives/repeat.js\";\nimport { v0_8 } from \"@a2ui/lit\";\nimport * as UI from \"@a2ui/lit/ui\";\n\n// Demo elements.\nimport \"./ui/ui.js\";\nimport { registerContactComponents } from \"./ui/custom-components/register-components.js\";\n\n// Register custom components for the contact app\nregisterContactComponents();\n\n@customElement(\"a2ui-contact\")\nexport class A2UIContactFinder extends SignalWatcher(LitElement) {\n  @provide({ context: UI.Context.themeContext })\n  accessor theme: v0_8.Types.Theme = uiTheme;\n\n  @state()\n  accessor #requesting = false;\n\n  @state()\n  accessor #error: string | null = null;\n\n  @state()\n  accessor #lastMessages: v0_8.Types.ServerToClientMessage[] = [];\n\n  static styles = [\n    unsafeCSS(v0_8.Styles.structuralStyles),\n    css`\n      :host {\n        display: block;\n        max-width: 640px;\n        margin: 0 auto;\n        min-height: 100%;\n      }\n\n      #surfaces {\n        display: flex;\n        flex-direction: column;\n        width: 100%;\n        padding: var(--bb-grid-size-3) 0;\n        animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 0.3s backwards;\n\n        & a2ui-surface {\n          align-items: center;\n        }\n      }\n\n      form {\n        display: flex;\n        flex-direction: column;\n        flex: 1;\n        gap: 16px;\n        align-items: center;\n        padding: 16px 0;\n        animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 1s backwards;\n\n        & > div {\n          display: flex;\n          flex: 1;\n          gap: 16px;\n          align-items: center;\n          width: 100%;\n\n          & > input {\n            display: block;\n            flex: 1;\n            border-radius: 32px;\n            padding: 16px 24px;\n            border: 1px solid var(--p-60);\n            font-size: 16px;\n          }\n\n          & > button {\n            display: flex;\n            align-items: center;\n            background: var(--p-40);\n            color: var(--n-100);\n            border: none;\n            padding: 8px 16px;\n            border-radius: 32px;\n            opacity: 0.5;\n\n            &:not([disabled]) {\n              cursor: pointer;\n              opacity: 1;\n            }\n          }\n        }\n      }\n\n      .rotate {\n        animation: rotate 1s linear infinite;\n      }\n\n      .pending {\n        width: 100%;\n        min-height: 200px;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 0.3s backwards;\n\n        & .g-icon {\n          margin-right: 8px;\n        }\n      }\n\n      .error {\n        color: var(--e-40);\n        background-color: var(--e-95);\n        border: 1px solid var(--e-80);\n        padding: 16px;\n        border-radius: 8px;\n      }\n\n      @keyframes fadeIn {\n        from {\n          opacity: 0;\n        }\n\n        to {\n          opacity: 1;\n        }\n      }\n\n      @keyframes rotate {\n        from {\n          rotate: 0deg;\n        }\n\n        to {\n          rotate: 360deg;\n        }\n      }\n    `,\n  ];\n\n  #processor = v0_8.Data.createSignalA2uiMessageProcessor();\n  #a2uiClient = new A2UIClient();\n  #snackbar: Snackbar | undefined = undefined;\n  #pendingSnackbarMessages: Array<{\n    message: SnackbarMessage;\n    replaceAll: boolean;\n  }> = [];\n\n  render() {\n    return [\n      this.#maybeRenderForm(),\n      this.#maybeRenderData(),\n      this.#maybeRenderError(),\n    ];\n  }\n\n  #maybeRenderError() {\n    if (!this.#error) return nothing;\n\n    return html`<div class=\"error\">${this.#error}</div>`;\n  }\n\n  #maybeRenderForm() {\n    if (this.#requesting) return nothing;\n    if (this.#lastMessages.length > 0) return nothing;\n    return html`<form\n      @submit=${async (evt: Event) => {\n        evt.preventDefault();\n        if (!(evt.target instanceof HTMLFormElement)) {\n          return;\n        }\n        const data = new FormData(evt.target);\n        const body = data.get(\"body\") ?? null;\n        if (!body) {\n          return;\n        }\n        const message = body as v0_8.Types.A2UIClientEventMessage;\n        await this.#sendAndProcessMessage(message);\n      }}\n    >\n      <h1 class=\"typography-f-sf typography-v-r typography-w-400 color-c-p30\">\n        Contact Finder\n      </h1>\n      <div>\n        <input\n          required\n          value=\"Casey Smith\"\n          autocomplete=\"off\"\n          id=\"body\"\n          name=\"body\"\n          type=\"text\"\n          ?disabled=${this.#requesting}\n        />\n        <button type=\"submit\" ?disabled=${this.#requesting}>\n          <span class=\"g-icon filled-heavy\">send</span>\n        </button>\n      </div>\n    </form>`;\n  }\n\n  #maybeRenderData() {\n    if (this.#requesting) {\n      return html` <div class=\"pending\">\n        <span class=\"g-icon filled-heavy rotate\">progress_activity</span>\n        Awaiting an answer...\n      </div>`;\n    }\n\n    const surfaces = this.#processor.getSurfaces();\n    if (surfaces.size === 0) {\n      return nothing;\n    }\n\n    return html`<section id=\"surfaces\">\n      ${repeat(\n      this.#processor.getSurfaces(),\n      ([surfaceId]) => surfaceId,\n      ([surfaceId, surface]) => {\n        return html`<a2ui-surface\n              @a2uiaction=${async (\n          evt: v0_8.Events.StateEvent<\"a2ui.action\">\n        ) => {\n            const [target] = evt.composedPath();\n            if (!(target instanceof HTMLElement)) {\n              return;\n            }\n\n            const context: v0_8.Types.A2UIClientEventMessage[\"userAction\"][\"context\"] =\n              {};\n            if (evt.detail.action.context) {\n              const srcContext = evt.detail.action.context;\n              for (const item of srcContext) {\n                if (item.value.literalBoolean) {\n                  context[item.key] = item.value.literalBoolean;\n                } else if (item.value.literalNumber) {\n                  context[item.key] = item.value.literalNumber;\n                } else if (item.value.literalString) {\n                  context[item.key] = item.value.literalString;\n                } else if (item.value.path) {\n                  const path = this.#processor.resolvePath(\n                    item.value.path,\n                    evt.detail.dataContextPath\n                  );\n                  const value = this.#processor.getData(\n                    evt.detail.sourceComponent,\n                    path,\n                    surfaceId\n                  );\n                  context[item.key] = value;\n                }\n              }\n            }\n\n            const message: v0_8.Types.A2UIClientEventMessage = {\n              userAction: {\n                surfaceId: surfaceId,\n                name: evt.detail.action.name,\n                sourceComponentId: target.id,\n                timestamp: new Date().toISOString(),\n                context,\n              },\n            };\n\n            await this.#sendAndProcessMessage(message);\n          }}\n              .surfaceId=${surfaceId}\n              .surface=${surface}\n              .processor=${this.#processor}\n            ></a2-uisurface>`;\n      }\n    )}\n    </section>`;\n  }\n\n  async #sendAndProcessMessage(request) {\n    const messages = await this.#sendMessage(request);\n\n    this.#lastMessages = messages;\n    this.#processor.clearSurfaces();\n    this.#processor.processMessages(messages);\n  }\n\n  async #sendMessage(\n    message: v0_8.Types.A2UIClientEventMessage\n  ): Promise<v0_8.Types.ServerToClientMessage[]> {\n    try {\n      this.#requesting = true;\n      const response = this.#a2uiClient.send(message);\n      await response;\n      this.#requesting = false;\n\n      return response;\n    } catch (err) {\n      this.snackbar(err as string, SnackType.ERROR);\n    } finally {\n      this.#requesting = false;\n    }\n\n    return [];\n  }\n\n  snackbar(\n    message: string | HTMLTemplateResult,\n    type: SnackType,\n    actions: SnackbarAction[] = [],\n    persistent = false,\n    id = globalThis.crypto.randomUUID(),\n    replaceAll = false\n  ) {\n    if (!this.#snackbar) {\n      this.#pendingSnackbarMessages.push({\n        message: {\n          id,\n          message,\n          type,\n          persistent,\n          actions,\n        },\n        replaceAll,\n      });\n      return;\n    }\n\n    return this.#snackbar.show(\n      {\n        id,\n        message,\n        type,\n        persistent,\n        actions,\n      },\n      replaceAll\n    );\n  }\n\n  unsnackbar(id?: SnackbarUUID) {\n    if (!this.#snackbar) {\n      return;\n    }\n\n    this.#snackbar.hide(id);\n  }\n}\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/events/events.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { HTMLTemplateResult } from \"lit\";\n\nconst eventInit = {\n  bubbles: true,\n  cancelable: true,\n  composed: true,\n};\n\nexport class SnackbarActionEvent extends Event {\n  static eventName = \"snackbaraction\";\n\n  constructor(\n    public readonly action: string,\n    public readonly value?: HTMLTemplateResult | string,\n    public readonly callback?: () => void\n  ) {\n    super(SnackbarActionEvent.eventName, { ...eventInit });\n  }\n}\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/index.html",
    "content": "<!DOCTYPE html>\n<!--\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Contact Finder Agent</title>\n\n    <link\n      rel=\"stylesheet\"\n      href=\"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&icon_names=account_circle,add,arrow_back,arrow_drop_down,arrow_forward,attach_file,calendar_today,call,camera,check,check_circle,close,communication,content_copy,delete,download,draw,edit,error,event,favorite,favorite_off,folder,help,home,info,location_on,lock,lock_open,mail,menu,mobile_layout,more_horiz,more_vert,notifications,notifications_off,payment,pen_size_1,person,phone,photo,print,progress_activity,rectangle,refresh,search,send,settings,share,shopping_cart,star,star_half,star_off,upload,visibility,visibility_off,warning\"\n    />\n\n    <style>\n      :root {\n        --n-100: #ffffff;\n        --n-99: #fcfcfc;\n        --n-98: #f9f9f9;\n        --n-95: #f1f1f1;\n        --n-90: #e2e2e2;\n        --n-80: #c6c6c6;\n        --n-70: #ababab;\n        --n-60: #919191;\n        --n-50: #777777;\n        --n-40: #5e5e5e;\n        --n-35: #525252;\n        --n-30: #474747;\n        --n-25: #3b3b3b;\n        --n-20: #303030;\n        --n-15: #262626;\n        --n-10: #1b1b1b;\n        --n-5: #111111;\n        --n-0: #000000;\n\n        --p-100: #ffffff;\n        --p-99: #fffbff;\n        --p-98: #fcf8ff;\n        --p-95: #f2efff;\n        --p-90: #e1e0ff;\n        --p-80: #c0c1ff;\n        --p-70: #a0a3ff;\n        --p-60: #8487ea;\n        --p-50: #6a6dcd;\n        --p-40: #5154b3;\n        --p-35: #4447a6;\n        --p-30: #383b99;\n        --p-25: #2c2e8d;\n        --p-20: #202182;\n        --p-15: #131178;\n        --p-10: #06006c;\n        --p-5: #03004d;\n        --p-0: #000000;\n\n        --s-100: #ffffff;\n        --s-99: #fffbff;\n        --s-98: #fcf8ff;\n        --s-95: #f2efff;\n        --s-90: #e2e0f9;\n        --s-80: #c6c4dd;\n        --s-70: #aaa9c1;\n        --s-60: #8f8fa5;\n        --s-50: #75758b;\n        --s-40: #5d5c72;\n        --s-35: #515165;\n        --s-30: #454559;\n        --s-25: #393a4d;\n        --s-20: #2e2f42;\n        --s-15: #242437;\n        --s-10: #191a2c;\n        --s-5: #0f0f21;\n        --s-0: #000000;\n\n        --t-100: #ffffff;\n        --t-99: #fffbff;\n        --t-98: #fff8f9;\n        --t-95: #ffecf4;\n        --t-90: #ffd8ec;\n        --t-80: #e9b9d3;\n        --t-70: #cc9eb8;\n        --t-60: #af849d;\n        --t-50: #946b83;\n        --t-40: #79536a;\n        --t-35: #6c475d;\n        --t-30: #5f3c51;\n        --t-25: #523146;\n        --t-20: #46263a;\n        --t-15: #3a1b2f;\n        --t-10: #2e1125;\n        --t-5: #22071a;\n        --t-0: #000000;\n\n        --nv-100: #ffffff;\n        --nv-99: #fffbff;\n        --nv-98: #fcf8ff;\n        --nv-95: #f2effa;\n        --nv-90: #e4e1ec;\n        --nv-80: #c8c5d0;\n        --nv-70: #acaab4;\n        --nv-60: #918f9a;\n        --nv-50: #777680;\n        --nv-40: #5e5d67;\n        --nv-35: #52515b;\n        --nv-30: #46464f;\n        --nv-25: #3b3b43;\n        --nv-20: #303038;\n        --nv-15: #25252d;\n        --nv-10: #1b1b23;\n        --nv-5: #101018;\n        --nv-0: #000000;\n\n        --e-100: #ffffff;\n        --e-99: #fffbff;\n        --e-98: #fff8f7;\n        --e-95: #ffedea;\n        --e-90: #ffdad6;\n        --e-80: #ffb4ab;\n        --e-70: #ff897d;\n        --e-60: #ff5449;\n        --e-50: #de3730;\n        --e-40: #ba1a1a;\n        --e-35: #a80710;\n        --e-30: #93000a;\n        --e-25: #7e0007;\n        --e-20: #690005;\n        --e-15: #540003;\n        --e-10: #410002;\n        --e-5: #2d0001;\n        --e-0: #000000;\n\n        --primary: #137fec;\n        --text-color: #fff;\n        --background-light: #f6f7f8;\n        --background-dark: #101922;\n        --border-color: oklch(\n          from var(--background-light) l c h / calc(alpha * 0.15)\n        );\n        --elevated-background-light: oklch(\n          from var(--background-light) l c h / calc(alpha * 0.05)\n        );\n        --bb-grid-size: 4px;\n        --bb-grid-size-2: calc(var(--bb-grid-size) * 2);\n        --bb-grid-size-3: calc(var(--bb-grid-size) * 3);\n        --bb-grid-size-4: calc(var(--bb-grid-size) * 4);\n        --bb-grid-size-5: calc(var(--bb-grid-size) * 5);\n        --bb-grid-size-6: calc(var(--bb-grid-size) * 6);\n        --bb-grid-size-7: calc(var(--bb-grid-size) * 7);\n        --bb-grid-size-8: calc(var(--bb-grid-size) * 8);\n        --bb-grid-size-9: calc(var(--bb-grid-size) * 9);\n        --bb-grid-size-10: calc(var(--bb-grid-size) * 10);\n        --bb-grid-size-11: calc(var(--bb-grid-size) * 11);\n        --bb-grid-size-12: calc(var(--bb-grid-size) * 12);\n        --bb-grid-size-13: calc(var(--bb-grid-size) * 13);\n        --bb-grid-size-14: calc(var(--bb-grid-size) * 14);\n        --bb-grid-size-15: calc(var(--bb-grid-size) * 15);\n        --bb-grid-size-16: calc(var(--bb-grid-size) * 16);\n      }\n\n      * {\n        box-sizing: border-box;\n      }\n\n      html,\n      body {\n        --font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n        --font-family-flex: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n        --font-family-mono: monospace;\n\n        background: var(--background-light);\n        font-family: var(--font-family);\n        margin: 0;\n        padding: 0;\n        width: 100svw;\n        height: 100svh;\n      }\n    </style>\n  </head>\n  <body>\n    <a2ui-contact></a2ui-contact>\n    <script src=\"./contact.ts\" type=\"module\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/middleware/a2a.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { IncomingMessage, ServerResponse } from \"http\";\nimport { Plugin, ViteDevServer } from \"vite\";\nimport { A2AClient } from \"@a2a-js/sdk/client\";\nimport {\n  MessageSendParams,\n  Part,\n  SendMessageSuccessResponse,\n  Task,\n} from \"@a2a-js/sdk\";\nimport { v4 as uuidv4 } from \"uuid\";\n\nconst A2AUI_MIME_TYPE = \"application/json+a2aui\";\n\nconst fetchWithCustomHeader: typeof fetch = async (url, init) => {\n  const headers = new Headers(init?.headers);\n  headers.set(\"X-A2A-Extensions\", \"https://a2ui.org/a2a-extension/a2ui/v0.8\");\n\n  const newInit = { ...init, headers };\n  return fetch(url, newInit);\n};\n\nconst isJson = (str: string) => {\n  try {\n    const parsed = JSON.parse(str);\n    return (\n      typeof parsed === \"object\" && parsed !== null && !Array.isArray(parsed)\n    );\n  } catch (err) {\n    console.warn(err);\n    return false;\n  }\n};\n\nlet client: A2AClient | null = null;\nconst createOrGetClient = async () => {\n  if (!client) {\n    // Create a client pointing to the agent's Agent Card URL.\n    client = await A2AClient.fromCardUrl(\n      \"http://localhost:10002/.well-known/agent-card.json\",\n      { fetchImpl: fetchWithCustomHeader }\n    );\n  }\n\n  return client;\n};\n\nexport const plugin = (): Plugin => {\n  return {\n    name: \"a2a-handler\",\n    configureServer(server: ViteDevServer) {\n      server.middlewares.use(\n        \"/a2a\",\n        async (req: IncomingMessage, res: ServerResponse, next: () => void) => {\n          if (req.method === \"POST\") {\n            let originalBody = \"\";\n\n            req.on(\"data\", (chunk) => {\n              originalBody += chunk.toString();\n            });\n\n            req.on(\"end\", async () => {\n              let sendParams: MessageSendParams;\n\n              if (isJson(originalBody)) {\n                console.log(\n                  \"[a2a-middleware] Received JSON UI event:\",\n                  originalBody\n                );\n\n                const clientEvent = JSON.parse(originalBody);\n                sendParams = {\n                  message: {\n                    messageId: uuidv4(),\n                    role: \"user\",\n                    parts: [\n                      {\n                        kind: \"data\",\n                        data: clientEvent,\n                        metadata: { 'mimeType': A2AUI_MIME_TYPE },\n                      } as Part,\n                    ],\n                    kind: \"message\",\n                  },\n                };\n              } else {\n                console.log(\n                  \"[a2a-middleware] Received text query:\",\n                  originalBody\n                );\n                sendParams = {\n                  message: {\n                    messageId: uuidv4(),\n                    role: \"user\",\n                    parts: [\n                      {\n                        kind: \"text\",\n                        text: originalBody,\n                      },\n                    ],\n                    kind: \"message\",\n                  },\n                };\n              }\n\n              const client = await createOrGetClient();\n              const response = await client.sendMessage(sendParams);\n              if (\"error\" in response) {\n                console.error(\"Error:\", response.error.message);\n                res.statusCode = 500;\n                res.setHeader(\"Content-Type\", \"application/json\");\n                res.end(JSON.stringify({ error: response.error.message }));\n                return;\n              } else {\n                const result = (response as SendMessageSuccessResponse)\n                  .result as Task;\n                if (result.kind === \"task\") {\n                  res.statusCode = 200;\n                  res.setHeader(\"Content-Type\", \"application/json\");\n                  res.end(JSON.stringify(result.status.message?.parts));\n                  return;\n                }\n              }\n\n              res.statusCode = 200;\n              res.setHeader(\"Content-Type\", \"application/json\");\n              res.end(JSON.stringify([]));\n            });\n\n            return;\n          } else {\n            next();\n          }\n        }\n      );\n    },\n  };\n};\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/middleware/index.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nexport * as A2AMiddleware from \"./a2a.js\";\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/package.json",
    "content": "{\n  \"name\": \"@a2ui/contact\",\n  \"private\": true,\n  \"version\": \"0.8.1\",\n  \"description\": \"A2UI Contact Demo\",\n  \"main\": \"./dist/contact.js\",\n  \"types\": \"./dist/contact.d.ts\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"prepack\": \"npm run build\",\n    \"build\": \"wireit\",\n    \"build:tsc\": \"wireit\",\n    \"dev\": \"npm run serve --watch\",\n    \"test\": \"wireit\",\n    \"serve\": \"wireit\"\n  },\n  \"wireit\": {\n    \"serve\": {\n      \"command\": \"vite dev\",\n      \"dependencies\": [\n        \"build\"\n      ],\n      \"service\": true\n    },\n    \"test\": {\n      \"command\": \"node --test --enable-source-maps --test-reporter spec dist/src/0.8/tests/**/*.test.js\",\n      \"dependencies\": [\n        \"build\"\n      ]\n    },\n    \"build\": {\n      \"dependencies\": [\n        \"build:tsc\"\n      ]\n    },\n    \"build:tsc\": {\n      \"command\": \"tsc -b --pretty\",\n      \"env\": {\n        \"FORCE_COLOR\": \"1\"\n      },\n      \"dependencies\": [\n        \"../../../../renderers/lit:build:tsc\"\n      ],\n      \"files\": [\n        \"**/*.ts\",\n        \"tsconfig.json\"\n      ],\n      \"output\": [\n        \"dist/\",\n        \"!dist/**/*.min.js{,.map}\"\n      ],\n      \"clean\": \"if-file-deleted\"\n    }\n  },\n  \"repository\": {\n    \"directory\": \"samples/client/lit/contact\",\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/google/A2UI.git\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"keywords\": [],\n  \"author\": \"Google\",\n  \"license\": \"Apache-2.0\",\n  \"bugs\": {\n    \"url\": \"https://github.com/google/A2UI/issues\"\n  },\n  \"homepage\": \"https://github.com/google/A2UI/tree/main/web#readme\",\n  \"devDependencies\": {\n    \"dotenv\": \"^17.2.3\",\n    \"typescript\": \"^5.8.3\",\n    \"uuid\": \"^13.0.0\",\n    \"vite\": \"^7.1.11\",\n    \"wireit\": \"^0.15.0-pre.2\"\n  },\n  \"dependencies\": {\n    \"@a2a-js/sdk\": \"^0.3.4\",\n    \"@a2ui/lit\": \"file:../../../../renderers/lit\",\n    \"@lit-labs/signals\": \"^0.1.3\",\n    \"@lit/context\": \"^1.1.4\",\n    \"lit\": \"^3.3.1\"\n  }\n}\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/theme/theme.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { v0_8 } from \"@a2ui/lit\";\n\n/** Elements */\n\nconst a = {\n  \"typography-f-sf\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-500\": true,\n  \"layout-as-n\": true,\n  \"layout-dis-iflx\": true,\n  \"layout-al-c\": true,\n};\n\nconst audio = {\n  \"layout-w-100\": true,\n};\n\nconst body = {\n  \"typography-f-s\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-400\": true,\n  \"layout-mt-0\": true,\n  \"layout-mb-2\": true,\n  \"typography-sz-bm\": true,\n  \"color-c-n10\": true,\n};\n\nconst button = {\n  \"typography-f-sf\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-500\": true,\n  \"layout-pt-3\": true,\n  \"layout-pb-3\": true,\n  \"layout-pl-5\": true,\n  \"layout-pr-5\": true,\n  \"layout-mb-1\": true,\n  \"border-br-16\": true,\n  \"border-bw-0\": true,\n  \"border-c-n70\": true,\n  \"border-bs-s\": true,\n  \"color-bgc-s30\": true,\n  \"color-c-n100\": true,\n  \"behavior-ho-80\": true,\n};\n\nconst heading = {\n  \"typography-f-sf\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-500\": true,\n  \"layout-mt-0\": true,\n  \"layout-mb-2\": true,\n  \"color-c-n10\": true,\n};\n\nconst h1 = {\n  ...heading,\n  \"typography-sz-tl\": true,\n};\n\nconst h2 = {\n  ...heading,\n  \"typography-sz-tm\": true,\n};\n\nconst h3 = {\n  ...heading,\n  \"typography-sz-ts\": true,\n};\n\nconst iframe = {\n  \"behavior-sw-n\": true,\n};\n\nconst input = {\n  \"typography-f-sf\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-400\": true,\n  \"layout-pl-4\": true,\n  \"layout-pr-4\": true,\n  \"layout-pt-2\": true,\n  \"layout-pb-2\": true,\n  \"border-br-6\": true,\n  \"border-bw-1\": true,\n  \"color-bc-s70\": true,\n  \"border-bs-s\": true,\n  \"layout-as-n\": true,\n  \"color-c-n10\": true,\n};\n\nconst p = {\n  \"typography-f-s\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-400\": true,\n  \"layout-m-0\": true,\n  \"typography-sz-bm\": true,\n  \"layout-as-n\": true,\n  \"color-c-n10\": true,\n};\n\nconst orderedList = {\n  \"typography-f-s\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-400\": true,\n  \"layout-m-0\": true,\n  \"typography-sz-bm\": true,\n  \"layout-as-n\": true,\n};\n\nconst unorderedList = {\n  \"typography-f-s\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-400\": true,\n  \"layout-m-0\": true,\n  \"typography-sz-bm\": true,\n  \"layout-as-n\": true,\n};\n\nconst listItem = {\n  \"typography-f-s\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-400\": true,\n  \"layout-m-0\": true,\n  \"typography-sz-bm\": true,\n  \"layout-as-n\": true,\n};\n\nconst pre = {\n  \"typography-f-c\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-400\": true,\n  \"typography-sz-bm\": true,\n  \"typography-ws-p\": true,\n  \"layout-as-n\": true,\n};\n\nconst textarea = {\n  ...input,\n  \"layout-r-none\": true,\n  \"layout-fs-c\": true,\n};\n\nconst video = {\n  \"layout-el-cv\": true,\n};\n\nconst aLight = v0_8.Styles.merge(a, { \"color-c-p30\": true });\nconst inputLight = v0_8.Styles.merge(input, { \"color-c-n5\": true });\nconst textareaLight = v0_8.Styles.merge(textarea, { \"color-c-n5\": true });\nconst buttonLight = v0_8.Styles.merge(button, { \"color-c-n100\": true });\nconst h1Light = v0_8.Styles.merge(h1, { \"color-c-n5\": true });\nconst h2Light = v0_8.Styles.merge(h2, { \"color-c-n5\": true });\nconst h3Light = v0_8.Styles.merge(h3, { \"color-c-n5\": true });\nconst bodyLight = v0_8.Styles.merge(body, { \"color-c-n5\": true });\nconst pLight = v0_8.Styles.merge(p, { \"color-c-n60\": true });\nconst preLight = v0_8.Styles.merge(pre, { \"color-c-n35\": true });\nconst orderedListLight = v0_8.Styles.merge(orderedList, {\n  \"color-c-n35\": true,\n});\nconst unorderedListLight = v0_8.Styles.merge(unorderedList, {\n  \"color-c-n35\": true,\n});\nconst listItemLight = v0_8.Styles.merge(listItem, {\n  \"color-c-n35\": true,\n});\n\nexport const theme: v0_8.Types.Theme = {\n  additionalStyles: {\n    Card: {\n      \"min-width\": \"320px\",\n    },\n    Button: {\n      \"--n-60\": \"var(--n-100)\",\n    },\n    Image: {\n      \"max-width\": \"120px\",\n      \"max-height\": \"120px\",\n      marginLeft: \"auto\",\n      marginRight: \"auto\",\n    },\n  },\n  components: {\n    AudioPlayer: {},\n    Button: {\n      \"layout-pt-2\": true,\n      \"layout-pb-2\": true,\n      \"layout-pl-5\": true,\n      \"layout-pr-5\": true,\n      \"border-br-2\": true,\n      \"border-bw-0\": true,\n      \"border-bs-s\": true,\n      \"color-bgc-p30\": true,\n      \"color-c-n100\": true,\n      \"behavior-ho-70\": true,\n    },\n    Card: {\n      \"border-br-4\": true,\n      \"color-bgc-p100\": true,\n      \"color-bc-n90\": true,\n      \"border-bw-1\": true,\n      \"border-bs-s\": true,\n      \"layout-pt-10\": true,\n      \"layout-pb-10\": true,\n      \"layout-pl-4\": true,\n      \"layout-pr-4\": true,\n    },\n    CheckBox: {\n      element: {\n        \"layout-m-0\": true,\n        \"layout-mr-2\": true,\n        \"layout-p-2\": true,\n        \"border-br-12\": true,\n        \"border-bw-1\": true,\n        \"border-bs-s\": true,\n        \"color-bgc-p100\": true,\n        \"color-bc-p60\": true,\n        \"color-c-n30\": true,\n        \"color-c-p30\": true,\n      },\n      label: {\n        \"color-c-p30\": true,\n        \"typography-f-sf\": true,\n        \"typography-v-r\": true,\n        \"typography-w-400\": true,\n        \"layout-flx-1\": true,\n        \"typography-sz-ll\": true,\n      },\n      container: {\n        \"layout-dsp-iflex\": true,\n        \"layout-al-c\": true,\n      },\n    },\n    Column: {},\n    DateTimeInput: {\n      container: {},\n      label: {},\n      element: {\n        \"layout-pt-2\": true,\n        \"layout-pb-2\": true,\n        \"layout-pl-3\": true,\n        \"layout-pr-3\": true,\n        \"border-br-12\": true,\n        \"border-bw-1\": true,\n        \"border-bs-s\": true,\n        \"color-bgc-p100\": true,\n        \"color-bc-p60\": true,\n        \"color-c-n30\": true,\n      },\n    },\n    Divider: {\n      \"color-bgc-n90\": true,\n      \"layout-mt-6\": true,\n      \"layout-mb-6\": true,\n    },\n    Image: {\n      all: {\n        \"border-br-50pc\": true,\n        \"layout-el-cv\": true,\n        \"layout-w-100\": true,\n        \"layout-h-100\": true,\n        \"layout-dsp-flexhor\": true,\n        \"layout-al-c\": true,\n        \"layout-sp-c\": true,\n        \"layout-mb-3\": true,\n      },\n      avatar: {},\n      header: {},\n      icon: {},\n      largeFeature: {},\n      mediumFeature: {},\n      smallFeature: {},\n    },\n    Icon: {\n      \"border-br-1\": true,\n      \"layout-p-2\": true,\n      \"color-bgc-n98\": true,\n      \"layout-dsp-flexhor\": true,\n      \"layout-al-c\": true,\n      \"layout-sp-c\": true,\n    },\n    List: {\n      \"layout-g-4\": true,\n      \"layout-p-2\": true,\n    },\n    Modal: {\n      backdrop: { \"color-bbgc-p60_20\": true },\n      element: {\n        \"border-br-2\": true,\n        \"color-bgc-p100\": true,\n        \"layout-p-4\": true,\n        \"border-bw-1\": true,\n        \"border-bs-s\": true,\n        \"color-bc-p80\": true,\n      },\n    },\n    MultipleChoice: {\n      container: {},\n      label: {},\n      element: {},\n    },\n    Row: {\n      \"layout-g-4\": true,\n      \"layout-mb-3\": true,\n    },\n    Slider: {\n      container: {},\n      label: {},\n      element: {},\n    },\n    Tabs: {\n      container: {},\n      controls: { all: {}, selected: {} },\n      element: {},\n    },\n    Text: {\n      all: {\n        \"layout-w-100\": true,\n        \"layout-g-2\": true,\n        \"color-c-p30\": true,\n      },\n      h1: {\n        \"typography-f-sf\": true,\n        \"typography-ta-c\": true,\n        \"typography-v-r\": true,\n        \"typography-w-500\": true,\n        \"layout-mt-0\": true,\n        \"layout-mr-0\": true,\n        \"layout-ml-0\": true,\n        \"layout-mb-2\": true,\n        \"layout-p-0\": true,\n        \"typography-sz-tl\": true,\n      },\n      h2: {\n        \"typography-f-sf\": true,\n        \"typography-ta-c\": true,\n        \"typography-v-r\": true,\n        \"typography-w-500\": true,\n        \"layout-mt-0\": true,\n        \"layout-mr-0\": true,\n        \"layout-ml-0\": true,\n        \"layout-mb-2\": true,\n        \"layout-p-0\": true,\n        \"typography-sz-tl\": true,\n      },\n      h3: {\n        \"typography-f-sf\": true,\n        \"typography-ta-c\": true,\n        \"typography-v-r\": true,\n        \"typography-w-500\": true,\n        \"layout-mt-0\": true,\n        \"layout-mr-0\": true,\n        \"layout-ml-0\": true,\n        \"layout-mb-0\": true,\n        \"layout-p-0\": true,\n        \"typography-sz-ts\": true,\n      },\n      h4: {\n        \"typography-f-sf\": true,\n        \"typography-ta-c\": true,\n        \"typography-v-r\": true,\n        \"typography-w-500\": true,\n        \"layout-mt-0\": true,\n        \"layout-mr-0\": true,\n        \"layout-ml-0\": true,\n        \"layout-mb-0\": true,\n        \"layout-p-0\": true,\n        \"typography-sz-bl\": true,\n      },\n      h5: {\n        \"typography-f-sf\": true,\n        \"typography-ta-c\": true,\n        \"typography-v-r\": true,\n        \"typography-w-500\": true,\n        \"layout-mt-0\": true,\n        \"layout-mr-0\": true,\n        \"layout-ml-0\": true,\n        \"layout-mb-0\": true,\n        \"layout-p-0\": true,\n        \"color-c-n30\": true,\n        \"typography-sz-bm\": true,\n        \"layout-mb-1\": true,\n      },\n      body: {},\n      caption: {},\n    },\n    TextField: {\n      container: {\n        \"typography-sz-bm\": true,\n        \"layout-w-100\": true,\n        \"layout-g-2\": true,\n        \"layout-dsp-flexhor\": true,\n        \"layout-al-c\": true,\n      },\n      label: {\n        \"layout-flx-0\": true,\n      },\n      element: {\n        \"typography-sz-bm\": true,\n        \"layout-pt-2\": true,\n        \"layout-pb-2\": true,\n        \"layout-pl-3\": true,\n        \"layout-pr-3\": true,\n        \"border-br-12\": true,\n        \"border-bw-1\": true,\n        \"border-bs-s\": true,\n        \"color-bgc-p100\": true,\n        \"color-bc-p60\": true,\n        \"color-c-n30\": true,\n        \"color-c-p30\": true,\n      },\n    },\n    Video: {\n      \"border-br-5\": true,\n      \"layout-el-cv\": true,\n    },\n  },\n  elements: {\n    a: aLight,\n    audio,\n    body: bodyLight,\n    button: buttonLight,\n    h1: h1Light,\n    h2: h2Light,\n    h3: h3Light,\n    h4: {},\n    h5: {},\n    iframe,\n    input: inputLight,\n    p: pLight,\n    pre: preLight,\n    textarea: textareaLight,\n    video,\n  },\n  markdown: {\n    p: [...Object.keys(pLight)],\n    h1: [...Object.keys(h1Light)],\n    h2: [...Object.keys(h2Light)],\n    h3: [...Object.keys(h3Light)],\n    h4: [],\n    h5: [],\n    ul: [...Object.keys(unorderedListLight)],\n    ol: [...Object.keys(orderedListLight)],\n    li: [...Object.keys(listItemLight)],\n    a: [...Object.keys(aLight)],\n    strong: [],\n    em: [],\n  },\n};\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n\n  \"compilerOptions\": {\n    \"composite\": false,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"incremental\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"inlineSources\": false,\n    \"preserveWatchOutput\": true,\n    \"sourceMap\": true,\n    \"target\": \"es2022\",\n    \"module\": \"es2022\",\n    \"lib\": [\"ESNext\", \"DOM\", \"DOM.Iterable\"],\n    \"skipLibCheck\": true,\n    \"useDefineForClassFields\": false,\n    \"rootDir\": \".\",\n    \"outDir\": \"dist\",\n    \"tsBuildInfoFile\": \"dist/.tsbuildinfo\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"strict\": false,\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true\n  },\n  \"references\": [{ \"path\": \"../../../../renderers/lit\" }]\n}\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/types/types.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { HTMLTemplateResult } from \"lit\";\n\nexport enum SnackType {\n  NONE = \"none\",\n  INFORMATION = \"information\",\n  WARNING = \"warning\",\n  ERROR = \"error\",\n  PENDING = \"pending\",\n}\n\nexport type SnackbarUUID = ReturnType<typeof globalThis.crypto.randomUUID>;\n\nexport type SnackbarAction = {\n  title: string;\n  action: string;\n  value?: HTMLTemplateResult | string;\n  callback?: () => void;\n};\n\nexport type SnackbarMessage = {\n  id: SnackbarUUID;\n  type: SnackType;\n  persistent: boolean;\n  message: string | HTMLTemplateResult;\n  actions?: SnackbarAction[];\n};\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/ui/custom-components/README.md",
    "content": "# A2UI custom component integration guide\n\nThis guide details how to create, register, and use a custom component in the A2UI client.\n\n## Create the component\n\nCreate a new Lit component file in `lib/src/0.8/ui/custom-components/`.\nExample: `my-component.ts`\n\n```typescript\nimport { html, css } from \"lit\";\nimport { property } from \"lit/decorators.js\";\n\nimport { Root } from \"../root.js\";\n\nexport class MyComponent extends Root {\n  @property() accessor myProp: string = \"Default\";\n\n  static styles = [\n    ...Root.styles, // Inherit base styles\n    css`\n      :host {\n        display: block;\n        padding: 16px;\n        border: 1px solid #ccc;\n      }\n    `,\n  ];\n\n  render() {\n    return html`\n      <div>\n        <h2>My Custom Component</h2>\n        <p>Prop value: ${this.myProp}</p>\n      </div>\n    `;\n  }\n}\n```\n\n## Register the component\n\nUpdate `lib/src/0.8/ui/custom-components/index.ts` to register your new component.\nYou must pass the desired tag name as the third argument.\n\n```typescript\nimport { componentRegistry } from \"../component-registry.js\";\nimport { MyComponent } from \"./my-component.js\"; // Import your component\n\nexport function registerCustomComponents() {\n  // Register with explicit tag name\n  componentRegistry.register(\"MyComponent\", MyComponent, \"my-component\");\n}\n\nexport { MyComponent }; // Export for type usage if needed\n```\n\n## Define the schema (server-side)\n\nCreate a JSON schema for your component properties. This will be used by the server to validate messages.\nExample: `lib/my_component_schema.json`\n\n```json\n{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"type\": \"object\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"type\": { \"const\": \"object\" },\n    \"properties\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"myProp\": {\n          \"type\": \"string\",\n          \"description\": \"A sample property.\"\n        }\n      },\n      \"required\": [\"myProp\"]\n    }\n  },\n  \"required\": [\"type\", \"properties\"]\n}\n```\n\n## Use in client application\n\nIn your client application (e.g., `contact` sample), ensure you import and call the registration function.\n\n```typescript\nimport { registerCustomComponents } from \"@a2ui/lit/ui\";\n\n// Call this once at startup\nregisterCustomComponents();\n```\n\n## Overriding standard components\n\nYou can replace standard A2UI components (like `TextField`, `Video`, `Button`) with your own custom implementations.\n\n### Steps to override\n\n1.  **Create your component** extending `Root` (just like a custom component).\n\n2.  **Ensure it accepts the standard properties** for that component type (e.g., `label` and `text` for `TextField`).\n\n3.  **Register it** using the **standard type name** (e.g., `\"TextField\"`).\n\n    ```typescript\n    // 1. Define your override\n    class MyPremiumTextField extends Root {\n      @property() accessor label = \"\";\n      @property() accessor text = \"\";\n\n      static styles = [\n        ...Root.styles,\n        css`\n          /* your premium styles */\n        `,\n      ];\n\n      render() {\n        return html`\n          <div class=\"premium-field\">\n            <label>${this.label}</label>\n            <input .value=\"${this.text}\" />\n          </div>\n        `;\n      }\n    }\n\n    // 2. Register with the STANDARD type name\n    import { componentRegistry } from \"@a2ui/lit/ui\";\n    componentRegistry.register(\n      \"TextField\",\n      MyPremiumTextField,\n      \"my-premium-textfield\"\n    );\n    ```\n\n**Result:**\nWhen the server sends a `TextField` component, the client will now render `<my-premium-textfield>` instead of the default `<a2ui-textfield>`.\n\n## Verify\n\nYou can verify the component by creating a simple HTML test file or by sending a server message with the new component type.\n\n**Server message example:**\n\n```json\n{\n  \"surfaceId\": \"main\",\n  \"component\": {\n    \"type\": \"MyComponent\",\n    \"id\": \"comp-1\",\n    \"properties\": {\n      \"myProp\": \"Hello World\"\n    }\n  }\n}\n```\n\n## Troubleshooting\n\n- **`NotSupportedError`**: If you see \"constructor has already been used\", ensure you **removed** the `@customElement` decorator from your component class.\n- **Component not rendering**: Check if `registerCustomComponents()` is actually called. Verify the tag name in the DOM matches what you registered (e.g., `<my-component>` vs `<a2ui-custom-mycomponent>`).\n- **Styles missing**: Ensure `static styles` includes `...Root.styles`.\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/ui/custom-components/org-chart.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { Root } from '@a2ui/lit/ui';\nimport { v0_8 } from '@a2ui/lit';\nimport { html, css, TemplateResult } from 'lit';\nimport { customElement, property } from 'lit/decorators.js';\nimport { map } from 'lit/directives/map.js';\n\n// Use aliases for convenience\nconst StateEvent = v0_8.Events.StateEvent;\ntype Action = v0_8.Types.Action;\n\nexport interface OrgChartNode {\n  title: string;\n  name: string;\n}\n\n@customElement('org-chart')\nexport class OrgChart extends Root {\n  @property({ type: Array }) accessor chain: OrgChartNode[] = [];\n  @property({ type: Object }) accessor action: Action | null = null;\n\n  static styles = [\n    ...Root.styles,\n    css`\n    :host {\n      display: block;\n      padding: 16px;\n      font-family: 'Roboto', sans-serif;\n    }\n\n    .container {\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n      gap: 16px;\n    }\n\n    .node {\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n      padding: 12px 24px;\n      background: #fff;\n      border: 1px solid #e0e0e0;\n      border-radius: 8px;\n      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\n      min-width: 200px;\n      position: relative;\n      transition: transform 0.2s, box-shadow 0.2s;\n      cursor: pointer;\n    }\n\n    .node:hover {\n      transform: translateY(-2px);\n      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);\n    }\n\n    .node:focus {\n      outline: 2px solid #1a73e8;\n      outline-offset: 2px;\n    }\n\n    .node.current {\n      background: #e8f0fe;\n      border-color: #1a73e8;\n      border-width: 2px;\n    }\n\n    .title {\n      font-size: 0.85rem;\n      color: #5f6368;\n      text-transform: uppercase;\n      letter-spacing: 0.5px;\n      margin-bottom: 4px;\n    }\n\n    .name {\n      font-size: 1.1rem;\n      font-weight: 500;\n      color: #202124;\n    }\n\n    .arrow {\n      color: #9aa0a6;\n      font-size: 24px;\n      line-height: 1;\n    }\n  `];\n\n  render() {\n    if (!this.chain || this.chain.length === 0) {\n      return html`<div class=\"empty\">No hierarchy data</div>`;\n    }\n\n    return html`\n      <div class=\"container\">\n        ${map(this.chain, (node, index) => {\n      const isLast = index === this.chain.length - 1;\n      return html`\n            <button\n              class=\"node ${isLast ? 'current' : ''}\"\n              @click=${() => this.handleNodeClick(node)}\n              aria-label=\"Select ${node.name} (${node.title})\"\n            >\n              <span class=\"title\">${node.title}</span>\n              <span class=\"name\">${node.name}</span>\n            </button>\n            ${!isLast ? html`<div class=\"arrow\">↓</div>` : ''}\n          `;\n    })}\n      </div>\n    `;\n  }\n\n  private handleNodeClick(node: OrgChartNode) {\n    if (!this.action) return;\n\n    // Create a new action with the node's context merged in\n    const newContext = [\n      ...(this.action.context || []),\n      {\n        key: 'clickedNodeTitle',\n        value: { literalString: node.title }\n      },\n      {\n        key: 'clickedNodeName',\n        value: { literalString: node.name }\n      }\n    ];\n\n    const actionWithContext: Action = {\n      ...this.action,\n      context: newContext as Action['context']\n    };\n\n    const evt = new StateEvent<\"a2ui.action\">({\n      eventType: \"a2ui.action\",\n      action: actionWithContext,\n      dataContextPath: this.dataContextPath,\n      sourceComponentId: this.id,\n      sourceComponent: this.component,\n    });\n    this.dispatchEvent(evt);\n  }\n}\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/ui/custom-components/premium-text-field.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { Root } from '@a2ui/lit/ui';\nimport { html, css } from 'lit';\nimport { property } from 'lit/decorators.js';\n\nexport class PremiumTextField extends Root {\n  @property() accessor label = '';\n  @property() accessor text = '';\n\n  static styles = [\n    ...Root.styles,\n    css`\n      :host {\n        display: block;\n        padding: 16px;\n        background: #fff;\n        border-radius: 12px;\n        box-shadow: 0 4px 12px rgba(0,0,0,0.08);\n        border: 1px solid #e0e0e0;\n        transition: all 0.2s ease;\n        font-family: 'Inter', sans-serif;\n      }\n      :host(:hover) {\n        transform: translateY(-2px);\n        box-shadow: 0 6px 16px rgba(0,0,0,0.12);\n      }\n      .input-container {\n        position: relative;\n        margin-top: 8px;\n      }\n      input {\n        width: 100%;\n        padding: 12px 16px;\n        font-size: 16px;\n        border: 2px solid #e0e0e0;\n        border-radius: 8px;\n        outline: none;\n        transition: border-color 0.2s;\n        box-sizing: border-box;\n        background: #fafafa;\n      }\n      input:focus {\n        border-color: #6200ee;\n        background: #fff;\n      }\n      label {\n        display: block;\n        font-size: 14px;\n        font-weight: 600;\n        color: #333;\n        margin-bottom: 4px;\n      }\n      .hint {\n        margin-top: 8px;\n        font-size: 12px;\n        color: #666;\n        display: flex;\n        align-items: center;\n        gap: 4px;\n      }\n      .badge {\n        background: #6200ee;\n        color: white;\n        padding: 2px 6px;\n        border-radius: 4px;\n        font-size: 10px;\n        font-weight: bold;\n        text-transform: uppercase;\n      }\n    `\n  ];\n\n  render() {\n    return html`\n      <label>${this.label}</label>\n      <div class=\"input-container\">\n        <input type=\"text\" .value=\"${this.text}\" placeholder=\"Type here...\">\n      </div>\n      <div class=\"hint\">\n        <span class=\"badge\">Custom</span>\n        <span>This is a premium override of the standard TextField.</span>\n      </div>\n    `;\n  }\n}\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/ui/custom-components/register-components.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { componentRegistry } from \"@a2ui/lit/ui\";\nimport { OrgChart } from \"./org-chart.js\";\nimport { PremiumTextField } from \"./premium-text-field.js\";\n\nexport function registerContactComponents() {\n  // Register OrgChart\n  componentRegistry.register(\"OrgChart\", OrgChart, \"org-chart\");\n\n  // Register PremiumTextField as an override for TextField\n  componentRegistry.register(\n    \"TextField\",\n    PremiumTextField,\n    \"premium-text-field\"\n  );\n\n  console.log(\"Registered Contact App Custom Components\");\n}\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/ui/custom-components/test/README.md",
    "content": "# Contact sample verification tests\n\nThis directory contains tests to verify custom component integration specifically within the `contact` sample application environment.\n\n## How to run\n\nThese tests run via the Vite development server used by the contact sample.\n\n### 1. Start the dev server\nFrom the `web/lit/samples/contact` directory, run:\n\n```bash\nnpm run dev\n```\n\n### 2. Access the tests\nOpen your browser and navigate to the local server (usually port 5173):\n\n-   **Component override test**:\n    [http://localhost:5173/ui/custom-components/test/override-test.html](http://localhost:5173/ui/custom-components/test/override-test.html)\n    *Verifies that a standard component (TextField) can be overridden by a custom implementation.*\n\n-   **Hierarchy graph integration test**:\n    [http://localhost:5173/ui/custom-components/test/hierarchy-test.html](http://localhost:5173/ui/custom-components/test/hierarchy-test.html)\n    *Verifies that the HierarchyGraph component renders correctly within the contact app's build setup.*\n\n## Files\n\n-   `override-test.html` & `override-test.ts`: Implements and tests a custom `TextField` override.\n-   `hierarchy-test.html`: Tests the `HierarchyGraph` component.\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/ui/custom-components/test/org-chart-test.html",
    "content": "<!--\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n -->\n\n<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>A2UI Org Chart Test (Contact Sample)</title>\n  <style>\n    body {\n      font-family: sans-serif;\n      padding: 20px;\n      background: #f5f5f5;\n    }\n\n    .container {\n      margin-top: 20px;\n      display: flex;\n      justify-content: center;\n    }\n  </style>\n  <script type=\"module\">\n    import { registerContactComponents } from '../register-components.js';\n\n    // Register custom components\n    registerContactComponents();\n    console.log('Custom components registered in Contact Sample');\n  </script>\n</head>\n\n<body>\n  <h1>A2UI Org Chart Test (Contact Sample)</h1>\n\n  <div class=\"container\">\n    <org-chart id=\"graph\"></org-chart>\n  </div>\n\n  <script>\n    customElements.whenDefined('org-chart').then(() => {\n      const graph = document.getElementById('graph');\n\n      graph.chain = [\n        { title: 'CEO', name: 'Alice Johnson' },\n        { title: 'SVP', name: 'Bob Smith' },\n        { title: 'VP', name: 'Charlie Brown' },\n        { title: 'Director', name: 'Diana Prince' },\n        { title: 'Software Engineer', name: 'Evan Wright' }\n      ];\n\n      graph.action = {\n        name: 'showContactCard',\n        context: []\n      };\n\n      graph.addEventListener('a2uiaction', (e) => {\n        const status = document.createElement('div');\n        const clickedNodeTitle = e.detail.action.context.find(c => c.key === 'clickedNodeTitle')?.value.literalString;\n        const clickedNodeName = e.detail.action.context.find(c => c.key === 'clickedNodeName')?.value.literalString;\n        status.textContent = `Clicked: ${clickedNodeTitle} - ${clickedNodeName}`;\n        status.style.marginTop = '20px';\n        status.style.padding = '10px';\n        status.style.background = '#e0f7fa';\n        status.style.border = '1px solid #006064';\n        status.id = 'action-status';\n        document.body.appendChild(status);\n      });\n    });\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/ui/custom-components/test/override-test.html",
    "content": "<!--\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n -->\n\n<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <title>A2UI Component Override Test</title>\n  <style>\n    body {\n      padding: 20px;\n      font-family: sans-serif;\n    }\n\n    .container {\n      margin-top: 20px;\n      border: 1px dashed #ccc;\n      padding: 10px;\n    }\n  </style>\n  <script type=\"module\" src=\"./override-test.ts\"></script>\n</head>\n\n<body>\n  <h1>Component Override Test</h1>\n  <div id=\"app\" class=\"container\"></div>\n</body>\n\n</html>"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/ui/custom-components/test/override-test.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { componentRegistry, Root } from \"@a2ui/lit/ui\";\nimport { html, css } from \"lit\";\nimport { property } from \"lit/decorators.js\";\n// 1. Define the override\nimport { PremiumTextField } from \"../premium-text-field.js\";\n\n// 2. Register it as \"TextField\"\ncomponentRegistry.register(\"TextField\", PremiumTextField, \"premium-text-field\");\nconsole.log(\"Registered PremiumTextField override\");\n\n// 3. Render a standard TextField component node\nconst container = document.getElementById(\"app\");\nif (container) {\n  const root = document.createElement(\"a2ui-root\") as Root;\n\n  const textFieldComponent = {\n    type: \"TextField\",\n    id: \"tf-1\",\n    properties: {\n      label: \"Enter your name\",\n      text: \"John Doe\",\n    },\n  };\n\n  // Root renders its *children*, so we must pass the component as a child.\n  root.childComponents = [textFieldComponent];\n\n  root.enableCustomElements = true; // Enable the feature\n  container.appendChild(root);\n}\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/ui/snackbar.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\nimport { LitElement, html, css, nothing, unsafeCSS } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { SnackbarMessage, SnackbarUUID, SnackType } from \"../types/types\";\nimport { repeat } from \"lit/directives/repeat.js\";\nimport { SnackbarActionEvent } from \"../events/events\";\nimport { classMap } from \"lit/directives/class-map.js\";\nimport { v0_8 } from \"@a2ui/lit\";\n\nconst DEFAULT_TIMEOUT = 8000;\n\n@customElement(\"ui-snackbar\")\nexport class Snackbar extends LitElement {\n  @property({ reflect: true, type: Boolean })\n  accessor active = false;\n\n  @property({ reflect: true, type: Boolean })\n  accessor error = false;\n\n  @property()\n  accessor timeout = DEFAULT_TIMEOUT;\n\n  #messages: SnackbarMessage[] = [];\n  #timeout = 0;\n\n  static styles = [\n    unsafeCSS(v0_8.Styles.structuralStyles),\n    css`\n      :host {\n        --text-color: var(--n-0);\n        --bb-body-medium: 16px;\n        --bb-body-line-height-medium: 24px;\n\n        display: flex;\n        align-items: center;\n        position: fixed;\n        bottom: var(--bb-grid-size-7);\n        left: 50%;\n        translate: -50% 0;\n        opacity: 0;\n        pointer-events: none;\n        border-radius: var(--bb-grid-size-2);\n        background: var(--n-90);\n        padding: var(--bb-grid-size-3) var(--bb-grid-size-6);\n        width: 60svw;\n        max-width: 720px;\n        z-index: 1800;\n        scrollbar-width: none;\n        overflow-x: scroll;\n        font: 400 var(--bb-body-medium) / var(--bb-body-line-height-medium)\n          var(--bb-font-family);\n      }\n\n      :host([active]) {\n        transition: opacity 0.3s cubic-bezier(0, 0, 0.3, 1) 0.2s;\n        opacity: 1;\n        pointer-events: auto;\n      }\n\n      :host([error]) {\n        background: var(--e-90);\n        --text-color: var(--e-40);\n      }\n\n      .g-icon {\n        flex: 0 0 auto;\n        color: var(--text-color);\n        margin-right: var(--bb-grid-size-4);\n\n        &.rotate {\n          animation: 1s linear 0s infinite normal forwards running rotate;\n        }\n      }\n\n      #messages {\n        color: var(--text-color);\n        flex: 1 1 auto;\n        margin-right: var(--bb-grid-size-11);\n\n        a,\n        a:visited {\n          color: var(--bb-ui-600);\n          text-decoration: none;\n\n          &:hover {\n            color: var(--bb-ui-500);\n            text-decoration: underline;\n          }\n        }\n      }\n\n      #actions {\n        flex: 0 1 auto;\n        width: fit-content;\n        margin-right: var(--bb-grid-size-3);\n\n        & button {\n          font: 500 var(--bb-body-medium) / var(--bb-body-line-height-medium)\n            var(--bb-font-family);\n          padding: 0;\n          background: transparent;\n          border: none;\n          margin: 0 var(--bb-grid-size-4);\n          color: var(--text-color);\n          opacity: 0.7;\n          transition: opacity 0.2s cubic-bezier(0, 0, 0.3, 1);\n\n          &:not([disabled]) {\n            cursor: pointer;\n\n            &:hover,\n            &:focus {\n              opacity: 1;\n            }\n          }\n        }\n      }\n\n      #close {\n        display: flex;\n        align-items: center;\n        padding: 0;\n        color: var(--text-color);\n        background: transparent;\n        border: none;\n        margin: 0 0 0 var(--bb-grid-size-2);\n        opacity: 0.7;\n        transition: opacity 0.2s cubic-bezier(0, 0, 0.3, 1);\n\n        .g-icon {\n          margin-right: 0;\n        }\n\n        &:not([disabled]) {\n          cursor: pointer;\n\n          &:hover,\n          &:focus {\n            opacity: 1;\n          }\n        }\n      }\n\n      @keyframes rotate {\n        from {\n          rotate: 0deg;\n        }\n\n        to {\n          rotate: 360deg;\n        }\n      }\n    `,\n  ];\n\n  show(message: SnackbarMessage, replaceAll = false) {\n    const existingMessage = this.#messages.findIndex(\n      (msg) => msg.id === message.id\n    );\n    if (existingMessage === -1) {\n      if (replaceAll) {\n        this.#messages.length = 0;\n      }\n\n      this.#messages.push(message);\n    } else {\n      this.#messages[existingMessage] = message;\n    }\n\n    window.clearTimeout(this.#timeout);\n    if (!this.#messages.every((msg) => msg.persistent)) {\n      this.#timeout = window.setTimeout(() => {\n        this.hide();\n      }, this.timeout);\n    }\n\n    this.error = this.#messages.some((msg) => msg.type === SnackType.ERROR);\n    this.active = true;\n    this.requestUpdate();\n\n    return message.id;\n  }\n\n  hide(id?: SnackbarUUID) {\n    if (id) {\n      const idx = this.#messages.findIndex((msg) => msg.id === id);\n      if (idx !== -1) {\n        this.#messages.splice(idx, 1);\n      }\n    } else {\n      this.#messages.length = 0;\n    }\n\n    this.active = this.#messages.length !== 0;\n    this.updateComplete.then((avoidedUpdate) => {\n      if (!avoidedUpdate) {\n        return;\n      }\n\n      this.requestUpdate();\n    });\n  }\n\n  render() {\n    let rotate = false;\n    let icon = \"\";\n    for (let i = this.#messages.length - 1; i >= 0; i--) {\n      if (\n        !this.#messages[i].type ||\n        this.#messages[i].type === SnackType.NONE\n      ) {\n        continue;\n      }\n\n      icon = this.#messages[i].type;\n      if (this.#messages[i].type === SnackType.PENDING) {\n        icon = \"progress_activity\";\n        rotate = true;\n      }\n      break;\n    }\n\n    return html` ${icon\n      ? html`<span\n            class=${classMap({\n        \"g-icon\": true,\n        round: true,\n        filled: true,\n        rotate,\n      })}\n            >${icon}</span\n          >`\n      : nothing}\n      <div id=\"messages\">\n        ${repeat(\n        this.#messages,\n        (message) => message.id,\n        (message) => {\n          return html`<div>${message.message}</div>`;\n        }\n      )}\n      </div>\n      <div id=\"actions\">\n        ${repeat(\n        this.#messages,\n        (message) => message.id,\n        (message) => {\n          if (!message.actions) {\n            return nothing;\n          }\n\n          return html`${repeat(\n            message.actions,\n            (action) => action.value,\n            (action) => {\n              return html`<button\n                  @click=${() => {\n                  this.hide();\n                  this.dispatchEvent(\n                    new SnackbarActionEvent(\n                      action.action,\n                      action.value,\n                      action.callback\n                    )\n                  );\n                }}\n                >\n                  ${action.title}\n                </button>`;\n            }\n          )}`;\n        }\n      )}\n      </div>\n      <button\n        id=\"close\"\n        @click=${() => {\n        this.hide();\n        this.dispatchEvent(new SnackbarActionEvent(\"dismiss\"));\n      }}\n      >\n        <span class=\"g-icon\">close</span>\n      </button>`;\n  }\n}\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/ui/ui.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nexport { Snackbar } from \"./snackbar\";\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/contact/vite.config.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { config } from \"dotenv\";\nimport { UserConfig } from \"vite\";\nimport * as Middleware from \"./middleware\";\nimport { dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nexport default async () => {\n  config();\n\n  const entry: Record<string, string> = {\n    contact: resolve(__dirname, \"index.html\"),\n  };\n\n  return {\n    plugins: [Middleware.A2AMiddleware.plugin()],\n    build: {\n      rollupOptions: {\n        input: entry,\n      },\n      target: \"esnext\",\n    },\n    define: {},\n    resolve: {\n      dedupe: [\"lit\"],\n    },\n  } satisfies UserConfig;\n};\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/package.json",
    "content": "{\n  \"name\": \"@a2ui/lit-samples\",\n  \"private\": true,\n  \"version\": \"0.8.1\",\n  \"description\": \"A2UI Lit Samples\",\n  \"workspaces\": [\n    \"contact\",\n    \"restaurant\",\n    \"shell\"\n  ],\n  \"scripts\": {\n    \"serve:agent:restaurant\": \"cd ../../general_agent && uv run .\",\n    \"serve:agent:contact_example\": \"cd ../../agent/as/contact_example && uv run .\",\n    \"serve:agent:contact_multi_surface\": \"cd ../../agent/adk/contact_multiple_surfaces && uv run .\",\n    \"serve:shell\": \"cd shell && npm run dev\",\n    \"build:renderer\": \"cd ../../../renderers/web_core && npm install && npm run build && cd ../lit && npm install && npm run build\",\n    \"demo:all\": \"npm install && npm run build:renderer && concurrently -k -n \\\"SHELL,REST,CONT1\\\" -c \\\"magenta,blue,green\\\" \\\"npm run serve:shell\\\" \\\"npm run serve:agent:restaurant\\\" \\\"npm run serve:agent:contact_example\\\"\",\n    \"demo:restaurant\": \"npm install && npm run build:renderer && concurrently -k -n \\\"SHELL,REST\\\" -c \\\"magenta,blue\\\" \\\"npm run serve:shell\\\" \\\"npm run serve:agent:restaurant\\\"\",\n    \"demo:contact_example\": \"npm install && npm run build:renderer && concurrently -k -n \\\"SHELL,CONT1\\\" -c \\\"magenta,green\\\" \\\"npm run serve:shell\\\" \\\"npm run serve:agent:contact_example\\\"\"\n  },\n  \"devDependencies\": {\n    \"concurrently\": \"9.2.1\"\n  }\n}\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/shell/README.md",
    "content": "# A2UI Generator\n\nThis is a UI to generate and visualize A2UI responses.\n\n## Prerequisites\n\n1. [nodejs](https://nodejs.org/en)\n\n## Running\n\nThis sample depends on the Lit renderer. Before running this sample, you need to build the renderer.\n\n1. **Build the renderer:**\n   ```bash\n   cd ../../../renderers/lit\n   npm install\n   npm run build\n   ```\n\n2. **Run this sample:**\n   ```bash\n   cd - # back to the sample directory\n   npm install\n   ```\n\n3. **Run the servers:**\n   - Run the [A2A server](../../../general_agent/)\n   - Run the dev server: `npm run dev`\n\nAfter starting the dev server, you can open http://localhost:5173/ to view the sample.\n\nImportant: The sample code provided is for demonstration purposes and illustrates the mechanics of A2UI and the Agent-to-Agent (A2A) protocol. When building production applications, it is critical to treat any agent operating outside of your direct control as a potentially untrusted entity.\n\nAll operational data received from an external agent—including its AgentCard, messages, artifacts, and task statuses—should be handled as untrusted input. For example, a malicious agent could provide crafted data in its fields (e.g., name, skills.description) that, if used without sanitization to construct prompts for a Large Language Model (LLM), could expose your application to prompt injection attacks.\n\nSimilarly, any UI definition or data stream received must be treated as untrusted. Malicious agents could attempt to spoof legitimate interfaces to deceive users (phishing), inject malicious scripts via property values (XSS), or generate excessive layout complexity to degrade client performance (DoS). If your application supports optional embedded content (such as iframes or web views), additional care must be taken to prevent exposure to malicious external sites.\n\nDeveloper Responsibility: Failure to properly validate data and strictly sandbox rendered content can introduce severe vulnerabilities. Developers are responsible for implementing appropriate security measures—such as input sanitization, Content Security Policies (CSP), strict isolation for optional embedded content, and secure credential handling—to protect their systems and users."
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/shell/THEMING.md",
    "content": "# A2UI Theming & Configuration Guide\n\nThis guide explains how the Universal App Shell handles theming and how to add new sample applications seamlessly.\n\n## Architecture Overview\n\nThe styling system is built on two distinct layers:\n\n### 1. **Base Layer (`default-theme.ts`)**\n\n- **Role**: Structural & Functional Styles.\n- **What it does**: Maps A2UI components (like `Text`, `Card`, `Row`) to functional CSS utility classes (e.g., `layout-w-100`, `typography-f-sf`).\n- **When to touch**: Rarely. Only if you need to change the fundamental layout behavior of a component across all shell apps.\n\n### 2. **Configuration Layer (`configs/*.ts`)**\n\n- **Role**: App Identity & Brand Overrides.\n- **What it does**: Allows for app-level theme overrides.\n- **Key Mechanism**: The `AppConfig` interface allows you to provide a new theme by setting items in the `theme` property.\n- **When to touch**: Whenever you add a new app and want to change an app's theme from the default theme provided with the shell.\n\n---\n\n## How to Add a New Sample App\n\nFollow these steps to add a new application (e.g., \"Flight Booker\") with its own unique theme.\n\n### Step 1: Create the Config\n\nCreate a new file `configs/flights.ts`:\n\n```typescript\nimport { AppConfig } from \"./types.js\";\nimport { cloneDefaultTheme } from \"../theme/clone-default-theme.js\";\n\nconst theme = cloneDefaultTheme();\n// Set your variables, e.g., theme.components.Card = { 'color-bgc-n100': true }\n\nexport const config: AppConfig = {\n  key: \"flights\",\n  title: \"Flight Booker\",\n  heroImage: \"/hero-flights.png\",\n  heroImageDark: \"/hero-flights-dark.png\", // Optional\n  placeholder: \"Where do you want to go?\",\n  loadingText: [\"Checking availability...\", \"Finding best rates...\"],\n  serverUrl: \"http://localhost:10004\", // Your agent's URL\n  theme, // Apply the theme.\n};\n```\n\n### Step 2: Register the Config\n\nUpdate `app.ts` to include your new config:\n\n```typescript\nimport { config as flightsConfig } from \"./configs/flights.js\";\n\nconst configs: Record<string, AppConfig> = {\n  restaurant: restaurantConfig,\n  contacts: contactsConfig,\n  flights: flightsConfig, // Add this line\n};\n```\n\n### Step 3: Run It\n\nAccess your new app by adding the `app` query parameter:\n`http://localhost:5173/?app=flights`\n\nThe App Shell will automatically:\n\n1.  Load your `flights` config.\n2.  Apply your theme to the A2UI root's theme context.\n3.  Connect to your specified `serverUrl`.\n\n---\n\n## Reference: Styling Levers\n\nThis section lists the available styling \"levers\" (utility classes) you can use in your `theme.ts` file or directly in your components. These are defined in the core library (`renderers/lit/src/0.8/styles`).\n\n### 1. Layout (`layout-`)\n\n**Source:** `styles/layout.ts`\n\n| Category        | Prefix        | Scale/Values                                | Examples                                                    |\n| :-------------- | :------------ | :------------------------------------------ | :---------------------------------------------------------- |\n| **Padding**     | `layout-p-`   | 0-24 (1 = 4px)                              | `layout-p-4` (16px), `layout-pt-2` (Top 8px), `layout-px-4` |\n| **Margin**      | `layout-m-`   | 0-24 (1 = 4px)                              | `layout-m-0`, `layout-mb-4` (Bottom 16px), `layout-mx-auto` |\n| **Gap**         | `layout-g-`   | 0-24 (1 = 4px)                              | `layout-g-2` (8px), `layout-g-4` (16px)                     |\n| **Width**       | `layout-w-`   | 10-100 (Percentage)                         | `layout-w-100` (100%), `layout-w-50` (50%)                  |\n| **Width (Px)**  | `layout-wp-`  | 0-15 (1 = 4px)                              | `layout-wp-10` (40px)                                       |\n| **Height**      | `layout-h-`   | 10-100 (Percentage)                         | `layout-h-100` (100%)                                       |\n| **Height (Px)** | `layout-hp-`  | 0-15 (1 = 4px)                              | `layout-hp-10` (40px)                                       |\n| **Display**     | `layout-dsp-` | `none`, `block`, `grid`, `flex`, `iflex`    | `layout-dsp-flexhor` (Row), `layout-dsp-flexvert` (Col)     |\n| **Alignment**   | `layout-al-`  | `fs` (Start), `fe` (End), `c` (Center)      | `layout-al-c` (Align Items Center)                          |\n| **Justify**     | `layout-sp-`  | `c` (Center), `bt` (Between), `ev` (Evenly) | `layout-sp-bt` (Justify Content Space Between)              |\n| **Flex**        | `layout-flx-` | `0` (None), `1` (Grow)                      | `layout-flx-1` (Flex Grow 1)                                |\n| **Position**    | `layout-pos-` | `a` (Absolute), `rel` (Relative)            | `layout-pos-rel`                                            |\n\n### 2. Colors (`color-`)\n\n**Source:** `styles/colors.ts`\n\n| Category         | Prefix       | Scale/Values        | Examples                                                              |\n| :--------------- | :----------- | :------------------ | :-------------------------------------------------------------------- |\n| **Text Color**   | `color-c-`   | Palette Key + Shade | `color-c-p50` (Primary), `color-c-n10` (Black), `color-c-e40` (Error) |\n| **Background**   | `color-bgc-` | Palette Key + Shade | `color-bgc-p100` (White/Lightest), `color-bgc-s30` (Secondary Dark)   |\n| **Border Color** | `color-bc-`  | Palette Key + Shade | `color-bc-p60` (Primary Border)                                       |\n\n**Palette Keys:**\n\n- `p` = Primary (Brand)\n- `s` = Secondary\n- `t` = Tertiary\n- `n` = Neutral (Grays)\n- `nv` = Neutral Variant\n- `e` = Error\n\n**Shades:** 0, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 95, 98, 99, 100\n\n### 3. Typography (`typography-`)\n\n**Source:** `styles/type.ts`\n\n| Category            | Prefix           | Scale/Values                              | Examples                                                                             |\n| :------------------ | :--------------- | :---------------------------------------- | :----------------------------------------------------------------------------------- |\n| **Font Family**     | `typography-f-`  | `sf` (Sans/Flex), `s` (Serif), `c` (Code) | `typography-f-sf` (System UI / Outfit)                                               |\n| **Weight**          | `typography-w-`  | 100-900                                   | `typography-w-400` (Regular), `typography-w-500` (Medium), `typography-w-700` (Bold) |\n| **Size (Body)**     | `typography-sz-` | `bs`, `bm`, `bl`                          | `typography-sz-bm` (Body Medium - 14px)                                              |\n| **Size (Title)**    | `typography-sz-` | `ts`, `tm`, `tl`                          | `typography-sz-tl` (Title Large - 22px)                                              |\n| **Size (Headline)** | `typography-sz-` | `hs`, `hm`, `hl`                          | `typography-sz-hl` (Headline Large - 32px)                                           |\n| **Size (Display)**  | `typography-sz-` | `ds`, `dm`, `dl`                          | `typography-sz-dl` (Display Large - 57px)                                            |\n| **Align**           | `typography-ta-` | `s` (Start), `c` (Center)                 | `typography-ta-c`                                                                    |\n\n### 4. Borders (`border-`)\n\n**Source:** `styles/border.ts`\n\n| Category   | Prefix       | Scale/Values   | Examples                                              |\n| :--------- | :----------- | :------------- | :---------------------------------------------------- |\n| **Radius** | `border-br-` | 0-24 (1 = 4px) | `border-br-4` (16px), `border-br-50pc` (50% / Circle) |\n| **Width**  | `border-bw-` | 0-24 (Pixels)  | `border-bw-1` (1px), `border-bw-2` (2px)              |\n| **Style**  | `border-bs-` | `s` (Solid)    | `border-bs-s`                                         |\n\n### 5. Behavior & Opacity\n\n**Source:** `styles/behavior.ts`, `styles/opacity.ts`\n\n| Category          | Prefix         | Scale/Values                           | Examples                                |\n| :---------------- | :------------- | :------------------------------------- | :-------------------------------------- |\n| **Hover Opacity** | `behavior-ho-` | 0-100 (Step 5)                         | `behavior-ho-80` (Opacity 0.8 on hover) |\n| **Opacity**       | `opacity-el-`  | 0-100 (Step 5)                         | `opacity-el-50` (Opacity 0.5)           |\n| **Overflow**      | `behavior-o-`  | `s` (Scroll), `a` (Auto), `h` (Hidden) | `behavior-o-h`                          |\n| **Scrollbar**     | `behavior-sw-` | `n` (None)                             | `behavior-sw-n`                         |\n\n### 6. Icons\n\n**Source:** `styles/icons.ts`\n\n- Class: `.g-icon`\n- Variants: `.filled`, `.filled-heavy`\n- Usage: `<span class=\"g-icon\">icon_name</span>`\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/shell/app.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { SignalWatcher } from \"@lit-labs/signals\";\nimport { provide } from \"@lit/context\";\nimport {\n  LitElement,\n  html,\n  css,\n  nothing,\n  HTMLTemplateResult,\n  unsafeCSS,\n} from \"lit\";\nimport { customElement, state } from \"lit/decorators.js\";\nimport { theme as uiTheme } from \"./theme/default-theme.js\";\nimport { A2UIClient } from \"./client.js\";\nimport {\n  SnackbarAction,\n  SnackbarMessage,\n  SnackbarUUID,\n  SnackType,\n} from \"./types/types.js\";\nimport { type Snackbar } from \"./ui/snackbar.js\";\nimport { repeat } from \"lit/directives/repeat.js\";\nimport { v0_8 } from \"@a2ui/lit\";\nimport * as UI from \"@a2ui/lit/ui\";\n\n// App elements.\nimport \"./ui/ui.js\";\n\n// Configurations\nimport { AppConfig } from \"./configs/types.js\";\nimport { config as restaurantConfig } from \"./configs/restaurant.js\";\nimport { config as contactsConfig } from \"./configs/contacts.js\";\nimport { styleMap } from \"lit/directives/style-map.js\";\n\nconst configs: Record<string, AppConfig> = {\n  restaurant: restaurantConfig,\n  contacts: contactsConfig,\n};\n\n@customElement(\"a2ui-shell\")\nexport class A2UILayoutEditor extends SignalWatcher(LitElement) {\n  @provide({ context: UI.Context.themeContext })\n  accessor theme: v0_8.Types.Theme = uiTheme;\n\n  @state()\n  accessor #requesting = false;\n\n  @state()\n  accessor #error: string | null = null;\n\n  @state()\n  accessor #lastMessages: v0_8.Types.ServerToClientMessage[] = [];\n\n  @state()\n  accessor config: AppConfig = configs.restaurant;\n\n  @state()\n  accessor #loadingTextIndex = 0;\n  #loadingInterval: number | undefined;\n\n  static styles = [\n    unsafeCSS(v0_8.Styles.structuralStyles),\n    css`\n      * {\n        box-sizing: border-box;\n      }\n\n      :host {\n        display: block;\n        max-width: 640px;\n        margin: 0 auto;\n        min-height: 100%;\n        color: light-dark(var(--n-10), var(--n-90));\n        font-family: var(--font-family);\n      }\n\n      #hero-img {\n        width: 100%;\n        max-width: 400px;\n        aspect-ratio: 1280/720;\n        height: auto;\n        margin-bottom: var(--bb-grid-size-6);\n        display: block;\n        margin: 0 auto;\n        background: var(--background-image-light) center center / contain\n          no-repeat;\n      }\n\n      #surfaces {\n        width: 100%;\n        max-width: 100svw;\n        padding: var(--bb-grid-size-3);\n        animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 0.3s backwards;\n      }\n\n      form {\n        display: flex;\n        flex-direction: column;\n        flex: 1;\n        gap: 16px;\n        align-items: center;\n        padding: 16px 0;\n        animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 1s backwards;\n\n        & h1 {\n          color: light-dark(var(--p-40), var(--n-90));\n        }\n\n        & > div {\n          display: flex;\n          flex: 1;\n          gap: 16px;\n          align-items: center;\n          width: 100%;\n\n          & > input {\n            display: block;\n            flex: 1;\n            border-radius: 32px;\n            padding: 16px 24px;\n            border: 1px solid var(--p-60);\n            background: light-dark(var(--n-100), var(--n-10));\n            font-size: 16px;\n          }\n\n          & > button {\n            display: flex;\n            align-items: center;\n            background: var(--p-40);\n            color: var(--n-100);\n            border: none;\n            padding: 8px 16px;\n            border-radius: 32px;\n            opacity: 0.5;\n\n            &:not([disabled]) {\n              cursor: pointer;\n              opacity: 1;\n            }\n          }\n        }\n      }\n\n      .rotate {\n        animation: rotate 1s linear infinite;\n      }\n\n      .pending {\n        width: 100%;\n        min-height: 200px;\n        display: flex;\n        flex-direction: column;\n        align-items: center;\n        justify-content: center;\n        animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 0.3s backwards;\n        gap: 16px;\n      }\n\n      .spinner {\n        width: 48px;\n        height: 48px;\n        border: 4px solid rgba(255, 255, 255, 0.1);\n        border-left-color: var(--p-60);\n        border-radius: 50%;\n        animation: spin 1s linear infinite;\n      }\n\n      .theme-toggle {\n        padding: 0;\n        margin: 0;\n        border: none;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        position: fixed;\n        top: var(--bb-grid-size-3);\n        right: var(--bb-grid-size-4);\n        background: light-dark(var(--n-100), var(--n-0));\n        border-radius: 50%;\n        color: var(--p-30);\n        cursor: pointer;\n        width: 48px;\n        height: 48px;\n        font-size: 32px;\n\n        & .g-icon {\n          pointer-events: none;\n\n          &::before {\n            content: \"dark_mode\";\n          }\n        }\n      }\n\n      @container style(--color-scheme: dark) {\n        .theme-toggle .g-icon::before {\n          content: \"light_mode\";\n          color: var(--n-90);\n        }\n\n        #hero-img {\n          background-image: var(--background-image-dark);\n        }\n      }\n\n      @keyframes spin {\n        to {\n          transform: rotate(360deg);\n        }\n      }\n\n      @keyframes pulse {\n        0% {\n          opacity: 0.6;\n        }\n        50% {\n          opacity: 1;\n        }\n        100% {\n          opacity: 0.6;\n        }\n      }\n\n      .error {\n        color: var(--e-40);\n        background-color: var(--e-95);\n        border: 1px solid var(--e-80);\n        padding: 16px;\n        border-radius: 8px;\n      }\n\n      @keyframes fadeIn {\n        from {\n          opacity: 0;\n        }\n\n        to {\n          opacity: 1;\n        }\n      }\n\n      @keyframes rotate {\n        from {\n          rotate: 0deg;\n        }\n\n        to {\n          rotate: 360deg;\n        }\n      }\n    `,\n  ];\n\n  #processor = v0_8.Data.createSignalA2uiMessageProcessor();\n  #a2uiClient = new A2UIClient();\n  #snackbar: Snackbar | undefined = undefined;\n  #pendingSnackbarMessages: Array<{\n    message: SnackbarMessage;\n    replaceAll: boolean;\n  }> = [];\n\n  #maybeRenderError() {\n    if (!this.#error) return nothing;\n\n    return html`<div class=\"error\">${this.#error}</div>`;\n  }\n\n  connectedCallback() {\n    super.connectedCallback();\n\n    // Load config from URL\n    const urlParams = new URLSearchParams(window.location.search);\n    const appKey = urlParams.get(\"app\") || \"restaurant\";\n    this.config = configs[appKey] || configs.restaurant;\n\n    // Apply the theme directly, which will use the Lit context.\n    if (this.config.theme) {\n      this.theme = this.config.theme;\n    }\n\n    window.document.title = this.config.title;\n    window.document.documentElement.style.setProperty(\n      \"--background\",\n      this.config.background\n    );\n\n    // Initialize client with configured URL\n    this.#a2uiClient = new A2UIClient(this.config.serverUrl);\n  }\n\n  render() {\n    return [\n      this.#renderThemeToggle(),\n      this.#maybeRenderForm(),\n      this.#maybeRenderData(),\n      this.#maybeRenderError(),\n    ];\n  }\n\n  #renderThemeToggle() {\n    return html` <div>\n      <button\n        @click=${(evt: Event) => {\n        if (!(evt.target instanceof HTMLButtonElement)) return;\n        const { colorScheme } = window.getComputedStyle(evt.target);\n        if (colorScheme === \"dark\") {\n          document.body.classList.add(\"light\");\n          document.body.classList.remove(\"dark\");\n        } else {\n          document.body.classList.add(\"dark\");\n          document.body.classList.remove(\"light\");\n        }\n      }}\n        class=\"theme-toggle\"\n      >\n        <span class=\"g-icon filled-heavy\"></span>\n      </button>\n    </div>`;\n  }\n\n  #maybeRenderForm() {\n    if (this.#requesting) return nothing;\n    if (this.#lastMessages.length > 0) return nothing;\n\n    return html` <form\n      @submit=${async (evt: Event) => {\n        evt.preventDefault();\n        if (!(evt.target instanceof HTMLFormElement)) {\n          return;\n        }\n        const data = new FormData(evt.target);\n        const body = data.get(\"body\") ?? null;\n        if (!body) {\n          return;\n        }\n        const message = body as v0_8.Types.A2UIClientEventMessage;\n        await this.#sendAndProcessMessage(message);\n      }}\n    >\n      ${this.config.heroImage\n        ? html`<div\n            style=${styleMap({\n          \"--background-image-light\": `url(${this.config.heroImage})`,\n          \"--background-image-dark\": `url(${this.config.heroImageDark ?? this.config.heroImage\n            })`,\n        })}\n            id=\"hero-img\"\n          ></div>`\n        : nothing}\n      <h1 class=\"app-title\">${this.config.title}</h1>\n      <div>\n        <input\n          required\n          value=\"${this.config.placeholder}\"\n          autocomplete=\"off\"\n          id=\"body\"\n          name=\"body\"\n          type=\"text\"\n          ?disabled=${this.#requesting}\n        />\n        <button type=\"submit\" ?disabled=${this.#requesting}>\n          <span class=\"g-icon filled-heavy\">send</span>\n        </button>\n      </div>\n    </form>`;\n  }\n\n  #startLoadingAnimation() {\n    if (\n      Array.isArray(this.config.loadingText) &&\n      this.config.loadingText.length > 1\n    ) {\n      this.#loadingTextIndex = 0;\n      this.#loadingInterval = window.setInterval(() => {\n        this.#loadingTextIndex =\n          (this.#loadingTextIndex + 1) %\n          (this.config.loadingText as string[]).length;\n      }, 2000);\n    }\n  }\n\n  #stopLoadingAnimation() {\n    if (this.#loadingInterval) {\n      clearInterval(this.#loadingInterval);\n      this.#loadingInterval = undefined;\n    }\n  }\n\n  async #sendMessage(\n    message: v0_8.Types.A2UIClientEventMessage\n  ): Promise<v0_8.Types.ServerToClientMessage[]> {\n    try {\n      this.#requesting = true;\n      this.#startLoadingAnimation();\n      const response = this.#a2uiClient.send(message);\n      await response;\n      this.#requesting = false;\n      this.#stopLoadingAnimation();\n\n      return response;\n    } catch (err) {\n      this.snackbar(err as string, SnackType.ERROR);\n    } finally {\n      this.#requesting = false;\n      this.#stopLoadingAnimation();\n    }\n\n    return [];\n  }\n\n  #maybeRenderData() {\n    if (this.#requesting) {\n      let text = \"Awaiting an answer...\";\n      if (this.config.loadingText) {\n        if (Array.isArray(this.config.loadingText)) {\n          text = this.config.loadingText[this.#loadingTextIndex];\n        } else {\n          text = this.config.loadingText;\n        }\n      }\n\n      return html` <div class=\"pending\">\n        <div class=\"spinner\"></div>\n        <div class=\"loading-text\">${text}</div>\n      </div>`;\n    }\n\n    const surfaces = this.#processor.getSurfaces();\n    if (surfaces.size === 0) {\n      return nothing;\n    }\n\n    return html`<section id=\"surfaces\">\n      ${repeat(\n      this.#processor.getSurfaces(),\n      ([surfaceId]) => surfaceId,\n      ([surfaceId, surface]) => {\n        return html`<a2ui-surface\n              @a2uiaction=${async (\n          evt: v0_8.Events.StateEvent<\"a2ui.action\">\n        ) => {\n            const [target] = evt.composedPath();\n            if (!(target instanceof HTMLElement)) {\n              return;\n            }\n\n            const context: v0_8.Types.A2UIClientEventMessage[\"userAction\"][\"context\"] =\n              {};\n            if (evt.detail.action.context) {\n              const srcContext = evt.detail.action.context;\n              for (const item of srcContext) {\n                if (item.value.literalBoolean) {\n                  context[item.key] = item.value.literalBoolean;\n                } else if (item.value.literalNumber) {\n                  context[item.key] = item.value.literalNumber;\n                } else if (item.value.literalString) {\n                  context[item.key] = item.value.literalString;\n                } else if (item.value.path) {\n                  const path = this.#processor.resolvePath(\n                    item.value.path,\n                    evt.detail.dataContextPath\n                  );\n                  const value = this.#processor.getData(\n                    evt.detail.sourceComponent,\n                    path,\n                    surfaceId\n                  );\n                  context[item.key] = value;\n                }\n              }\n            }\n\n            const message: v0_8.Types.A2UIClientEventMessage = {\n              userAction: {\n                name: evt.detail.action.name,\n                surfaceId,\n                sourceComponentId: target.id,\n                timestamp: new Date().toISOString(),\n                context,\n              },\n            };\n\n            await this.#sendAndProcessMessage(message);\n          }}\n              .surfaceId=${surfaceId}\n              .surface=${surface}\n              .processor=${this.#processor}\n            ></a2-uisurface>`;\n      }\n    )}\n    </section>`;\n  }\n\n  async #sendAndProcessMessage(request) {\n    const messages = await this.#sendMessage(request);\n\n    console.log(messages);\n\n    this.#lastMessages = messages;\n    this.#processor.clearSurfaces();\n    this.#processor.processMessages(messages);\n  }\n\n  snackbar(\n    message: string | HTMLTemplateResult,\n    type: SnackType,\n    actions: SnackbarAction[] = [],\n    persistent = false,\n    id = globalThis.crypto.randomUUID(),\n    replaceAll = false\n  ) {\n    if (!this.#snackbar) {\n      this.#pendingSnackbarMessages.push({\n        message: {\n          id,\n          message,\n          type,\n          persistent,\n          actions,\n        },\n        replaceAll,\n      });\n      return;\n    }\n\n    return this.#snackbar.show(\n      {\n        id,\n        message,\n        type,\n        persistent,\n        actions,\n      },\n      replaceAll\n    );\n  }\n\n  unsnackbar(id?: SnackbarUUID) {\n    if (!this.#snackbar) {\n      return;\n    }\n\n    this.#snackbar.hide(id);\n  }\n}\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/shell/client.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { Message, Part, SendMessageSuccessResponse, Task } from \"@a2a-js/sdk\";\nimport { A2AClient } from \"@a2a-js/sdk/client\";\nimport { v0_8 } from \"@a2ui/lit\";\n\nconst A2AUI_MIME_TYPE = \"application/json+a2aui\";\n\nexport class A2UIClient {\n  #serverUrl: string;\n  #client: A2AClient | null = null;\n\n  constructor(serverUrl: string = \"\") {\n    this.#serverUrl = serverUrl;\n  }\n\n  #ready: Promise<void> = Promise.resolve();\n  get ready() {\n    return this.#ready;\n  }\n\n  async #getClient() {\n    if (!this.#client) {\n      // Default to localhost:10002 if no URL provided (fallback for restaurant app default)\n      const baseUrl = this.#serverUrl || \"http://localhost:10002\";\n\n      this.#client = await A2AClient.fromCardUrl(\n        `${baseUrl}/.well-known/agent-card.json`,\n        {\n          fetchImpl: async (url, init) => {\n            const headers = new Headers(init?.headers);\n            headers.set(\"X-A2A-Extensions\", \"https://a2ui.org/a2a-extension/a2ui/v0.8\");\n            return fetch(url, { ...init, headers });\n          }\n        }\n      );\n    }\n    return this.#client;\n  }\n\n  async send(\n    message: v0_8.Types.A2UIClientEventMessage | string\n  ): Promise<v0_8.Types.ServerToClientMessage[]> {\n    const client = await this.#getClient();\n\n    let parts: Part[] = [];\n\n    if (typeof message === 'string') {\n      // Try to parse as JSON first, just in case\n      try {\n        const parsed = JSON.parse(message);\n        if (typeof parsed === 'object' && parsed !== null) {\n          parts = [{\n            kind: \"data\",\n            data: parsed as unknown as Record<string, unknown>,\n            mimeType: A2AUI_MIME_TYPE,\n          } as Part];\n        } else {\n          parts = [{ kind: \"text\", text: message }];\n        }\n      } catch {\n        parts = [{ kind: \"text\", text: message }];\n      }\n    } else {\n      parts = [{\n        kind: \"data\",\n        data: message as unknown as Record<string, unknown>,\n        mimeType: A2AUI_MIME_TYPE,\n      } as Part];\n    }\n\n    const response = await client.sendMessage({\n      message: {\n        messageId: crypto.randomUUID(),\n        role: \"user\",\n        parts: parts,\n        kind: \"message\",\n      },\n    });\n\n    if (\"error\" in response) {\n      throw new Error(response.error.message);\n    }\n\n    const result = (response as SendMessageSuccessResponse).result as Task;\n    if (result.kind === \"task\" && result.status.message?.parts) {\n      const messages: v0_8.Types.ServerToClientMessage[] = [];\n      for (const part of result.status.message.parts) {\n        if (part.kind === 'data') {\n          messages.push(part.data as v0_8.Types.ServerToClientMessage);\n        }\n      }\n      return messages;\n    }\n\n    // const result = (response as SendMessageSuccessResponse).result as Message;\n    // if (result.kind === \"message\" && result.parts) {\n    //   const messages: v0_8.Types.ServerToClientMessage[] = [];\n    //   for (const part of result.parts) {\n    //     if (part.kind === 'data') {\n    //       // Parse data.arguments (JSON string) to object\n    //       const parsedData = typeof part.data?.arguments === 'string'\n    //         ? JSON.parse(part.data.arguments)\n    //         : part.data?.arguments;\n    //       messages.push(parsedData as v0_8.Types.ServerToClientMessage);\n    //     }\n    //   }\n    //   return messages;\n    // }\n\n    return [];\n  }\n}\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/shell/configs/contacts.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { cloneDefaultTheme } from \"../theme/clone-default-theme.js\";\nimport { AppConfig } from \"./types.js\";\nimport { v0_8 } from \"@a2ui/lit\";\n\n/** Elements */\n\nconst a = {\n  \"typography-f-sf\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-500\": true,\n  \"layout-as-n\": true,\n  \"layout-dis-iflx\": true,\n  \"layout-al-c\": true,\n};\n\nconst heading = {\n  \"typography-f-sf\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-500\": true,\n  \"layout-mt-0\": true,\n  \"layout-mb-2\": true,\n  \"color-c-n10\": true,\n};\n\nconst orderedList = {\n  \"typography-f-s\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-400\": true,\n  \"layout-m-0\": true,\n  \"typography-sz-bm\": true,\n  \"layout-as-n\": true,\n};\n\nconst unorderedList = {\n  \"typography-f-s\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-400\": true,\n  \"layout-m-0\": true,\n  \"typography-sz-bm\": true,\n  \"layout-as-n\": true,\n};\n\nconst listItem = {\n  \"typography-f-s\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-400\": true,\n  \"layout-m-0\": true,\n  \"typography-sz-bm\": true,\n  \"layout-as-n\": true,\n};\n\nconst theme: v0_8.Types.Theme = {\n  ...cloneDefaultTheme(),\n  additionalStyles: {\n    Card: {\n      \"min-width\": \"320px\",\n      \"max-width\": \"400px\",\n      margin: \"0 auto\",\n      background:\n        \"linear-gradient(135deg, light-dark(#ffffff99, #ffffff44) 0%, light-dark(#ffffff, #ffffff04) 100%)\",\n      border: \"1px solid light-dark(transparent, #ffffff35)\",\n      boxShadow:\n        \"inset 0 20px 48px light-dark(rgba(0, 0, 0, 0.02), rgba(255, 255, 255, 0.08))\",\n    },\n    Button: {\n      \"--p-70\": \"light-dark(var(--p-60), var(--n-10))\",\n      \"--n-60\": \"light-dark(var(--n-100), var(--n-0))\",\n    },\n    Image: {\n      \"max-width\": \"120px\",\n      \"max-height\": \"120px\",\n      marginLeft: \"auto\",\n      marginRight: \"auto\",\n    },\n    Text: {\n      \"--n-40\": \"light-dark(var(--p-60), var(--n-90))\",\n    },\n  },\n  components: {\n    AudioPlayer: {},\n    Button: {\n      \"layout-pt-2\": true,\n      \"layout-pb-2\": true,\n      \"layout-pl-5\": true,\n      \"layout-pr-5\": true,\n      \"border-br-2\": true,\n      \"border-bw-0\": true,\n      \"border-bs-s\": true,\n      \"color-bgc-p30\": true,\n      \"color-c-n100\": true,\n      \"behavior-ho-70\": true,\n    },\n    Card: {\n      \"border-br-4\": true,\n      \"color-bgc-p100\": true,\n      \"layout-pt-10\": true,\n      \"layout-pb-10\": true,\n      \"layout-pl-4\": true,\n      \"layout-pr-4\": true,\n    },\n    CheckBox: {\n      element: {\n        \"layout-m-0\": true,\n        \"layout-mr-2\": true,\n        \"layout-p-2\": true,\n        \"border-br-12\": true,\n        \"border-bw-1\": true,\n        \"border-bs-s\": true,\n        \"color-bgc-p100\": true,\n        \"color-bc-p60\": true,\n        \"color-c-n30\": true,\n        \"color-c-p30\": true,\n      },\n      label: {\n        \"color-c-p30\": true,\n        \"typography-f-sf\": true,\n        \"typography-v-r\": true,\n        \"typography-w-400\": true,\n        \"layout-flx-1\": true,\n        \"typography-sz-ll\": true,\n      },\n      container: {\n        \"layout-dsp-iflex\": true,\n        \"layout-al-c\": true,\n      },\n    },\n    Column: {},\n    DateTimeInput: {\n      container: {},\n      label: {},\n      element: {\n        \"layout-pt-2\": true,\n        \"layout-pb-2\": true,\n        \"layout-pl-3\": true,\n        \"layout-pr-3\": true,\n        \"border-br-12\": true,\n        \"border-bw-1\": true,\n        \"border-bs-s\": true,\n        \"color-bgc-p100\": true,\n        \"color-bc-p60\": true,\n        \"color-c-n30\": true,\n      },\n    },\n    Divider: {\n      \"color-bgc-n90\": true,\n      \"layout-mt-6\": true,\n      \"layout-mb-6\": true,\n    },\n    Image: {\n      all: {\n        \"border-br-50pc\": true,\n        \"layout-el-cv\": true,\n        \"layout-w-100\": true,\n        \"layout-h-100\": true,\n        \"layout-dsp-flexhor\": true,\n        \"layout-al-c\": true,\n        \"layout-sp-c\": true,\n        \"layout-mb-3\": true,\n      },\n      avatar: {},\n      header: {},\n      icon: {},\n      largeFeature: {},\n      mediumFeature: {},\n      smallFeature: {},\n    },\n    Icon: {\n      \"border-br-1\": true,\n      \"layout-p-2\": true,\n      \"color-bgc-n98\": true,\n      \"layout-dsp-flexhor\": true,\n      \"layout-al-c\": true,\n      \"layout-sp-c\": true,\n    },\n    List: {\n      \"layout-g-4\": true,\n      \"layout-p-2\": true,\n    },\n    Modal: {\n      backdrop: { \"color-bbgc-p60_20\": true },\n      element: {\n        \"border-br-2\": true,\n        \"color-bgc-p100\": true,\n        \"layout-p-4\": true,\n        \"border-bw-1\": true,\n        \"border-bs-s\": true,\n        \"color-bc-p80\": true,\n      },\n    },\n    MultipleChoice: {\n      container: {},\n      label: {},\n      element: {},\n    },\n    Row: {\n      \"layout-g-4\": true,\n      \"layout-mb-3\": true,\n    },\n    Slider: {\n      container: {},\n      label: {},\n      element: {},\n    },\n    Tabs: {\n      container: {},\n      controls: { all: {}, selected: {} },\n      element: {},\n    },\n    Text: {\n      all: {\n        \"layout-w-100\": true,\n        \"layout-g-2\": true,\n        \"color-c-p30\": true,\n      },\n      h1: {\n        \"typography-f-sf\": true,\n        \"typography-ta-c\": true,\n        \"typography-v-r\": true,\n        \"typography-w-500\": true,\n        \"layout-mt-0\": true,\n        \"layout-mr-0\": true,\n        \"layout-ml-0\": true,\n        \"layout-mb-2\": true,\n        \"layout-p-0\": true,\n        \"typography-sz-tl\": true,\n      },\n      h2: {\n        \"typography-f-sf\": true,\n        \"typography-ta-c\": true,\n        \"typography-v-r\": true,\n        \"typography-w-500\": true,\n        \"layout-mt-0\": true,\n        \"layout-mr-0\": true,\n        \"layout-ml-0\": true,\n        \"layout-mb-2\": true,\n        \"layout-p-0\": true,\n        \"typography-sz-tl\": true,\n      },\n      h3: {\n        \"typography-f-sf\": true,\n        \"typography-ta-c\": true,\n        \"typography-v-r\": true,\n        \"typography-w-500\": true,\n        \"layout-mt-0\": true,\n        \"layout-mr-0\": true,\n        \"layout-ml-0\": true,\n        \"layout-mb-0\": true,\n        \"layout-p-0\": true,\n        \"typography-sz-ts\": true,\n      },\n      h4: {\n        \"typography-f-sf\": true,\n        \"typography-ta-c\": true,\n        \"typography-v-r\": true,\n        \"typography-w-500\": true,\n        \"layout-mt-0\": true,\n        \"layout-mr-0\": true,\n        \"layout-ml-0\": true,\n        \"layout-mb-0\": true,\n        \"layout-p-0\": true,\n        \"typography-sz-bl\": true,\n      },\n      h5: {\n        \"typography-f-sf\": true,\n        \"typography-ta-c\": true,\n        \"typography-v-r\": true,\n        \"typography-w-500\": true,\n        \"layout-mt-0\": true,\n        \"layout-mr-0\": true,\n        \"layout-ml-0\": true,\n        \"layout-mb-0\": true,\n        \"layout-p-0\": true,\n        \"color-c-n30\": true,\n        \"typography-sz-bm\": true,\n        \"layout-mb-1\": true,\n      },\n      body: {},\n      caption: {},\n    },\n    TextField: {\n      container: {\n        \"typography-sz-bm\": true,\n        \"layout-w-100\": true,\n        \"layout-g-2\": true,\n        \"layout-dsp-flexhor\": true,\n        \"layout-al-c\": true,\n      },\n      label: {\n        \"layout-flx-0\": true,\n      },\n      element: {\n        \"typography-sz-bm\": true,\n        \"layout-pt-2\": true,\n        \"layout-pb-2\": true,\n        \"layout-pl-3\": true,\n        \"layout-pr-3\": true,\n        \"border-br-12\": true,\n        \"border-bw-1\": true,\n        \"border-bs-s\": true,\n        \"color-bgc-p100\": true,\n        \"color-bc-p60\": true,\n        \"color-c-n30\": true,\n        \"color-c-p30\": true,\n      },\n    },\n    Video: {\n      \"border-br-5\": true,\n      \"layout-el-cv\": true,\n    },\n  },\n};\n\nexport const config: AppConfig = {\n  key: \"contacts\",\n  title: \"Contact Manager\",\n  background: `radial-gradient(at 0% 0%, light-dark(rgba(45, 212, 191, 0.4), rgba(20, 184, 166, 0.2)) 0px, transparent 50%),\n     radial-gradient(at 100% 0%, light-dark(rgba(56, 189, 248, 0.4), rgba(14, 165, 233, 0.2)) 0px, transparent 50%),\n     radial-gradient(at 100% 100%, light-dark(rgba(163, 230, 53, 0.4), rgba(132, 204, 22, 0.2)) 0px, transparent 50%),\n     radial-gradient(at 0% 100%, light-dark(rgba(52, 211, 153, 0.4), rgba(16, 185, 129, 0.2)) 0px, transparent 50%),\n     linear-gradient(120deg, light-dark(#f0fdf4, #022c22) 0%, light-dark(#dcfce7, #064e3b) 100%)`,\n  placeholder: \"Alex Jordan\",\n  loadingText: [\n    \"Searching contacts...\",\n    \"Looking up details...\",\n    \"Verifying information...\",\n    \"Just a moment...\",\n  ],\n  serverUrl: \"http://localhost:10003\",\n  theme,\n};\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/shell/configs/restaurant.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { AppConfig } from \"./types.js\";\n\nexport const config: AppConfig = {\n  key: \"restaurant\",\n  title: \"A2UI Agent\",\n  heroImage: \"/hero.png\",\n  heroImageDark: \"/hero-dark.png\",\n  background: `radial-gradient(\n    at 0% 0%,\n    light-dark(rgba(161, 196, 253, 0.3), rgba(6, 182, 212, 0.15)) 0px,\n    transparent 50%\n  ),\n  radial-gradient(\n    at 100% 0%,\n    light-dark(rgba(255, 226, 226, 0.3), rgba(59, 130, 246, 0.15)) 0px,\n    transparent 50%\n  ),\n  radial-gradient(\n    at 100% 100%,\n    light-dark(rgba(162, 210, 255, 0.3), rgba(20, 184, 166, 0.15)) 0px,\n    transparent 50%\n  ),\n  radial-gradient(\n    at 0% 100%,\n    light-dark(rgba(255, 200, 221, 0.3), rgba(99, 102, 241, 0.15)) 0px,\n    transparent 50%\n  ),\n  linear-gradient(\n    120deg,\n    light-dark(#f0f4f8, #0f172a) 0%,\n    light-dark(#e2e8f0, #1e293b) 100%\n  )`,\n  placeholder: \"Help me test my English.\",\n  loadingText: [\n    \"A2UI Agent is working on your request...\",\n    \"Analyzing your information...\",\n    \"Working...\",\n  ],\n  serverUrl: \"http://localhost:10002\",\n};\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/shell/configs/types.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { v0_8 } from \"@a2ui/lit\";\n\n/**\n * Configuration interface for the Universal App Shell.\n */\nexport interface AppConfig {\n  /** Unique key for the app (e.g., 'restaurant', 'contacts') */\n  key: string;\n  /** Display title of the application */\n  title: string;\n  /** The background for the page */\n  background?: string;\n  /** Path to the hero image */\n  heroImage?: string;\n  /** Path to the hero image */\n  heroImageDark?: string;\n  /** Placeholder text for the input field */\n  placeholder: string;\n  /** Text to display while loading (optional). Can be a single string or an array of strings to rotate. */\n  loadingText?: string | string[];\n  /** Optional server URL for the agent (e.g., http://localhost:10003) */\n  serverUrl?: string;\n  /** Theme overrides (CSS Variables) */\n  theme?: v0_8.Types.Theme;\n}\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/shell/events/events.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { HTMLTemplateResult } from \"lit\";\n\nconst eventInit = {\n  bubbles: true,\n  cancelable: true,\n  composed: true,\n};\n\nexport class SnackbarActionEvent extends Event {\n  static eventName = \"snackbaraction\";\n\n  constructor(\n    public readonly action: string,\n    public readonly value?: HTMLTemplateResult | string,\n    public readonly callback?: () => void\n  ) {\n    super(SnackbarActionEvent.eventName, { ...eventInit });\n  }\n}\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/shell/index.html",
    "content": "<!DOCTYPE html>\n<!--\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Loading...</title>\n\n    <link\n      rel=\"stylesheet\"\n      href=\"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&icon_names=account_circle,add,arrow_back,arrow_drop_down,arrow_forward,attach_file,calendar_today,call,camera,check,check_circle,close,communication,content_copy,dark_mode,delete,download,draw,edit,error,event,favorite,favorite_off,folder,help,home,info,light_mode,location_on,lock,lock_open,mail,menu,mobile_layout,more_horiz,more_vert,notifications,notifications_off,payment,pen_size_1,person,phone,photo,print,progress_activity,rectangle,refresh,search,send,settings,share,shopping_cart,star,star_half,star_off,upload,visibility,visibility_off,warning\"\n    />\n    <link\n      href=\"https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap\"\n      rel=\"stylesheet\"\n    />\n\n    <style>\n      :root {\n        --n-100: #ffffff;\n        --n-99: #fcfcfc;\n        --n-98: #f9f9f9;\n        --n-95: #f1f1f1;\n        --n-90: #e2e2e2;\n        --n-80: #c6c6c6;\n        --n-70: #ababab;\n        --n-60: #919191;\n        --n-50: #777777;\n        --n-40: #5e5e5e;\n        --n-35: #525252;\n        --n-30: #474747;\n        --n-25: #3b3b3b;\n        --n-20: #303030;\n        --n-15: #262626;\n        --n-10: #1b1b1b;\n        --n-5: #111111;\n        --n-0: #000000;\n\n        --p-100: #ffffff;\n        --p-99: #fffbff;\n        --p-98: #fcf8ff;\n        --p-95: #f2efff;\n        --p-90: #e1e0ff;\n        --p-80: #c0c1ff;\n        --p-70: #a0a3ff;\n        --p-60: #8487ea;\n        --p-50: #6a6dcd;\n        --p-40: #5154b3;\n        --p-35: #4447a6;\n        --p-30: #383b99;\n        --p-25: #2c2e8d;\n        --p-20: #202182;\n        --p-15: #131178;\n        --p-10: #06006c;\n        --p-5: #03004d;\n        --p-0: #000000;\n\n        --s-100: #ffffff;\n        --s-99: #fffbff;\n        --s-98: #fcf8ff;\n        --s-95: #f2efff;\n        --s-90: #e2e0f9;\n        --s-80: #c6c4dd;\n        --s-70: #aaa9c1;\n        --s-60: #8f8fa5;\n        --s-50: #75758b;\n        --s-40: #5d5c72;\n        --s-35: #515165;\n        --s-30: #454559;\n        --s-25: #393a4d;\n        --s-20: #2e2f42;\n        --s-15: #242437;\n        --s-10: #191a2c;\n        --s-5: #0f0f21;\n        --s-0: #000000;\n\n        --t-100: #ffffff;\n        --t-99: #fffbff;\n        --t-98: #fff8f9;\n        --t-95: #ffecf4;\n        --t-90: #ffd8ec;\n        --t-80: #e9b9d3;\n        --t-70: #cc9eb8;\n        --t-60: #af849d;\n        --t-50: #946b83;\n        --t-40: #79536a;\n        --t-35: #6c475d;\n        --t-30: #5f3c51;\n        --t-25: #523146;\n        --t-20: #46263a;\n        --t-15: #3a1b2f;\n        --t-10: #2e1125;\n        --t-5: #22071a;\n        --t-0: #000000;\n\n        --nv-100: #ffffff;\n        --nv-99: #fffbff;\n        --nv-98: #fcf8ff;\n        --nv-95: #f2effa;\n        --nv-90: #e4e1ec;\n        --nv-80: #c8c5d0;\n        --nv-70: #acaab4;\n        --nv-60: #918f9a;\n        --nv-50: #777680;\n        --nv-40: #5e5d67;\n        --nv-35: #52515b;\n        --nv-30: #46464f;\n        --nv-25: #3b3b43;\n        --nv-20: #303038;\n        --nv-15: #25252d;\n        --nv-10: #1b1b23;\n        --nv-5: #101018;\n        --nv-0: #000000;\n\n        --e-100: #ffffff;\n        --e-99: #fffbff;\n        --e-98: #fff8f7;\n        --e-95: #ffedea;\n        --e-90: #ffdad6;\n        --e-80: #ffb4ab;\n        --e-70: #ff897d;\n        --e-60: #ff5449;\n        --e-50: #de3730;\n        --e-40: #ba1a1a;\n        --e-35: #a80710;\n        --e-30: #93000a;\n        --e-25: #7e0007;\n        --e-20: #690005;\n        --e-15: #540003;\n        --e-10: #410002;\n        --e-5: #2d0001;\n        --e-0: #000000;\n\n        --primary: #137fec;\n        --text-color: #fff;\n        --background: light-dark(#f6f7f8, #101922);\n        --border-color: oklch(\n          from var(--background-light) l c h / calc(alpha * 0.15)\n        );\n        --elevated-background-light: oklch(\n          from var(--background-light) l c h / calc(alpha * 0.05)\n        );\n        --bb-grid-size: 4px;\n        --bb-grid-size-2: calc(var(--bb-grid-size) * 2);\n        --bb-grid-size-3: calc(var(--bb-grid-size) * 3);\n        --bb-grid-size-4: calc(var(--bb-grid-size) * 4);\n        --bb-grid-size-5: calc(var(--bb-grid-size) * 5);\n        --bb-grid-size-6: calc(var(--bb-grid-size) * 6);\n        --bb-grid-size-7: calc(var(--bb-grid-size) * 7);\n        --bb-grid-size-8: calc(var(--bb-grid-size) * 8);\n        --bb-grid-size-9: calc(var(--bb-grid-size) * 9);\n        --bb-grid-size-10: calc(var(--bb-grid-size) * 10);\n        --bb-grid-size-11: calc(var(--bb-grid-size) * 11);\n        --bb-grid-size-12: calc(var(--bb-grid-size) * 12);\n        --bb-grid-size-13: calc(var(--bb-grid-size) * 13);\n        --bb-grid-size-14: calc(var(--bb-grid-size) * 14);\n        --bb-grid-size-15: calc(var(--bb-grid-size) * 15);\n        --bb-grid-size-16: calc(var(--bb-grid-size) * 16);\n\n        --background: radial-gradient(\n            at 0% 0%,\n            light-dark(rgba(161, 196, 253, 0.3), rgba(6, 182, 212, 0.15)) 0px,\n            transparent 50%\n          ),\n          radial-gradient(\n            at 100% 0%,\n            light-dark(rgba(255, 226, 226, 0.3), rgba(59, 130, 246, 0.15)) 0px,\n            transparent 50%\n          ),\n          radial-gradient(\n            at 100% 100%,\n            light-dark(rgba(162, 210, 255, 0.3), rgba(20, 184, 166, 0.15)) 0px,\n            transparent 50%\n          ),\n          radial-gradient(\n            at 0% 100%,\n            light-dark(rgba(255, 200, 221, 0.3), rgba(99, 102, 241, 0.15)) 0px,\n            transparent 50%\n          ),\n          linear-gradient(\n            120deg,\n            light-dark(#f0f4f8, #0f172a) 0%,\n            light-dark(#e2e8f0, #1e293b) 100%\n          );\n      }\n\n      * {\n        box-sizing: border-box;\n      }\n\n      html,\n      body {\n        --font-family: \"Outfit\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n        --font-family-flex: \"Outfit\", \"Helvetica Neue\", Helvetica, Arial,\n          sans-serif;\n        --font-family-mono: monospace;\n\n        background: var(--background);\n        font-family: var(--font-family);\n        margin: 0;\n        padding: 0;\n        width: 100svw;\n        height: 100svh;\n        overflow: auto;\n\n        --color-scheme: light;\n        color-scheme: var(--color-scheme);\n\n        &.light {\n          --color-scheme: light;\n        }\n\n        &.dark {\n          --color-scheme: dark;\n        }\n      }\n\n      @media (prefers-color-scheme: dark) {\n        html,\n        body {\n          --color-scheme: dark;\n        }\n      }\n    </style>\n  </head>\n\n  <body>\n    <a2ui-shell></a2ui-shell>\n    <script type=\"module\" src=\"./app.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/shell/middleware/a2a.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { IncomingMessage, ServerResponse } from \"http\";\nimport { Plugin, ViteDevServer } from \"vite\";\nimport { A2AClient } from \"@a2a-js/sdk/client\";\nimport {\n  MessageSendParams,\n  Part,\n  SendMessageSuccessResponse,\n  Task,\n} from \"@a2a-js/sdk\";\nimport { v4 as uuidv4 } from \"uuid\";\n\nconst A2AUI_MIME_TYPE = \"application/json+a2aui\";\n\nconst fetchWithCustomHeader: typeof fetch = async (url, init) => {\n  const headers = new Headers(init?.headers);\n  headers.set(\"X-A2A-Extensions\", \"https://a2ui.org/a2a-extension/a2ui/v0.8\");\n\n  const newInit = { ...init, headers };\n  return fetch(url, newInit);\n};\n\nconst isJson = (str: string) => {\n  try {\n    const parsed = JSON.parse(str);\n    return (\n      typeof parsed === \"object\" && parsed !== null && !Array.isArray(parsed)\n    );\n  } catch (err) {\n    console.warn(err);\n    return false;\n  }\n};\n\nlet client: A2AClient | null = null;\nconst createOrGetClient = async () => {\n  if (!client) {\n    // Create a client pointing to the agent's Agent Card URL.\n    client = await A2AClient.fromCardUrl(\n      \"http://localhost:10002/.well-known/agent-card.json\",\n      { fetchImpl: fetchWithCustomHeader }\n    );\n  }\n\n  return client;\n};\n\nexport const plugin = (): Plugin => {\n  return {\n    name: \"a2a-handler\",\n    configureServer(server: ViteDevServer) {\n      server.middlewares.use(\n        \"/a2a\",\n        async (req: IncomingMessage, res: ServerResponse, next: () => void) => {\n          if (req.method === \"POST\") {\n            let originalBody = \"\";\n\n            req.on(\"data\", (chunk) => {\n              originalBody += chunk.toString();\n            });\n\n            req.on(\"end\", async () => {\n              let sendParams: MessageSendParams;\n\n              if (isJson(originalBody)) {\n                console.log(\n                  \"[a2a-middleware] Received JSON UI event:\",\n                  originalBody\n                );\n\n                const clientEvent = JSON.parse(originalBody);\n                sendParams = {\n                  message: {\n                    messageId: uuidv4(),\n                    role: \"user\",\n                    parts: [\n                      {\n                        kind: \"data\",\n                        data: clientEvent,\n                        metadata: { 'mimeType': A2AUI_MIME_TYPE },\n                      } as Part,\n                    ],\n                    kind: \"message\",\n                  },\n                };\n              } else {\n                console.log(\n                  \"[a2a-middleware] Received text query:\",\n                  originalBody\n                );\n                sendParams = {\n                  message: {\n                    messageId: uuidv4(),\n                    role: \"user\",\n                    parts: [\n                      {\n                        kind: \"text\",\n                        text: originalBody,\n                      },\n                    ],\n                    kind: \"message\",\n                  },\n                };\n              }\n\n              const client = await createOrGetClient();\n              const response = await client.sendMessage(sendParams);\n              if (\"error\" in response) {\n                console.error(\"Error:\", response.error.message);\n                res.statusCode = 500;\n                res.setHeader(\"Content-Type\", \"application/json\");\n                res.end(JSON.stringify({ error: response.error.message }));\n                return;\n              } else {\n                const result = (response as SendMessageSuccessResponse)\n                  .result as Task;\n                if (result.kind === \"task\") {\n                  res.statusCode = 200;\n                  res.setHeader(\"Content-Type\", \"application/json\");\n                  res.end(JSON.stringify(result.status.message?.parts));\n                  return;\n                }\n              }\n\n              res.statusCode = 200;\n              res.setHeader(\"Content-Type\", \"application/json\");\n              res.end(JSON.stringify([]));\n            });\n\n            return;\n          } else {\n            next();\n          }\n        }\n      );\n    },\n  };\n};\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/shell/middleware/index.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nexport * as A2AMiddleware from \"./a2a.js\";\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/shell/package.json",
    "content": "{\n  \"name\": \"@a2ui/shell\",\n  \"private\": true,\n  \"version\": \"0.8.1\",\n  \"description\": \"A2UI Universal Shell\",\n  \"main\": \"./dist/shell.js\",\n  \"types\": \"./dist/shell.d.ts\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"prepack\": \"npm run build\",\n    \"build\": \"wireit\",\n    \"build:tsc\": \"wireit\",\n    \"dev\": \"npm run serve --watch\",\n    \"test\": \"wireit\",\n    \"serve\": \"wireit\"\n  },\n  \"wireit\": {\n    \"serve\": {\n      \"command\": \"vite dev\",\n      \"dependencies\": [\n        \"build\"\n      ],\n      \"service\": true\n    },\n    \"test\": {\n      \"command\": \"node --test --enable-source-maps --test-reporter spec dist/src/0.8/tests/**/*.test.js\",\n      \"dependencies\": [\n        \"build\"\n      ]\n    },\n    \"build\": {\n      \"dependencies\": [\n        \"build:tsc\"\n      ]\n    },\n    \"build:tsc\": {\n      \"command\": \"tsc -b --pretty\",\n      \"env\": {\n        \"FORCE_COLOR\": \"1\"\n      },\n      \"dependencies\": [\n        \"../../../../renderers/lit:build:tsc\"\n      ],\n      \"files\": [\n        \"**/*.ts\",\n        \"tsconfig.json\"\n      ],\n      \"output\": [\n        \"dist/\",\n        \"!dist/**/*.min.js{,.map}\"\n      ],\n      \"clean\": \"if-file-deleted\"\n    }\n  },\n  \"repository\": {\n    \"directory\": \"samples/client/lit/shell\",\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/google/A2UI.git\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"keywords\": [],\n  \"author\": \"Google\",\n  \"license\": \"Apache-2.0\",\n  \"bugs\": {\n    \"url\": \"https://github.com/google/A2UI/issues\"\n  },\n  \"homepage\": \"https://github.com/google/A2UI/tree/main/web#readme\",\n  \"devDependencies\": {\n    \"dotenv\": \"^17.2.3\",\n    \"typescript\": \"^5.8.3\",\n    \"uuid\": \"^13.0.0\",\n    \"vite\": \"^7.1.11\",\n    \"wireit\": \"^0.15.0-pre.2\"\n  },\n  \"dependencies\": {\n    \"@a2a-js/sdk\": \"^0.3.4\",\n    \"@a2ui/lit\": \"file:../../../../renderers/lit\",\n    \"@google/genai\": \"^1.22.0\",\n    \"@lit-labs/signals\": \"^0.1.3\",\n    \"@lit/context\": \"^1.1.4\",\n    \"@types/node\": \"^24.7.1\",\n    \"lit\": \"^3.3.1\"\n  }\n}"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/shell/theme/clone-default-theme.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { theme } from \"./default-theme.js\";\nimport { v0_8 } from \"@a2ui/lit\";\n\nexport function cloneDefaultTheme(): v0_8.Types.Theme {\n  return structuredClone(theme);\n}\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/shell/theme/default-theme.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { v0_8 } from \"@a2ui/lit\";\n\n/** Elements */\n\nconst a = {\n  \"typography-f-sf\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-500\": true,\n  \"layout-as-n\": true,\n  \"layout-dis-iflx\": true,\n  \"layout-al-c\": true,\n  \"typography-td-none\": true,\n  \"color-c-p40\": true,\n};\n\nconst audio = {\n  \"layout-w-100\": true,\n};\n\nconst body = {\n  \"typography-f-s\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-400\": true,\n  \"layout-mt-0\": true,\n  \"layout-mb-2\": true,\n  \"typography-sz-bm\": true,\n  \"color-c-n10\": true,\n};\n\nconst button = {\n  \"typography-f-sf\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-500\": true,\n  \"layout-pt-3\": true,\n  \"layout-pb-3\": true,\n  \"layout-pl-5\": true,\n  \"layout-pr-5\": true,\n  \"layout-mb-1\": true,\n  \"border-br-16\": true,\n  \"border-bw-0\": true,\n  \"border-c-n70\": true,\n  \"border-bs-s\": true,\n  \"color-bgc-s30\": true,\n  \"behavior-ho-80\": true,\n};\n\nconst heading = {\n  \"typography-f-sf\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-500\": true,\n  \"layout-mt-0\": true,\n  \"layout-mb-2\": true,\n};\n\nconst iframe = {\n  \"behavior-sw-n\": true,\n};\n\nconst input = {\n  \"typography-f-sf\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-400\": true,\n  \"layout-pl-4\": true,\n  \"layout-pr-4\": true,\n  \"layout-pt-2\": true,\n  \"layout-pb-2\": true,\n  \"border-br-6\": true,\n  \"border-bw-1\": true,\n  \"color-bc-s70\": true,\n  \"border-bs-s\": true,\n  \"layout-as-n\": true,\n  \"color-c-n10\": true,\n};\n\nconst p = {\n  \"typography-f-s\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-400\": true,\n  \"layout-m-0\": true,\n  \"typography-sz-bm\": true,\n  \"layout-as-n\": true,\n  \"color-c-n10\": true,\n};\n\nconst orderedList = {\n  \"typography-f-s\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-400\": true,\n  \"layout-m-0\": true,\n  \"typography-sz-bm\": true,\n  \"layout-as-n\": true,\n  \"color-c-n10\": true,\n};\n\nconst unorderedList = {\n  \"typography-f-s\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-400\": true,\n  \"layout-m-0\": true,\n  \"typography-sz-bm\": true,\n  \"layout-as-n\": true,\n  \"color-c-n10\": true,\n};\n\nconst listItem = {\n  \"typography-f-s\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-400\": true,\n  \"layout-m-0\": true,\n  \"typography-sz-bm\": true,\n  \"layout-as-n\": true,\n  \"color-c-n10\": true,\n};\n\nconst pre = {\n  \"typography-f-c\": true,\n  \"typography-fs-n\": true,\n  \"typography-w-400\": true,\n  \"typography-sz-bm\": true,\n  \"typography-ws-p\": true,\n  \"layout-as-n\": true,\n};\n\nconst textarea = {\n  ...input,\n  \"layout-r-none\": true,\n  \"layout-fs-c\": true,\n};\n\nconst video = {\n  \"layout-el-cv\": true,\n};\n\nconst aLight = v0_8.Styles.merge(a, {});\nconst inputLight = v0_8.Styles.merge(input, {});\nconst textareaLight = v0_8.Styles.merge(textarea, {});\nconst buttonLight = v0_8.Styles.merge(button, {});\nconst bodyLight = v0_8.Styles.merge(body, {});\nconst pLight = v0_8.Styles.merge(p, {});\nconst preLight = v0_8.Styles.merge(pre, {});\nconst orderedListLight = v0_8.Styles.merge(orderedList, {});\nconst unorderedListLight = v0_8.Styles.merge(unorderedList, {});\nconst listItemLight = v0_8.Styles.merge(listItem, {});\n\nexport const theme: v0_8.Types.Theme = {\n  additionalStyles: {\n    Button: {\n      \"--n-35\": \"var(--n-100)\",\n      \"--n-10\": \"var(--n-0)\",\n      background:\n        \"linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)\",\n      boxShadow: \"0 4px 15px rgba(102, 126, 234, 0.4)\",\n      padding: \"12px 28px\",\n      textTransform: \"uppercase\",\n    },\n    Text: {\n      h1: {\n        color: \"transparent\",\n        background:\n          \"linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)\",\n        \"-webkit-background-clip\": \"text\",\n        \"background-clip\": \"text\",\n        \"-webkit-text-fill-color\": \"transparent\",\n      },\n      h2: {\n        color: \"transparent\",\n        background:\n          \"linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)\",\n        \"-webkit-background-clip\": \"text\",\n        \"background-clip\": \"text\",\n        \"-webkit-text-fill-color\": \"transparent\",\n      },\n      h3: {\n        color: \"transparent\",\n        background:\n          \"linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)\",\n        \"-webkit-background-clip\": \"text\",\n        \"background-clip\": \"text\",\n        \"-webkit-text-fill-color\": \"transparent\",\n      },\n      h4: {},\n      h5: {},\n      body: {},\n      caption: {},\n    },\n    Card: {\n      background:\n        \"radial-gradient(circle at top left, light-dark(transparent, rgba(6, 182, 212, 0.15)), transparent 40%), radial-gradient(circle at bottom right, light-dark(transparent, rgba(139, 92, 246, 0.15)), transparent 40%), linear-gradient(135deg, light-dark(rgba(255, 255, 255, 0.7), rgba(30, 41, 59, 0.7)), light-dark(rgba(255, 255, 255, 0.7), rgba(15, 23, 42, 0.8)))\",\n    },\n    TextField: {\n      \"--p-0\": \"light-dark(var(--n-0), #1e293b)\",\n    },\n  },\n  components: {\n    AudioPlayer: {},\n    Button: {\n      \"layout-pt-2\": true,\n      \"layout-pb-2\": true,\n      \"layout-pl-3\": true,\n      \"layout-pr-3\": true,\n      \"border-br-12\": true,\n      \"border-bw-0\": true,\n      \"border-bs-s\": true,\n      \"color-bgc-p30\": true,\n      \"behavior-ho-70\": true,\n      \"typography-w-400\": true,\n    },\n    Card: { \"border-br-9\": true, \"layout-p-4\": true, \"color-bgc-n100\": true },\n    CheckBox: {\n      element: {\n        \"layout-m-0\": true,\n        \"layout-mr-2\": true,\n        \"layout-p-2\": true,\n        \"border-br-12\": true,\n        \"border-bw-1\": true,\n        \"border-bs-s\": true,\n        \"color-bgc-p100\": true,\n        \"color-bc-p60\": true,\n        \"color-c-n30\": true,\n        \"color-c-p30\": true,\n      },\n      label: {\n        \"color-c-p30\": true,\n        \"typography-f-sf\": true,\n        \"typography-v-r\": true,\n        \"typography-w-400\": true,\n        \"layout-flx-1\": true,\n        \"typography-sz-ll\": true,\n      },\n      container: {\n        \"layout-dsp-iflex\": true,\n        \"layout-al-c\": true,\n      },\n    },\n    Column: {\n      \"layout-g-2\": true,\n    },\n    DateTimeInput: {\n      container: {\n        \"typography-sz-bm\": true,\n        \"layout-w-100\": true,\n        \"layout-g-2\": true,\n        \"layout-dsp-flexhor\": true,\n        \"layout-al-c\": true,\n        \"typography-ws-nw\": true,\n      },\n      label: {\n        \"color-c-p30\": true,\n        \"typography-sz-bm\": true,\n      },\n      element: {\n        \"layout-pt-2\": true,\n        \"layout-pb-2\": true,\n        \"layout-pl-3\": true,\n        \"layout-pr-3\": true,\n        \"border-br-2\": true,\n        \"border-bw-1\": true,\n        \"border-bs-s\": true,\n        \"color-bgc-p100\": true,\n        \"color-bc-p60\": true,\n        \"color-c-n30\": true,\n        \"color-c-p30\": true,\n      },\n    },\n    Divider: {},\n    Image: {\n      all: {\n        \"border-br-5\": true,\n        \"layout-el-cv\": true,\n        \"layout-w-100\": true,\n        \"layout-h-100\": true,\n      },\n      avatar: { \"is-avatar\": true },\n      header: {},\n      icon: {},\n      largeFeature: {},\n      mediumFeature: {},\n      smallFeature: {},\n    },\n    Icon: {},\n    List: {\n      \"layout-g-4\": true,\n      \"layout-p-2\": true,\n    },\n    Modal: {\n      backdrop: { \"color-bbgc-p60_20\": true },\n      element: {\n        \"border-br-2\": true,\n        \"color-bgc-p100\": true,\n        \"layout-p-4\": true,\n        \"border-bw-1\": true,\n        \"border-bs-s\": true,\n        \"color-bc-p80\": true,\n      },\n    },\n    MultipleChoice: {\n      container: {},\n      label: {},\n      element: {},\n    },\n    Row: {\n      \"layout-g-4\": true,\n    },\n    Slider: {\n      container: {},\n      label: {},\n      element: {},\n    },\n    Tabs: {\n      container: {},\n      controls: { all: {}, selected: {} },\n      element: {},\n    },\n    Text: {\n      all: {\n        \"layout-w-100\": true,\n        \"layout-g-2\": true,\n      },\n      h1: {\n        \"typography-f-sf\": true,\n        \"typography-v-r\": true,\n        \"typography-w-400\": true,\n        \"layout-m-0\": true,\n        \"layout-p-0\": true,\n        \"typography-sz-hs\": true,\n      },\n      h2: {\n        \"typography-f-sf\": true,\n        \"typography-v-r\": true,\n        \"typography-w-400\": true,\n        \"layout-m-0\": true,\n        \"layout-p-0\": true,\n        \"typography-sz-tl\": true,\n      },\n      h3: {\n        \"typography-f-sf\": true,\n        \"typography-v-r\": true,\n        \"typography-w-400\": true,\n        \"layout-m-0\": true,\n        \"layout-p-0\": true,\n        \"typography-sz-tl\": true,\n      },\n      h4: {\n        \"typography-f-sf\": true,\n        \"typography-v-r\": true,\n        \"typography-w-400\": true,\n        \"layout-m-0\": true,\n        \"layout-p-0\": true,\n        \"typography-sz-bl\": true,\n      },\n      h5: {\n        \"typography-f-sf\": true,\n        \"typography-v-r\": true,\n        \"typography-w-400\": true,\n        \"layout-m-0\": true,\n        \"layout-p-0\": true,\n        \"typography-sz-bm\": true,\n      },\n      body: {},\n      caption: {},\n    },\n    TextField: {\n      container: {\n        \"typography-sz-bm\": true,\n        \"layout-w-100\": true,\n        \"layout-g-2\": true,\n        \"layout-dsp-flexhor\": true,\n        \"layout-al-c\": true,\n        \"typography-ws-nw\": true,\n      },\n      label: {\n        \"layout-flx-0\": true,\n        \"color-c-p30\": true,\n      },\n      element: {\n        \"typography-sz-bm\": true,\n        \"layout-pt-2\": true,\n        \"layout-pb-2\": true,\n        \"layout-pl-3\": true,\n        \"layout-pr-3\": true,\n        \"border-br-2\": true,\n        \"border-bw-1\": true,\n        \"border-bs-s\": true,\n        \"color-bgc-p100\": true,\n        \"color-bc-p60\": true,\n        \"color-c-n30\": true,\n        \"color-c-p30\": true,\n      },\n    },\n    Video: {\n      \"border-br-5\": true,\n      \"layout-el-cv\": true,\n    },\n  },\n  elements: {\n    a: aLight,\n    audio,\n    body: bodyLight,\n    button: buttonLight,\n    h1: heading,\n    h2: heading,\n    h3: heading,\n    h4: heading,\n    h5: heading,\n    iframe,\n    input: inputLight,\n    p: pLight,\n    pre: preLight,\n    textarea: textareaLight,\n    video,\n  },\n  markdown: {\n    p: [...Object.keys(pLight)],\n    h1: [...Object.keys(heading)],\n    h2: [...Object.keys(heading)],\n    h3: [...Object.keys(heading)],\n    h4: [...Object.keys(heading)],\n    h5: [...Object.keys(heading)],\n    ul: [...Object.keys(unorderedListLight)],\n    ol: [...Object.keys(orderedListLight)],\n    li: [...Object.keys(listItemLight)],\n    a: [...Object.keys(aLight)],\n    strong: [],\n    em: [],\n  },\n};\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/shell/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n\n  \"compilerOptions\": {\n    \"composite\": false,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"incremental\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"inlineSources\": false,\n    \"preserveWatchOutput\": true,\n    \"sourceMap\": true,\n    \"target\": \"es2022\",\n    \"module\": \"es2022\",\n    \"lib\": [\"ESNext\", \"DOM\", \"DOM.Iterable\"],\n    \"skipLibCheck\": true,\n    \"useDefineForClassFields\": false,\n    \"rootDir\": \".\",\n    \"outDir\": \"dist\",\n    \"tsBuildInfoFile\": \"dist/.tsbuildinfo\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"strict\": false,\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true\n  },\n  \"references\": [{ \"path\": \"../../../../renderers/lit\" }]\n}\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/shell/types/types.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { HTMLTemplateResult } from \"lit\";\n\nexport enum SnackType {\n  NONE = \"none\",\n  INFORMATION = \"information\",\n  WARNING = \"warning\",\n  ERROR = \"error\",\n  PENDING = \"pending\",\n}\n\nexport type SnackbarUUID = ReturnType<typeof globalThis.crypto.randomUUID>;\n\nexport type SnackbarAction = {\n  title: string;\n  action: string;\n  value?: HTMLTemplateResult | string;\n  callback?: () => void;\n};\n\nexport type SnackbarMessage = {\n  id: SnackbarUUID;\n  type: SnackType;\n  persistent: boolean;\n  message: string | HTMLTemplateResult;\n  actions?: SnackbarAction[];\n};\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/shell/ui/snackbar.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\nimport { LitElement, html, css, nothing, unsafeCSS } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { SnackbarMessage, SnackbarUUID, SnackType } from \"../types/types\";\nimport { repeat } from \"lit/directives/repeat.js\";\nimport { SnackbarActionEvent } from \"../events/events\";\nimport { classMap } from \"lit/directives/class-map.js\";\nimport { v0_8 } from \"@a2ui/lit\";\n\nconst DEFAULT_TIMEOUT = 8000;\n\n@customElement(\"ui-snackbar\")\nexport class Snackbar extends LitElement {\n  @property({ reflect: true, type: Boolean })\n  accessor active = false;\n\n  @property({ reflect: true, type: Boolean })\n  accessor error = false;\n\n  @property()\n  accessor timeout = DEFAULT_TIMEOUT;\n\n  #messages: SnackbarMessage[] = [];\n  #timeout = 0;\n\n  static styles = [\n    unsafeCSS(v0_8.Styles.structuralStyles),\n    css`\n      :host {\n        --text-color: var(--n-0);\n        --bb-body-medium: 16px;\n        --bb-body-line-height-medium: 24px;\n\n        display: flex;\n        align-items: center;\n        position: fixed;\n        bottom: var(--bb-grid-size-7);\n        left: 50%;\n        translate: -50% 0;\n        opacity: 0;\n        pointer-events: none;\n        border-radius: var(--bb-grid-size-2);\n        background: var(--n-90);\n        padding: var(--bb-grid-size-3) var(--bb-grid-size-6);\n        width: 60svw;\n        max-width: 720px;\n        z-index: 1800;\n        scrollbar-width: none;\n        overflow-x: scroll;\n        font: 400 var(--bb-body-medium) / var(--bb-body-line-height-medium)\n          var(--bb-font-family);\n      }\n\n      :host([active]) {\n        transition: opacity 0.3s cubic-bezier(0, 0, 0.3, 1) 0.2s;\n        opacity: 1;\n        pointer-events: auto;\n      }\n\n      :host([error]) {\n        background: var(--e-90);\n        --text-color: var(--e-40);\n      }\n\n      .g-icon {\n        flex: 0 0 auto;\n        color: var(--text-color);\n        margin-right: var(--bb-grid-size-4);\n\n        &.rotate {\n          animation: 1s linear 0s infinite normal forwards running rotate;\n        }\n      }\n\n      #messages {\n        color: var(--text-color);\n        flex: 1 1 auto;\n        margin-right: var(--bb-grid-size-11);\n\n        a,\n        a:visited {\n          color: var(--bb-ui-600);\n          text-decoration: none;\n\n          &:hover {\n            color: var(--bb-ui-500);\n            text-decoration: underline;\n          }\n        }\n      }\n\n      #actions {\n        flex: 0 1 auto;\n        width: fit-content;\n        margin-right: var(--bb-grid-size-3);\n\n        & button {\n          font: 500 var(--bb-body-medium) / var(--bb-body-line-height-medium)\n            var(--bb-font-family);\n          padding: 0;\n          background: transparent;\n          border: none;\n          margin: 0 var(--bb-grid-size-4);\n          color: var(--text-color);\n          opacity: 0.7;\n          transition: opacity 0.2s cubic-bezier(0, 0, 0.3, 1);\n\n          &:not([disabled]) {\n            cursor: pointer;\n\n            &:hover,\n            &:focus {\n              opacity: 1;\n            }\n          }\n        }\n      }\n\n      #close {\n        display: flex;\n        align-items: center;\n        padding: 0;\n        color: var(--text-color);\n        background: transparent;\n        border: none;\n        margin: 0 0 0 var(--bb-grid-size-2);\n        opacity: 0.7;\n        transition: opacity 0.2s cubic-bezier(0, 0, 0.3, 1);\n\n        .g-icon {\n          margin-right: 0;\n        }\n\n        &:not([disabled]) {\n          cursor: pointer;\n\n          &:hover,\n          &:focus {\n            opacity: 1;\n          }\n        }\n      }\n\n      @keyframes rotate {\n        from {\n          rotate: 0deg;\n        }\n\n        to {\n          rotate: 360deg;\n        }\n      }\n    `,\n  ];\n\n  show(message: SnackbarMessage, replaceAll = false) {\n    const existingMessage = this.#messages.findIndex(\n      (msg) => msg.id === message.id\n    );\n    if (existingMessage === -1) {\n      if (replaceAll) {\n        this.#messages.length = 0;\n      }\n\n      this.#messages.push(message);\n    } else {\n      this.#messages[existingMessage] = message;\n    }\n\n    window.clearTimeout(this.#timeout);\n    if (!this.#messages.every((msg) => msg.persistent)) {\n      this.#timeout = window.setTimeout(() => {\n        this.hide();\n      }, this.timeout);\n    }\n\n    this.error = this.#messages.some((msg) => msg.type === SnackType.ERROR);\n    this.active = true;\n    this.requestUpdate();\n\n    return message.id;\n  }\n\n  hide(id?: SnackbarUUID) {\n    if (id) {\n      const idx = this.#messages.findIndex((msg) => msg.id === id);\n      if (idx !== -1) {\n        this.#messages.splice(idx, 1);\n      }\n    } else {\n      this.#messages.length = 0;\n    }\n\n    this.active = this.#messages.length !== 0;\n    this.updateComplete.then((avoidedUpdate) => {\n      if (!avoidedUpdate) {\n        return;\n      }\n\n      this.requestUpdate();\n    });\n  }\n\n  render() {\n    let rotate = false;\n    let icon = \"\";\n    for (let i = this.#messages.length - 1; i >= 0; i--) {\n      if (\n        !this.#messages[i].type ||\n        this.#messages[i].type === SnackType.NONE\n      ) {\n        continue;\n      }\n\n      icon = this.#messages[i].type;\n      if (this.#messages[i].type === SnackType.PENDING) {\n        icon = \"progress_activity\";\n        rotate = true;\n      }\n      break;\n    }\n\n    return html` ${icon\n      ? html`<span\n            class=${classMap({\n        \"g-icon\": true,\n        round: true,\n        filled: true,\n        rotate,\n      })}\n            >${icon}</span\n          >`\n      : nothing}\n      <div id=\"messages\">\n        ${repeat(\n        this.#messages,\n        (message) => message.id,\n        (message) => {\n          return html`<div>${message.message}</div>`;\n        }\n      )}\n      </div>\n      <div id=\"actions\">\n        ${repeat(\n        this.#messages,\n        (message) => message.id,\n        (message) => {\n          if (!message.actions) {\n            return nothing;\n          }\n\n          return html`${repeat(\n            message.actions,\n            (action) => action.value,\n            (action) => {\n              return html`<button\n                  @click=${() => {\n                  this.hide();\n                  this.dispatchEvent(\n                    new SnackbarActionEvent(\n                      action.action,\n                      action.value,\n                      action.callback\n                    )\n                  );\n                }}\n                >\n                  ${action.title}\n                </button>`;\n            }\n          )}`;\n        }\n      )}\n      </div>\n      <button\n        id=\"close\"\n        @click=${() => {\n        this.hide();\n        this.dispatchEvent(new SnackbarActionEvent(\"dismiss\"));\n      }}\n      >\n        <span class=\"g-icon\">close</span>\n      </button>`;\n  }\n}\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/shell/ui/ui.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nexport { Snackbar } from \"./snackbar\";\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/client/lit/shell/vite.config.ts",
    "content": "/*\n Copyright 2025 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n */\n\nimport { config } from \"dotenv\";\nimport { UserConfig } from \"vite\";\nimport * as Middleware from \"./middleware\";\nimport { dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nexport default async () => {\n  config();\n\n  const entry: Record<string, string> = {\n    shell: resolve(__dirname, \"index.html\"),\n  };\n\n  return {\n    plugins: [Middleware.A2AMiddleware.plugin()],\n    build: {\n      rollupOptions: {\n        input: entry,\n      },\n      target: \"esnext\",\n    },\n    define: {},\n    resolve: {\n      dedupe: [\"lit\"],\n    },\n  } satisfies UserConfig;\n};\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/__main__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Main entry point for running the restaurant finder A2UI server.\"\"\"\nimport logging\nimport os\nimport uvicorn\nfrom starlette.staticfiles import StaticFiles\nfrom setup_a2ui_server import app\n\nif __name__ == \"__main__\":\n    # Get the directory where this script is located\n    script_dir = os.path.dirname(os.path.abspath(__file__))\n    images_dir = os.path.join(script_dir, \"images\")\n\n    # Mount static files if images directory exists\n    if os.path.exists(images_dir):\n        app.mount(\"/static\", StaticFiles(directory=images_dir), name=\"static\")\n        logging.info(\n            \"Mounted static files from %s at /static\",\n            images_dir,\n        )\n    else:\n        logging.warning(\n            \"Images directory not found at %s, \"\n            \"static files will not be served\",\n            images_dir,\n        )\n\n    # Run the app manually with uvicorn\n    uvicorn.run(\n        app,\n        host=\"0.0.0.0\",\n        port=10002,\n        log_level=\"info\",\n        access_log=True,\n    )\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/a2ui_utils.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Utility functions for A2UI agent integration.\"\"\"\nimport json\nfrom typing import Any\nfrom pydantic import BaseModel\nfrom pydantic import Field\n\nfrom a2a.types import (\n    DataPart,\n    TextPart,\n    Message,\n    Part,\n)\nfrom a2ui.extension.a2ui_extension import (\n    A2UI_MIME_TYPE,\n    MIME_TYPE_KEY,\n    A2UI_EXTENSION_URI,\n)\n\nfrom agentscope._logging import logger\n\n\nclass A2UIResponse(BaseModel):\n    \"\"\"Response model for A2UI formatted output.\"\"\"\n\n    response_with_a2ui: str = Field(\n        description=\"The response with A2UI JSON\",\n    )\n\n\ndef check_a2ui_extension(*args: Any) -> bool:\n    \"\"\"Check if a2ui extension is requested in the request context.\n\n    Args:\n        *args: Variable arguments that may contain ServerCallContext as the\n            first element.\n\n    Returns:\n        True if a2ui extension is requested and activated, False otherwise.\n    \"\"\"\n    # Extract context from args (ServerCallContext is typically the first\n    # element)\n    if not args or len(args) == 0:\n        logger.warning(\"check_a2ui_extension: No context provided in args\")\n        return False\n\n    context = args[0]\n\n    # Check if context has requested_extensions attribute\n    if not hasattr(context, \"requested_extensions\"):\n        logger.warning(\n            \"check_a2ui_extension: Context does not have \"\n            \"requested_extensions attribute\",\n        )\n        return False\n\n    # Check if A2UI extension is requested\n    if A2UI_EXTENSION_URI in context.requested_extensions:\n        # Activate the extension if add_activated_extension method exists\n        if hasattr(context, \"add_activated_extension\"):\n            context.add_activated_extension(A2UI_EXTENSION_URI)\n            logger.info(\"A2UI extension activated: %s\", A2UI_EXTENSION_URI)\n        else:\n            logger.warning(\n                \"check_a2ui_extension: Context does not have \"\n                \"add_activated_extension method\",\n            )\n        return True\n\n    return False\n\n\ndef transfer_ui_event_to_query(ui_event_part: dict) -> str:\n    \"\"\"Transfer UI event to a query string.\n\n    Args:\n        ui_event_part: A dictionary containing UI event information with\n            actionName and context.\n\n    Returns:\n        A formatted query string based on the UI event action.\n    \"\"\"\n    action = ui_event_part.get(\"actionName\")\n    ctx = ui_event_part.get(\"context\", {})\n\n    if action in [\"book_restaurant\", \"select_item\"]:\n        restaurant_name = ctx.get(\"restaurantName\", \"Unknown Restaurant\")\n        address = ctx.get(\"address\", \"Address not provided\")\n        image_url = ctx.get(\"imageUrl\", \"\")\n        query = (\n            f\"USER_WANTS_TO_BOOK: {restaurant_name}, \"\n            f\"Address: {address}, ImageURL: {image_url}\"\n        )\n    elif action == \"submit_booking\":\n        restaurant_name = ctx.get(\"restaurantName\", \"Unknown Restaurant\")\n        party_size = ctx.get(\"partySize\", \"Unknown Size\")\n        reservation_time = ctx.get(\"reservationTime\", \"Unknown Time\")\n        dietary_reqs = ctx.get(\"dietary\", \"None\")\n        image_url = ctx.get(\"imageUrl\", \"\")\n        query = (\n            f\"User submitted a booking for {restaurant_name} \"\n            f\"for {party_size} people at {reservation_time} \"\n            f\"with dietary requirements: {dietary_reqs}. \"\n            f\"The image URL is {image_url}\"\n        )\n    else:\n        # Note: The A2UI original example uses `ctx` as the data source.\n        # However, in generated UI components, the `ctx` field may be empty\n        # when the databinding path cannot be resolved. To ensure we capture\n        # all available event data, we use the entire `ui_event_part` instead.\n        query = f\"User submitted an event: {action} with data: {ui_event_part}\"\n\n    return query\n\n\ndef pre_process_request_with_ui_event(message: Message) -> Any:\n    \"\"\"Pre-process the request.\n\n    Args:\n        message: The agent request object.\n\n    Returns:\n        The pre-processed request.\n    \"\"\"\n\n    if message and message.parts:\n        logger.info(\n            \"--- AGENT_EXECUTOR: Processing %s message parts ---\",\n            len(message.parts),\n        )\n        for i, part in enumerate(message.parts):\n            if isinstance(part.root, DataPart):\n                if \"userAction\" in part.root.data:\n                    logger.info(\n                        \"  Part %s: Found a2ui UI ClientEvent payload: %s\",\n                        i,\n                        json.dumps(part.root.data[\"userAction\"], indent=4),\n                    )\n                    ui_event_part = part.root.data[\"userAction\"]\n                    message.parts[i] = Part(\n                        root=TextPart(\n                            text=transfer_ui_event_to_query(ui_event_part),\n                        ),\n                    )\n    return message\n\n\ndef _find_json_end(json_string: str) -> int:\n    \"\"\"Find the end position of a JSON array or object.\n\n    Finds the end by matching brackets/braces.\n\n    Args:\n        json_string: The JSON string to search.\n\n    Returns:\n        The end position (index + 1) of the JSON structure.\n    \"\"\"\n    if json_string.startswith(\"[\"):\n        # Find matching closing bracket\n        bracket_count = 0\n        for i, char in enumerate(json_string):\n            if char == \"[\":\n                bracket_count += 1\n            elif char == \"]\":\n                bracket_count -= 1\n                if bracket_count == 0:\n                    return i + 1\n    elif json_string.startswith(\"{\"):\n        # Find matching closing brace\n        brace_count = 0\n        for i, char in enumerate(json_string):\n            if char == \"{\":\n                brace_count += 1\n            elif char == \"}\":\n                brace_count -= 1\n                if brace_count == 0:\n                    return i + 1\n    return len(json_string)\n\n\ndef extract_ui_json_from_text(content_str: str) -> tuple[str, None]:\n    \"\"\"Extract the UI JSON from the text.\n\n    Args:\n        text: The text to extract the UI JSON from.\n\n    Returns:\n        The UI JSON.\n    \"\"\"\n    text_content, json_string = content_str.split(\"---a2ui_JSON---\", 1)\n    json_data = None\n    if json_string.strip():\n        try:\n            # Clean JSON string (remove markdown code blocks if present)\n            json_string_cleaned = (\n                json_string.strip().lstrip(\"```json\").rstrip(\"```\").strip()\n            )\n\n            # Find the end of JSON array/object by matching brackets/braces\n            json_end = _find_json_end(json_string_cleaned)\n            json_string_final = json_string_cleaned[:json_end].strip()\n            json_data = json.loads(json_string_final)\n        except json.JSONDecodeError as e:\n            logger.error(\"Failed to parse UI JSON: %s\", e)\n            # On error, keep the JSON as text content\n            return content_str, None\n    return text_content, json_data\n\n\ndef check_a2ui_json_in_message(\n    part: Part,\n    is_final: bool,\n) -> tuple[bool, str | None]:\n    \"\"\"Check if the message contains A2UI JSON.\n\n    Args:\n        message: The message to check.\n\n    Returns:\n        A tuple containing a boolean indicating if A2UI JSON is found and\n        the A2UI JSON string if found.\n    \"\"\"\n    # for the case when ReActAgent max iters is reached, the message will be\n    # the last complete message, with text message.\n    if (\n        isinstance(part.root, TextPart)\n        and \"---a2ui_JSON---\" in part.root.text\n        and is_final\n    ):\n        logger.info(\n            \"--- Found A2UI JSON in the message: %s ---\",\n            part.root.text,\n        )\n        return True, part.root.text\n\n    # for the case when ReActAgent max iters is not reached, if it contains\n    # tool use block with name \"generate_response\" and type \"tool_use\", and\n    # the response_with_a2ui contains \"---a2ui_JSON---\", then return True,\n    # response_with_a2ui.\n    if (\n        isinstance(part.root, DataPart)\n        and part.root.data.get(\"name\") == \"generate_response\"\n        and part.root.data.get(\"type\") == \"tool_use\"\n        and not is_final\n    ):\n        input_data = part.root.data.get(\"input\")\n        if input_data and isinstance(input_data, dict):\n            response_with_a2ui = input_data.get(\"response_with_a2ui\")\n            if response_with_a2ui and \"---a2ui_JSON---\" in response_with_a2ui:\n                return True, response_with_a2ui\n    return False, None\n\n\ndef post_process_a2a_message_for_ui(\n    message: Message,\n) -> Message:\n    \"\"\"Post-process the transferred A2A message.\n\n    Args:\n        message: The transferred A2A message.\n\n    Returns:\n        The post-processed A2A message.\n    \"\"\"\n    new_parts = []\n    # pylint: disable=too-many-nested-blocks\n    for part in message.parts:\n        # Check if it's a text block and contains the A2UI JSON marker\n        if isinstance(part.root, TextPart):\n            text_content_str = part.root.text\n            if \"---a2ui_JSON---\" in text_content_str:\n                # Extract and process A2UI JSON\n                text_content, json_data = extract_ui_json_from_text(\n                    text_content_str,\n                )\n                if json_data:\n                    # Replace the part with a TextPart and multiple DataParts\n                    # with the same metadata for a2ui\n                    try:\n                        new_parts.append(\n                            Part(\n                                root=TextPart(\n                                    text=text_content,\n                                ),\n                            ),\n                        )\n\n                        for item in json_data:\n                            new_parts.append(\n                                Part(\n                                    root=DataPart(\n                                        data=item,\n                                        metadata={\n                                            MIME_TYPE_KEY: A2UI_MIME_TYPE,\n                                        },\n                                    ),\n                                ),\n                            )\n\n                    except Exception as e:\n                        logger.error(\n                            \"Error processing a2ui JSON parts: %s\",\n                            e,\n                            exc_info=True,\n                        )\n                        raise\n                else:\n                    # If JSON extraction failed, keep the original text block\n                    new_parts.append(part)\n            else:\n                # Keep the original text block if it doesn't contain the marker\n                new_parts.append(part)\n        else:\n            # For non-text parts, keep the original logic\n            new_parts.append(part)\n\n    message.parts = new_parts\n    return message\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/agent_card.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The agent card definition for the A2A agent.\"\"\"\nfrom a2a.types import AgentCard, AgentCapabilities, AgentSkill\nfrom a2ui.extension.a2ui_extension import get_a2ui_agent_extension\n\nagent_card = AgentCard(\n    name=\"Friday\",\n    description=\"A simple ReAct agent that handles input queries\",\n    url=\"http://localhost:10002\",\n    version=\"1.0.0\",\n    capabilities=AgentCapabilities(\n        push_notifications=False,\n        state_transition_history=True,\n        streaming=True,\n        extensions=[get_a2ui_agent_extension()],\n    ),\n    default_input_modes=[\"text/plain\"],\n    default_output_modes=[\"text/plain\"],\n    skills=[\n        AgentSkill(\n            name=\"execute_python_code\",\n            id=\"execute_python_code\",\n            description=\"Execute Python code snippets.\",\n            tags=[\"code_execution\"],\n        ),\n        AgentSkill(\n            name=\"execute_shell_command\",\n            id=\"execute_shell_command\",\n            description=\"Execute shell commands on the server.\",\n            tags=[\"code_execution\"],\n        ),\n        AgentSkill(\n            name=\"view_text_file\",\n            id=\"view_text_file\",\n            description=\"View the content of a text file on the server.\",\n            tags=[\"file_viewing\"],\n        ),\n    ],\n)\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/prompt_builder.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Prompt builder for Agent with A2UI support.\"\"\"\n# Copyright 2025 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# flake8: noqa: E501\n# pylint: disable=C0301\n\n\ndef get_ui_prompt() -> str:\n    \"\"\"\n    Constructs the full prompt with UI instructions, rules, examples, and schema.\n\n    Args:\n    Returns:\n        A formatted string to be used as the system prompt for the LLM.\n    \"\"\"\n    return \"\"\"\n    You are a helpful assistant specialized in generating appropriate A2UI UI JSON responses to display content to users and help them complete their tasks.\n\n    To generate the appropriate A2UI UI JSON responses, you MUST follow these rules:\n    1.  **CRITICAL FIRST STEP**: Before generating ANY response with UI JSON, you MUST ensure that you have loaded the schema and examples from the `A2UI_response_generator` skill:\n        - Read the SKILL.md file from the skill directory using `view_text_file`\n        - Execute the Python command in the skill directory using `execute_shell_command` to load the schema and examples\n        - DO NOT assume you know the A2UI format - you MUST load it from the skill\n\n    2.  When you plan to generate the A2UI JSON response, you MUST output text directly. The text output MUST contain two parts, separated by the delimiter: `---a2ui_JSON---`. The first part is your conversational text response, and the second part is a single, raw JSON object which is a list of A2UI messages.\n\n    ### CRITICAL REQUIREMENTS:\n    1.  ALL your schema and examples about A2UI MUST come from your equipped skills - do NOT use any prior knowledge.\n    2. You MUST ONLY use `execute_shell_command` tool to execute the Python command in the skill directory. DO NOT use `execute_python_code` to execute the command.\n    3.  **You MUST directly generate the A2UI JSON based on the task content. DO NOT ask the user about their preference regarding UI type (list, form, confirmation, detail view). You should automatically determine the most appropriate UI type based on the context and generate the response accordingly.**\n    4.  **ALWAYS remember the user's task and objective. Your UI responses should be directly aligned with helping the user accomplish their specific goal. Never lose sight of what the user is trying to achieve.**\n    5.  **WHEN ASKING QUESTIONS**: When you need to ask the user task-related questions, you MUST include `---a2ui_JSON---` followed by the appropriate UI JSON (such as forms, selection cards, or input fields) that allows the user to provide their answer. The question text should come first, then the delimiter, then the UI JSON for collecting the user's response.\n    6.  **WHEN PROVIDING INFORMATION IN TEXT FORMAT**: When you need to provide information to the user in text format, you MUST include `---a2ui_JSON---` followed by the appropriate UI JSON that allows the user to view the information. The information text should come first, then the delimiter, then the UI JSON for displaying the information.\n    7. **The Most Important Rule**: You MUST always include `---a2ui_JSON---` in your response.\n    8.  **CRITICAL FORMAT REQUIREMENT**: When generating A2UI JSON responses, you MUST output text directly. The text MUST contain two parts separated by `---a2ui_JSON---`: your conversational response first, followed by the A2UI JSON array that best describes the inferface that you want to display to the user.\n    9. **If you skip using the `A2UI_response_generator` skill, your response will be incorrect and invalid.**\n\n    \"\"\"\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/pyproject.toml",
    "content": "[project]\nname = \"general-agent\"\nversion = \"0.1.0\"\nrequires-python = \">=3.10\"\ndependencies = [\n    \"agentscope[a2a]\",\n    \"agentscope-runtime\",\n    \"uvicorn\",\n    \"starlette\",\n    \"fastapi\",\n    \"shortuuid\",\n    \"a2ui-agent\"\n]\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\".\"]\n\n[tool.hatch.metadata]\nallow-direct-references = true\n\n[tool.uv.sources]\na2ui-agent = { path = \"../../../../../../A2UI/a2a_agents/python/a2ui_agent\" }\nagentscope = { path = \"../../../../../\" }"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/setup_a2ui_server.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Set up an A2A server with a ReAct agent to handle the input query\"\"\"\nimport os\nimport uuid\nimport copy\nfrom typing import AsyncGenerator, Any\n\nfrom a2a.server.apps import A2AStarletteApplication\nfrom a2a.server.events import Event\nfrom a2a.types import (\n    Task,\n    TaskStatus,\n    TaskState,\n    Message,\n    MessageSendParams,\n    TaskStatusUpdateEvent,\n)\nfrom starlette.middleware.cors import CORSMiddleware\n\nfrom agent_card import agent_card\nfrom prompt_builder import get_ui_prompt\nfrom a2ui_utils import (\n    pre_process_request_with_ui_event,\n    post_process_a2a_message_for_ui,\n)\nfrom agentscope._logging import logger\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter, A2AChatFormatter\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.pipeline import stream_printing_messages\nfrom agentscope.session import JSONSession\nfrom agentscope.message import Msg\n\n\nclass SimpleStreamHandler:\n    \"\"\"A simple request handler that handles the input query by a\n    ReAct agent.\n\n    This handler processes A2A protocol messages by using a ReAct agent\n    to generate responses. It supports both streaming and non-streaming\n    message handling, and manages session state for conversation continuity.\n    \"\"\"\n\n    async def _prepare_final_message(\n        self,\n        formatter: A2AChatFormatter,\n        final_msg: Msg | None,\n    ) -> Message:\n        \"\"\"Prepare the final message for response.\n\n        Args:\n            formatter (`A2AChatFormatter`):\n                The A2AChatFormatter instance.\n            final_msg (`Msg | None`, optional):\n                The final message if available.\n\n        Returns:\n            `Message`:\n                The prepared final message.\n        \"\"\"\n        logger.info(\n            \"--- Processing final response, final_msg: %s ---\",\n            final_msg is not None,\n        )\n\n        if final_msg is not None:\n            logger.info(\"--- Using final message for final message ---\")\n            final_a2a_message = await formatter.format(\n                [final_msg],\n            )\n        else:\n            logger.info(\n                \"--- Using last complete message for final message ---\",\n            )\n\n        logger.info(\n            \"--- Post-processing message for UI: %s ---\",\n            final_a2a_message,\n        )\n        final_a2a_message = post_process_a2a_message_for_ui(\n            final_a2a_message,\n        )\n        return final_a2a_message\n\n    async def on_message_send(\n        self,  # pylint: disable=unused-argument\n        params: MessageSendParams,\n        *args: Any,\n        **kwargs: Any,\n    ) -> Task:\n        \"\"\"Handles non-streaming message_send requests by collecting\n        events from the stream and returning the final Task.\n\n        Args:\n            params (`MessageSendParams`):\n                The parameters for sending the message.\n            *args (`Any`):\n                Additional positional arguments.\n            **kwargs (`Any`):\n                Additional keyword arguments.\n\n        Returns:\n            `Task`:\n                The final Task object.\n        \"\"\"\n        logger.info(\"--- params: %s ---\", params)\n        logger.info(\"args: %s ---\", args)\n        logger.info(\"kwargs: %s ---\", kwargs)\n        # Collect all events from the stream\n        final_event = None\n        task_id = params.message.task_id or uuid.uuid4().hex\n        context_id = params.message.context_id or \"default-context\"\n\n        async for event in self.on_message_send_stream(\n            params,\n            *args,\n            **kwargs,\n        ):\n            if event.final:\n                final_event = event\n                break\n\n        # Ensure we always return a valid Task\n        if final_event is None:\n            # If no final event was found, create one with completed state\n            logger.warning(\n                \"No final event found in stream, \"\n                \"creating default completed event\",\n            )\n            final_event = TaskStatusUpdateEvent(\n                task_id=task_id,\n                context_id=context_id,\n                status=TaskStatus(state=TaskState.failed),\n                final=True,\n            )\n\n        # Convert TaskStatusUpdateEvent to Task\n        # A2A protocol expects on_message_send to return a Task,\n        # not TaskStatusUpdateEvent\n        return Task(\n            id=final_event.task_id,\n            context_id=final_event.context_id,\n            status=final_event.status,\n            artifacts=[],\n        )\n\n    async def on_message_send_stream(\n        self,  # pylint: disable=unused-argument\n        params: MessageSendParams,\n        *args: Any,\n        **kwargs: Any,\n    ) -> AsyncGenerator[Event, None]:\n        \"\"\"Handles the message_send method by the agent.\n\n        Args:\n            params (`MessageSendParams`):\n                The parameters for sending the message.\n            *args (`Any`):\n                Additional positional arguments.\n            **kwargs (`Any`):\n                Additional keyword arguments.\n\n        Returns:\n            `AsyncGenerator[Event, None]`:\n                An asynchronous generator that yields task status update\n                events.\n        \"\"\"\n\n        task_id = params.message.task_id or uuid.uuid4().hex\n        context_id = params.message.context_id or \"default-context\"\n        # ============ Agent Logic ============\n        from agentscope.tool import (\n            Toolkit,\n            view_text_file,\n            execute_python_code,\n            execute_shell_command,\n        )\n\n        toolkit = Toolkit()\n        toolkit.register_tool_function(execute_python_code)\n        toolkit.register_tool_function(execute_shell_command)\n        toolkit.register_tool_function(view_text_file)\n        # Get the skill path relative to this file\n        # From restaurant_finder/ to restaurant_finder/skills/\n        # A2UI_response_generator\n        skill_path = os.path.abspath(\n            os.path.join(\n                os.path.dirname(__file__),\n                \"skills\",\n                \"A2UI_response_generator\",\n            ),\n        )\n        toolkit.register_agent_skill(skill_path)\n\n        # Create the agent instance\n        agent = ReActAgent(\n            name=\"Friday\",\n            sys_prompt=get_ui_prompt(),\n            model=DashScopeChatModel(\n                model_name=\"qwen3-max\",\n                api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n            ),\n            formatter=DashScopeChatFormatter(),\n            toolkit=toolkit,\n            max_iters=10,\n        )\n        logger.info(\"Agent system prompt: %s\", agent.sys_prompt)\n\n        session = JSONSession(save_dir=\"./sessions\")\n        session_id = params.message.task_id or \"test-a2ui-agent\"\n        await session.load_session_state(\n            session_id=session_id,\n            agent=agent,\n        )\n\n        # pre-process the A2A message with UI event,\n        # and then convert to AgentScope Msg objects\n        formatter = A2AChatFormatter()\n        as_msg = await formatter.format_a2a_message(\n            name=\"Friday\",\n            message=pre_process_request_with_ui_event(\n                params.message,\n            ),\n        )\n\n        yield TaskStatusUpdateEvent(\n            task_id=task_id,\n            context_id=context_id,\n            status=TaskStatus(state=TaskState.working),\n            final=False,\n        )\n\n        # Collect all messages from the stream\n        # The 'last' flag indicates the last chunk of a streaming message,\n        # not the last message from the agent\n        message_count = 0\n        final_msg = None\n        try:\n            async for msg, last in stream_printing_messages(\n                agents=[agent],\n                coroutine_task=agent(as_msg),\n            ):\n                message_count += 1\n                if last:\n                    final_msg = copy.deepcopy(msg)\n        except Exception as e:\n            logger.error(\n                \"--- Error in message stream: %s ---\",\n                e,\n                exc_info=True,\n            )\n            raise\n        finally:\n            logger.info(\n                \"--- Message stream collection completed. \"\n                \"Total messages: %s, \"\n                \"Last message: %s ---\",\n                message_count,\n                final_msg,\n            )\n\n        # Save session state (move before final message processing\n        # to avoid blocking yield)\n        await session.save_session_state(\n            session_id=session_id,\n            agent=agent,\n        )\n\n        final_a2a_message = await self._prepare_final_message(\n            formatter,\n            final_msg,\n        )\n\n        logger.info(\"--- Yielding final TaskStatusUpdateEvent ---\")\n        yield TaskStatusUpdateEvent(\n            task_id=task_id,\n            context_id=context_id,\n            status=TaskStatus(\n                state=TaskState.input_required,\n                message=final_a2a_message,\n            ),\n            final=True,\n        )\n\n\nhandler = SimpleStreamHandler()\napp_instance = A2AStarletteApplication(\n    agent_card,\n    handler,\n)\napp = app_instance.build()\n\n# Add CORS middleware to handle OPTIONS requests\napp.add_middleware(\n    CORSMiddleware,\n    allow_origins=[\"*\"],  # Allow all origins for development\n    allow_credentials=False,  # Cannot use \"*\" with credentials=True\n    allow_methods=[\"*\"],  # Allow all HTTP methods including OPTIONS\n    allow_headers=[\"*\"],  # Allow all headers\n)\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/skills/A2UI_response_generator/SKILL.md",
    "content": "---\nname: A2UI_response_generator\ndescription: A skill that can retrieve A2UI UI JSON schematics and UI templates that best show the response. This skill is essential and must be used before generating A2UI (Agent to UI) JSON responses.\n---\n\n# A2UI response generation Skill\n\n## Overview\n\nThis skill is **essential and must be used before generating A2UI (Agent to UI) JSON responses**. It enables agents to retrieve A2UI UI JSON schematics and UI templates that best show the response, allowing for the generation of rich, interactive UI responses using the A2UI protocol.\n\nInstead of loading the entire A2UI schema and all examples at once, this skill allows agents to **retrieve** only the relevant UI templates and schematics based on the response content. The A2UI protocol defines a JSON-based format for dynamically constructing and updating user interfaces. By breaking down the examples into modular templates, agents can:\n\n1. Retrieve the appropriate A2UI UI JSON schematics for validation and structure reference\n2. Select UI templates that best match and display the response content\n3. Reduce prompt token usage by loading only necessary templates\n4. Easily extend with new UI templates for different domains\n\n### File Structure\n\n```\nA2UI_response_generator/\n├── SKILL.md                          # This file - main skill documentation\n├── view_a2ui_schema.py               # Tool to view the complete A2UI schema (schema included in file)\n├── view_a2ui_examples.py             # Tool to view UI template examples (templates included in file)\n├── __init__.py                       # Package initialization\n├── schema/                           # A2UI schema definitions\n│   ├── __init__.py\n│   └── base_schema.py                # Base A2UI schema\n└── UI_templete_examples/             # UI template examples\n    ├── __init__.py\n    ├── booking_form.py               # Booking form template\n    ├── contact_form.py               # Contact form template\n    ├── email_compose_form.py         # Email compose form template\n    ├── error_message.py              # Error message template\n    ├── info_message.py               # Info message template\n    ├── item_detail_card_with_image.py # Item detail card with image template\n    ├── profile_view.py               # Profile view template\n    ├── search_filter_form.py         # Search filter form template\n    ├── simple_column_list_without_image.py # Simple list template\n    ├── single_column_list.py         # Single column list template\n    ├── success_confirmation_with_image.py # Success confirmation template\n    └── two_column_list.py            # Two column list template\n```\n\n## Quick Start\n\nWhen it is required to generate UI JSON, follow these steps:\n\nImportant: Please use the `execute_shell_command` tool to execute Python command.\n\n### Step 1: Load the A2UI Schema\n\nRun the following script to load the complete A2UI schema.\n\nCurrently available `schema_category` is `BASE_SCHEMA`.\n\n**Use the `execute_shell_command` tool to run (make sure you are in the skill directory):**\n```bash\npython -m view_a2ui_schema --schema_category BASE_SCHEMA\n```\n\n**Usage**: `python -m view_a2ui_schema --schema_category [schema_category]` - Loads the A2UI schema definition for validating A2UI JSON response structure. Currently only `BASE_SCHEMA` is available.\n\nAbout detailed usage, please refer to the `./view_a2ui_schema.py` script (located in the same folder as this SKILL.md file).\n\n### Step 2: Select UI Template Examples\n\nSelect appropriate UI template examples based on your response content.\n\n**IMPORTANT**: You MUST use the **exact template names** listed in the \"Available UI template examples\" table below. Do NOT use generic category names like 'list', 'form', 'confirmation', or 'detail'. You MUST use the specific template name (e.g., `SINGLE_COLUMN_LIST_WITH_IMAGE`, `BOOKING_FORM_WITH_IMAGE`, etc.).\n\n**Use the `execute_shell_command` tool to run (make sure you are in the skill directory):**\n```bash\npython -m view_a2ui_examples --template_name SINGLE_COLUMN_LIST_WITH_IMAGE\n```\n\n**Usage**: `python -m view_a2ui_examples --template_name [template_name]` - Loads a UI template example for reference when generating A2UI responses. Accepts a single template name from the available templates table below.\n\n**Available UI template examples** (when `schema_category` is `BASE_SCHEME`, you MUST use these exact names, case-sensitive):\n\n| Template Name | Use Case | Selection Guide | Image Support |\n| --- | --- | --- | --- |\n| `SINGLE_COLUMN_LIST_WITH_IMAGE` | Vertical list with detailed cards (for ≤5 items) | Use for **list display** with ≤5 items | ✅ With image |\n| `TWO_COLUMN_LIST_WITH_IMAGE` | Grid layout with cards (for >5 items) | Use for **list display** with >5 items | ✅ With image |\n| `SIMPLE_LIST` | Compact list without images | Use for **compact lists** without images | ❌ Without image |\n| `SELECTION_CARD` | Multiple choice questions | Use for **multiple choice questions** | ❌ Without image |\n| `MULTIPLE_SELECTION_CARDS` | Multiple selection cards in a list | Use for **multiple selection cards** displayed together | ❌ Without image |\n| `BOOKING_FORM_WITH_IMAGE` | Reservation, booking, registration | Use for **booking/reservation forms** | ✅ With image |\n| `SEARCH_FILTER_FORM_WITH_IMAGE` | Search forms with filters | Use for **search forms with filters** | ❌ Without image |\n| `CONTACT_FORM_WITH_IMAGE` | Contact or feedback forms | Use for **contact/feedback forms** | ❌ Without image |\n| `EMAIL_COMPOSE_FORM_WITH_IMAGE` | Email composition forms | Use for **email composition forms** | ❌ Without image |\n| `SUCCESS_CONFIRMATION_WITH_IMAGE` | Success message after action | Use for **success confirmations** | ✅ With image |\n| `ERROR_MESSAGE` | Error or warning display | Use for **error messages** | ❌ Without image |\n| `INFO_MESSAGE` | Informational messages | Use for **info messages** | ❌ Without image |\n| `ITEM_DETAIL_CARD` | Detailed view of single item | Use for **item detail views** | ✅ With image |\n| `ITEM_DETAIL_CARD_WITH_IMAGE` | Detailed view of single item with image | Use for **item detail views** with images | ✅ With image |\n| `PROFILE_VIEW` | User or entity profile display | Use for **profile views** | ✅ With image |\n\n**Remember**: Always use the exact template names from the table above. Never use generic terms like 'list' or 'form' - they are NOT valid template names.\n\nAbout detailed usage, please refer to the `./view_a2ui_examples.py` script (located in the same folder as this SKILL.md file).\n\n### Step 3: Generate the A2UI Response\n\nAfter loading the schema and examples, output your A2UI response directly as text. The text output must contain two parts separated by the delimiter `---a2ui_JSON---`:\n\nFirst Part: **Conversational text response**: Your natural language reply to the user\nSecond Part. **A2UI JSON messages**: A raw JSON array of A2UI message objects that MUST validate against the A2UI schema\n\n**Format:**\n```\n[Your conversational response here]\n\n---a2ui_JSON---\n[\n  { \"beginRendering\": { ... } },\n  { \"surfaceUpdate\": { ... } },\n  { \"dataModelUpdate\": { ... } }\n]\n```\n\n**Important**: The JSON portion must be valid JSON and conform to the A2UI schema loaded in Step 1.\n\n\n\n## Domain-Specific Extensions\n\nTo add support for a new domain (e.g., flight booking, e-commerce), add new templates to `view_a2ui_examples.py`:\n\n1. Define a new template constant in `view_a2ui_examples.py` (e.g., `FLIGHT_BOOKING_FORM_EXAMPLE`)\n2. Add the template to the `TEMPLATE_MAP` dictionary in `view_a2ui_examples.py`\n3. Update this SKILL.md to include the new templates in the available templates list\n\n\n## Troubleshooting\n\nIf you encounter any issues running the scripts, make sure:\nyou use the tool `execute_shell_command` run the python command.\n\n1. You are in the correct skill directory (check the skill description for the actual path)\n2. The script files (`view_a2ui_schema.py` and `view_a2ui_examples.py`) exist in the skill directory\n3. You have the required Python dependencies installed\n\nFor detailed usage of each script, please refer to:\n- `./view_a2ui_schema.py` - View the A2UI schema\n- `./view_a2ui_examples.py` - View A2UI template examples\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/skills/A2UI_response_generator/UI_templete_examples/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n\"\"\"A2UI template examples for generating UI responses.\"\"\"\nfrom .booking_form import BOOKING_FORM_WITH_IMAGE\nfrom .contact_form import CONTACT_FORM_EXAMPLE\nfrom .email_compose_form import EMAIL_COMPOSE_FORM_EXAMPLE\nfrom .error_message import ERROR_MESSAGE_EXAMPLE\nfrom .info_message import INFO_MESSAGE_EXAMPLE\nfrom .item_detail_card_with_image import ITEM_DETAIL_CARD_EXAMPLE_WITH_IMAGE\nfrom .profile_view import PROFILE_VIEW_WITH_IMAGE_EXAMPLE\nfrom .search_filter_form import SEARCH_FILTER_FORM_EXAMPLE\nfrom .simple_column_list_without_image import SIMPLE_LIST_EXAMPLE\nfrom .single_column_list import SINGLE_COLUMN_LIST_WITH_IMAGE_EXAMPLE\nfrom .success_confirmation_with_image import (\n    SUCCESS_CONFIRMATION_WITH_IMAGE_EXAMPLE,\n)\nfrom .two_column_list import TWO_COLUMN_LIST_WITH_IMAGE_EXAMPLE\nfrom .selection_card import (\n    SELECTION_CARD_EXAMPLE,\n    MULTIPLE_SELECTION_CARDS_EXAMPLE,\n)\n\n__all__ = [\n    \"BOOKING_FORM_WITH_IMAGE\",\n    \"CONTACT_FORM_EXAMPLE\",\n    \"EMAIL_COMPOSE_FORM_EXAMPLE\",\n    \"ERROR_MESSAGE_EXAMPLE\",\n    \"INFO_MESSAGE_EXAMPLE\",\n    \"ITEM_DETAIL_CARD_EXAMPLE_WITH_IMAGE\",\n    \"PROFILE_VIEW_WITH_IMAGE_EXAMPLE\",\n    \"SEARCH_FILTER_FORM_EXAMPLE\",\n    \"SIMPLE_LIST_EXAMPLE\",\n    \"SINGLE_COLUMN_LIST_WITH_IMAGE_EXAMPLE\",\n    \"SUCCESS_CONFIRMATION_WITH_IMAGE_EXAMPLE\",\n    \"TWO_COLUMN_LIST_WITH_IMAGE_EXAMPLE\",\n    \"SELECTION_CARD_EXAMPLE\",\n    \"MULTIPLE_SELECTION_CARDS_EXAMPLE\",\n]\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/skills/A2UI_response_generator/UI_templete_examples/booking_form.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n\"\"\"A2UI template example for booking form.\"\"\"\n\nBOOKING_FORM_WITH_IMAGE = \"\"\"\n---BEGIN BOOKING_FORM_WITH_IMAGE_EXAMPLE---\nUse this template for booking, reservation, or registration forms.\n\n[\n  {{ \"beginRendering\": {{ \"surfaceId\": \"booking-form\", \"root\": \"form-column\", \"styles\": {{ \"primaryColor\": \"#FF5722\", \"font\": \"Roboto\" }} }} }},\n  {{ \"surfaceUpdate\": {{\n    \"surfaceId\": \"booking-form\",\n    \"components\": [\n      {{ \"id\": \"form-column\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"form-title\", \"form-image\", \"form-address\", \"party-size-field\", \"datetime-field\", \"notes-field\", \"submit-button\"] }} }} }} }},\n      {{ \"id\": \"form-title\", \"component\": {{ \"Text\": {{ \"usageHint\": \"h2\", \"text\": {{ \"path\": \"title\" }} }} }} }},\n      {{ \"id\": \"form-image\", \"component\": {{ \"Image\": {{ \"url\": {{ \"path\": \"imageUrl\" }}, \"usageHint\": \"mediumFeature\" }} }} }},\n      {{ \"id\": \"form-address\", \"component\": {{ \"Text\": {{ \"text\": {{ \"path\": \"address\" }} }} }} }},\n      {{ \"id\": \"party-size-field\", \"component\": {{ \"TextField\": {{ \"label\": {{ \"literalString\": \"Party Size\" }}, \"text\": {{ \"path\": \"partySize\" }}, \"type\": \"number\" }} }} }},\n      {{ \"id\": \"datetime-field\", \"component\": {{ \"DateTimeInput\": {{ \"label\": {{ \"literalString\": \"Date & Time\" }}, \"value\": {{ \"path\": \"reservationTime\" }}, \"enableDate\": true, \"enableTime\": true }} }} }},\n      {{ \"id\": \"notes-field\", \"component\": {{ \"TextField\": {{ \"label\": {{ \"literalString\": \"Special Requests\" }}, \"text\": {{ \"path\": \"notes\" }}, \"multiline\": true }} }} }},\n      {{ \"id\": \"submit-button\", \"component\": {{ \"Button\": {{ \"child\": \"submit-text\", \"primary\": true, \"action\": {{ \"name\": \"submit_booking\", \"context\": [ {{ \"key\": \"itemName\", \"value\": {{ \"path\": \"itemName\" }} }}, {{ \"key\": \"partySize\", \"value\": {{ \"path\": \"partySize\" }} }}, {{ \"key\": \"reservationTime\", \"value\": {{ \"path\": \"reservationTime\" }} }}, {{ \"key\": \"notes\", \"value\": {{ \"path\": \"notes\" }} }} ] }} }} }} }},\n      {{ \"id\": \"submit-text\", \"component\": {{ \"Text\": {{ \"text\": {{ \"literalString\": \"Submit Reservation\" }} }} }} }}\n    ]\n  }} }},\n  {{ \"dataModelUpdate\": {{\n    \"surfaceId\": \"booking-form\",\n    \"path\": \"/\",\n    \"contents\": [\n      {{ \"key\": \"title\", \"valueString\": \"Book [Item Name]\" }},\n      {{ \"key\": \"address\", \"valueString\": \"[Address]\" }},\n      {{ \"key\": \"itemName\", \"valueString\": \"[Item Name]\" }},\n      {{ \"key\": \"imageUrl\", \"valueString\": \"[Image URL]\" }},\n      {{ \"key\": \"partySize\", \"valueString\": \"2\" }},\n      {{ \"key\": \"reservationTime\", \"valueString\": \"\" }},\n      {{ \"key\": \"notes\", \"valueString\": \"\" }}\n    ]\n  }} }}\n]\n---END BOOKING_FORM_WITH_IMAGE_EXAMPLE---\n\"\"\"\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/skills/A2UI_response_generator/UI_templete_examples/contact_form.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n\"\"\"A2UI template example for contact form.\"\"\"\n\nCONTACT_FORM_EXAMPLE = \"\"\"\n---BEGIN CONTACT_FORM_EXAMPLE---\nUse this template for contact or feedback forms.\n\n[\n  {{ \"beginRendering\": {{ \"surfaceId\": \"contact-form\", \"root\": \"contact-column\", \"styles\": {{ \"primaryColor\": \"#4CAF50\", \"font\": \"Roboto\" }} }} }},\n  {{ \"surfaceUpdate\": {{\n    \"surfaceId\": \"contact-form\",\n    \"components\": [\n      {{ \"id\": \"contact-column\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"contact-title\", \"name-field\", \"email-field\", \"subject-field\", \"message-field\", \"send-button\"] }} }} }} }},\n      {{ \"id\": \"contact-title\", \"component\": {{ \"Text\": {{ \"usageHint\": \"h2\", \"text\": {{ \"literalString\": \"Contact Us\" }} }} }} }},\n      {{ \"id\": \"name-field\", \"component\": {{ \"TextField\": {{ \"label\": {{ \"literalString\": \"Your Name\" }}, \"text\": {{ \"path\": \"name\" }} }} }} }},\n      {{ \"id\": \"email-field\", \"component\": {{ \"TextField\": {{ \"label\": {{ \"literalString\": \"Email Address\" }}, \"text\": {{ \"path\": \"email\" }}, \"type\": \"email\" }} }} }},\n      {{ \"id\": \"subject-field\", \"component\": {{ \"TextField\": {{ \"label\": {{ \"literalString\": \"Subject\" }}, \"text\": {{ \"path\": \"subject\" }} }} }} }},\n      {{ \"id\": \"message-field\", \"component\": {{ \"TextField\": {{ \"label\": {{ \"literalString\": \"Message\" }}, \"text\": {{ \"path\": \"message\" }}, \"multiline\": true }} }} }},\n      {{ \"id\": \"send-button\", \"component\": {{ \"Button\": {{ \"child\": \"send-text\", \"primary\": true, \"action\": {{ \"name\": \"send_message\", \"context\": [ {{ \"key\": \"name\", \"value\": {{ \"path\": \"name\" }} }}, {{ \"key\": \"email\", \"value\": {{ \"path\": \"email\" }} }}, {{ \"key\": \"subject\", \"value\": {{ \"path\": \"subject\" }} }}, {{ \"key\": \"message\", \"value\": {{ \"path\": \"message\" }} }} ] }} }} }} }},\n      {{ \"id\": \"send-text\", \"component\": {{ \"Text\": {{ \"text\": {{ \"literalString\": \"Send Message\" }} }} }} }}\n    ]\n  }} }},\n  {{ \"dataModelUpdate\": {{\n    \"surfaceId\": \"contact-form\",\n    \"path\": \"/\",\n    \"contents\": [\n      {{ \"key\": \"name\", \"valueString\": \"\" }},\n      {{ \"key\": \"email\", \"valueString\": \"\" }},\n      {{ \"key\": \"subject\", \"valueString\": \"\" }},\n      {{ \"key\": \"message\", \"valueString\": \"\" }}\n    ]\n  }} }}\n]\n---END CONTACT_FORM_EXAMPLE---\n\"\"\"\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/skills/A2UI_response_generator/UI_templete_examples/email_compose_form.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n\"\"\"A2UI template example for email compose form.\"\"\"\n\nEMAIL_COMPOSE_FORM_EXAMPLE = \"\"\"\n---BEGIN EMAIL_COMPOSE_FORM_EXAMPLE---\nUse this template for composing and sending emails.\n\n[\n  {{ \"beginRendering\": {{ \"surfaceId\": \"email-compose\", \"root\": \"email-column\", \"styles\": {{ \"primaryColor\": \"#1976D2\", \"font\": \"Roboto\" }} }} }},\n  {{ \"surfaceUpdate\": {{\n    \"surfaceId\": \"email-compose\",\n    \"components\": [\n      {{ \"id\": \"email-column\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"email-title\", \"to-field\", \"cc-field\", \"bcc-field\", \"subject-field\", \"body-field\", \"attach-row\", \"send-button-row\"] }} }} }} }},\n      {{ \"id\": \"email-title\", \"component\": {{ \"Text\": {{ \"usageHint\": \"h2\", \"text\": {{ \"literalString\": \"Compose Email\" }} }} }} }},\n      {{ \"id\": \"to-field\", \"component\": {{ \"TextField\": {{ \"label\": {{ \"literalString\": \"To\" }}, \"text\": {{ \"path\": \"to\" }}, \"type\": \"email\" }} }} }},\n      {{ \"id\": \"cc-field\", \"component\": {{ \"TextField\": {{ \"label\": {{ \"literalString\": \"Cc\" }}, \"text\": {{ \"path\": \"cc\" }}, \"type\": \"email\" }} }} }},\n      {{ \"id\": \"bcc-field\", \"component\": {{ \"TextField\": {{ \"label\": {{ \"literalString\": \"Bcc\" }}, \"text\": {{ \"path\": \"bcc\" }}, \"type\": \"email\" }} }} }},\n      {{ \"id\": \"subject-field\", \"component\": {{ \"TextField\": {{ \"label\": {{ \"literalString\": \"Subject\" }}, \"text\": {{ \"path\": \"subject\" }} }} }} }},\n      {{ \"id\": \"body-field\", \"component\": {{ \"TextField\": {{ \"label\": {{ \"literalString\": \"Message\" }}, \"text\": {{ \"path\": \"body\" }}, \"multiline\": true }} }} }},\n      {{ \"id\": \"attach-row\", \"component\": {{ \"Row\": {{ \"children\": {{ \"explicitList\": [\"attach-icon\", \"attach-text\"] }} }} }} }},\n      {{ \"id\": \"attach-icon\", \"component\": {{ \"Icon\": {{ \"name\": {{ \"literalString\": \"attachFile\" }} }} }} }},\n      {{ \"id\": \"attach-text\", \"weight\": 1, \"component\": {{ \"Text\": {{ \"text\": {{ \"literalString\": \"Attach File\" }}, \"usageHint\": \"caption\" }} }} }},\n      {{ \"id\": \"send-button-row\", \"component\": {{ \"Row\": {{ \"children\": {{ \"explicitList\": [\"send-button\", \"draft-button\"] }}, \"distribution\": \"end\" }} }} }},\n      {{ \"id\": \"send-button\", \"component\": {{ \"Button\": {{ \"child\": \"send-text\", \"primary\": true, \"action\": {{ \"name\": \"send_email\", \"context\": [ {{ \"key\": \"to\", \"value\": {{ \"path\": \"to\" }} }}, {{ \"key\": \"cc\", \"value\": {{ \"path\": \"cc\" }} }}, {{ \"key\": \"bcc\", \"value\": {{ \"path\": \"bcc\" }} }}, {{ \"key\": \"subject\", \"value\": {{ \"path\": \"subject\" }} }}, {{ \"key\": \"body\", \"value\": {{ \"path\": \"body\" }} }} ] }} }} }},\n      {{ \"id\": \"send-text\", \"component\": {{ \"Text\": {{ \"text\": {{ \"literalString\": \"Send\" }} }} }} }},\n      {{ \"id\": \"draft-button\", \"component\": {{ \"Button\": {{ \"child\": \"draft-text\", \"action\": {{ \"name\": \"save_draft\", \"context\": [ {{ \"key\": \"to\", \"value\": {{ \"path\": \"to\" }} }}, {{ \"key\": \"cc\", \"value\": {{ \"path\": \"cc\" }} }}, {{ \"key\": \"bcc\", \"value\": {{ \"path\": \"bcc\" }} }}, {{ \"key\": \"subject\", \"value\": {{ \"path\": \"subject\" }} }}, {{ \"key\": \"body\", \"value\": {{ \"path\": \"body\" }} }} ] }} }} }},\n      {{ \"id\": \"draft-text\", \"component\": {{ \"Text\": {{ \"text\": {{ \"literalString\": \"Save Draft\" }} }} }} }}\n    ]\n  }} }},\n  {{ \"dataModelUpdate\": {{\n    \"surfaceId\": \"email-compose\",\n    \"path\": \"/\",\n    \"contents\": [\n      {{ \"key\": \"to\", \"valueString\": \"\" }},\n      {{ \"key\": \"cc\", \"valueString\": \"\" }},\n      {{ \"key\": \"bcc\", \"valueString\": \"\" }},\n      {{ \"key\": \"subject\", \"valueString\": \"\" }},\n      {{ \"key\": \"body\", \"valueString\": \"\" }}\n    ]\n  }} }}\n]\n---END EMAIL_COMPOSE_FORM_EXAMPLE---\n\"\"\"\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/skills/A2UI_response_generator/UI_templete_examples/error_message.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n\"\"\"A2UI template example for error message.\"\"\"\n\nERROR_MESSAGE_EXAMPLE = \"\"\"\n---BEGIN ERROR_MESSAGE_EXAMPLE---\nUse this template to display error or warning messages.\n\n[\n  {{ \"beginRendering\": {{\n    \"surfaceId\": \"error-message\",\n    \"root\": \"error-card\",\n    \"styles\": {{ \"primaryColor\": \"#F44336\", \"font\": \"Roboto\" }}\n  }} }},\n  {{ \"surfaceUpdate\": {{\n    \"surfaceId\": \"error-message\",\n    \"components\": [\n      {{ \"id\": \"error-card\", \"component\": {{\n        \"Card\": {{ \"child\": \"error-column\" }}\n      }} }},\n      {{ \"id\": \"error-column\", \"component\": {{ \"Column\": {{\n        \"children\": {{ \"explicitList\": [\n          \"error-icon\", \"error-title\", \"error-message\", \"retry-button\"\n        ] }}\n      }} }} }},\n      {{ \"id\": \"error-icon\", \"component\": {{ \"Icon\": {{\n        \"name\": {{ \"literalString\": \"error\" }}\n      }} }} }},\n      {{ \"id\": \"error-title\", \"component\": {{\n        \"Text\": {{ \"usageHint\": \"h3\", \"text\": {{ \"path\": \"title\" }} }}\n      }} }},\n      {{ \"id\": \"error-message\", \"component\": {{\n        \"Text\": {{ \"text\": {{ \"path\": \"message\" }} }}\n      }} }},\n      {{ \"id\": \"retry-button\", \"component\": {{\n        \"Button\": {{\n          \"child\": \"retry-text\",\n          \"primary\": true,\n          \"action\": {{ \"name\": \"retry\", \"context\": [] }}\n        }}\n      }} }},\n      {{ \"id\": \"retry-text\", \"component\": {{\n        \"Text\": {{ \"text\": {{ \"literalString\": \"Try Again\" }} }}\n      }} }}\n    ]\n  }} }},\n  {{ \"dataModelUpdate\": {{\n    \"surfaceId\": \"error-message\",\n    \"path\": \"/\",\n    \"contents\": [\n      {{ \"key\": \"title\", \"valueString\": \"Something went wrong\" }},\n      {{ \"key\": \"message\",\n        \"valueString\": \"[Error description and suggested action]\" }}\n    ]\n  }} }}\n]\n---END ERROR_MESSAGE_EXAMPLE---\n\"\"\"\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/skills/A2UI_response_generator/UI_templete_examples/info_message.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n\"\"\"A2UI template example for info message.\"\"\"\n\nINFO_MESSAGE_EXAMPLE = \"\"\"\n---BEGIN INFO_MESSAGE_EXAMPLE---\nUse this template to display informational messages.\n\n[\n  {{ \"beginRendering\": {{\n    \"surfaceId\": \"info-message\",\n    \"root\": \"info-card\",\n    \"styles\": {{ \"primaryColor\": \"#2196F3\", \"font\": \"Roboto\" }}\n  }} }},\n  {{ \"surfaceUpdate\": {{\n    \"surfaceId\": \"info-message\",\n    \"components\": [\n      {{ \"id\": \"info-card\", \"component\": {{ \"Card\": {{ \"child\": \"info-column\" }} }} }},\n      {{ \"id\": \"info-column\", \"component\": {{\n        \"Column\": {{\n          \"children\": {{\n            \"explicitList\": [\n              \"info-icon\", \"info-title\", \"info-message\", \"dismiss-button\"\n            ]\n          }}\n        }}\n      }} }},\n      {{ \"id\": \"info-icon\", \"component\": {{ \"Icon\": {{ \"name\": {{ \"literalString\": \"info\" }} }} }} }},\n      {{ \"id\": \"info-title\", \"component\": {{\n        \"Text\": {{ \"usageHint\": \"h3\", \"text\": {{ \"path\": \"title\" }} }}\n      }} }},\n      {{ \"id\": \"info-message\", \"component\": {{\n        \"Text\": {{ \"text\": {{ \"path\": \"message\" }} }}\n      }} }},\n      {{ \"id\": \"dismiss-button\", \"component\": {{\n        \"Button\": {{\n          \"child\": \"dismiss-text\",\n          \"action\": {{ \"name\": \"dismiss\", \"context\": [] }}\n        }}\n      }} }},\n      {{ \"id\": \"dismiss-text\", \"component\": {{\n        \"Text\": {{ \"text\": {{ \"literalString\": \"Got it\" }} }}\n      }} }}\n    ]\n  }} }},\n  {{ \"dataModelUpdate\": {{\n    \"surfaceId\": \"info-message\",\n    \"path\": \"/\",\n    \"contents\": [\n      {{ \"key\": \"title\", \"valueString\": \"[Info Title]\" }},\n      {{ \"key\": \"message\",\n        \"valueString\": \"[Informational message]\" }}\n    ]\n  }} }}\n]\n---END INFO_MESSAGE_EXAMPLE---\n\"\"\"\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/skills/A2UI_response_generator/UI_templete_examples/item_detail_card_with_image.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n\"\"\"A2UI template example for item detail card with image.\"\"\"\n\n# Detail examples\nITEM_DETAIL_CARD_EXAMPLE_WITH_IMAGE = \"\"\"\n---BEGIN ITEM_DETAIL_CARD_EXAMPLE_WITH_IMAGE---\nUse this template to display detailed information about a single item.\n\n[\n  {{ \"beginRendering\": {{ \"surfaceId\": \"item-detail\", \"root\": \"detail-column\", \"styles\": {{ \"primaryColor\": \"#673AB7\", \"font\": \"Roboto\" }} }} }},\n  {{ \"surfaceUpdate\": {{\n    \"surfaceId\": \"item-detail\",\n    \"components\": [\n      {{ \"id\": \"detail-column\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"header-image\", \"detail-card\"] }} }} }} }},\n      {{ \"id\": \"header-image\", \"component\": {{ \"Image\": {{ \"url\": {{ \"path\": \"imageUrl\" }}, \"usageHint\": \"header\" }} }} }},\n      {{ \"id\": \"detail-card\", \"component\": {{ \"Card\": {{ \"child\": \"card-content\" }} }} }},\n      {{ \"id\": \"card-content\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"item-title\", \"item-subtitle\", \"divider1\", \"description-section\", \"divider2\", \"info-section\", \"action-row\"] }} }} }} }},\n      {{ \"id\": \"item-title\", \"component\": {{ \"Text\": {{ \"usageHint\": \"h1\", \"text\": {{ \"path\": \"name\" }} }} }} }},\n      {{ \"id\": \"item-subtitle\", \"component\": {{ \"Text\": {{ \"usageHint\": \"caption\", \"text\": {{ \"path\": \"subtitle\" }} }} }} }},\n      {{ \"id\": \"divider1\", \"component\": {{ \"Divider\": {{}} }} }},\n      {{ \"id\": \"description-section\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"description-title\", \"description-text\"] }} }} }} }},\n      {{ \"id\": \"description-title\", \"component\": {{ \"Text\": {{ \"usageHint\": \"h4\", \"text\": {{ \"literalString\": \"Description\" }} }} }} }},\n      {{ \"id\": \"description-text\", \"component\": {{ \"Text\": {{ \"text\": {{ \"path\": \"description\" }} }} }} }},\n      {{ \"id\": \"divider2\", \"component\": {{ \"Divider\": {{}} }} }},\n      {{ \"id\": \"info-section\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"info-row-1\", \"info-row-2\", \"info-row-3\"] }} }} }} }},\n      {{ \"id\": \"info-row-1\", \"component\": {{ \"Row\": {{ \"children\": {{ \"explicitList\": [\"info-icon-1\", \"info-text-1\"] }} }} }} }},\n      {{ \"id\": \"info-icon-1\", \"component\": {{ \"Icon\": {{ \"name\": {{ \"literalString\": \"locationOn\" }} }} }} }},\n      {{ \"id\": \"info-text-1\", \"weight\": 1, \"component\": {{ \"Text\": {{ \"text\": {{ \"path\": \"location\" }} }} }} }},\n      {{ \"id\": \"info-row-2\", \"component\": {{ \"Row\": {{ \"children\": {{ \"explicitList\": [\"info-icon-2\", \"info-text-2\"] }} }} }} }},\n      {{ \"id\": \"info-icon-2\", \"component\": {{ \"Icon\": {{ \"name\": {{ \"literalString\": \"phone\" }} }} }} }},\n      {{ \"id\": \"info-text-2\", \"weight\": 1, \"component\": {{ \"Text\": {{ \"text\": {{ \"path\": \"phone\" }} }} }} }},\n      {{ \"id\": \"info-row-3\", \"component\": {{ \"Row\": {{ \"children\": {{ \"explicitList\": [\"info-icon-3\", \"info-text-3\"] }} }} }} }},\n      {{ \"id\": \"info-icon-3\", \"component\": {{ \"Icon\": {{ \"name\": {{ \"literalString\": \"star\" }} }} }} }},\n      {{ \"id\": \"info-text-3\", \"weight\": 1, \"component\": {{ \"Text\": {{ \"text\": {{ \"path\": \"rating\" }} }} }} }},\n      {{ \"id\": \"action-row\", \"component\": {{ \"Row\": {{ \"children\": {{ \"explicitList\": [\"share-button\", \"primary-action-button\"] }} }} }} }},\n      {{ \"id\": \"share-button\", \"weight\": 1, \"component\": {{ \"Button\": {{ \"child\": \"share-text\", \"action\": {{ \"name\": \"share\", \"context\": [ {{ \"key\": \"itemId\", \"value\": {{ \"path\": \"id\" }} }} ] }} }} }} }},\n      {{ \"id\": \"share-text\", \"component\": {{ \"Text\": {{ \"text\": {{ \"literalString\": \"Share\" }} }} }} }},\n      {{ \"id\": \"primary-action-button\", \"weight\": 1, \"component\": {{ \"Button\": {{ \"child\": \"action-text\", \"primary\": true, \"action\": {{ \"name\": \"select_item\", \"context\": [ {{ \"key\": \"itemId\", \"value\": {{ \"path\": \"id\" }} }}, {{ \"key\": \"itemName\", \"value\": {{ \"path\": \"name\" }} }} ] }} }} }} }},\n      {{ \"id\": \"action-text\", \"component\": {{ \"Text\": {{ \"text\": {{ \"literalString\": \"Book Now\" }} }} }} }}\n    ]\n  }} }},\n  {{ \"dataModelUpdate\": {{\n    \"surfaceId\": \"item-detail\",\n    \"path\": \"/\",\n    \"contents\": [\n      {{ \"key\": \"id\", \"valueString\": \"[Item ID]\" }},\n      {{ \"key\": \"name\", \"valueString\": \"[Item Name]\" }},\n      {{ \"key\": \"subtitle\", \"valueString\": \"[Category or Type]\" }},\n      {{ \"key\": \"imageUrl\", \"valueString\": \"[Header Image URL]\" }},\n      {{ \"key\": \"description\", \"valueString\": \"[Detailed description of the item]\" }},\n      {{ \"key\": \"location\", \"valueString\": \"[Address or Location]\" }},\n      {{ \"key\": \"phone\", \"valueString\": \"[Phone Number]\" }},\n      {{ \"key\": \"rating\", \"valueString\": \"[Rating] stars\" }}\n    ]\n  }} }}\n]\n---END ITEM_DETAIL_CARD_EXAMPLE_WITH_IMAGE---\n\"\"\"\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/skills/A2UI_response_generator/UI_templete_examples/profile_view.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n\"\"\"A2UI template example for profile view.\"\"\"\n\nPROFILE_VIEW_WITH_IMAGE_EXAMPLE = \"\"\"\n---BEGIN PROFILE_VIEW_WITH_IMAGE_EXAMPLE---\nUse this template to display user or entity profile information.\n\n[\n  {{ \"beginRendering\": {{ \"surfaceId\": \"profile\", \"root\": \"profile-column\", \"styles\": {{ \"primaryColor\": \"#009688\", \"font\": \"Roboto\" }} }} }},\n  {{ \"surfaceUpdate\": {{\n    \"surfaceId\": \"profile\",\n    \"components\": [\n      {{ \"id\": \"profile-column\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"profile-header\", \"profile-card\"] }} }} }} }},\n      {{ \"id\": \"profile-header\", \"component\": {{ \"Row\": {{ \"children\": {{ \"explicitList\": [\"avatar-image\", \"header-info\"] }} }} }} }},\n      {{ \"id\": \"avatar-image\", \"component\": {{ \"Image\": {{ \"url\": {{ \"path\": \"avatarUrl\" }}, \"usageHint\": \"avatar\" }} }} }},\n      {{ \"id\": \"header-info\", \"weight\": 1, \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"profile-name\", \"profile-title\"] }} }} }} }},\n      {{ \"id\": \"profile-name\", \"component\": {{ \"Text\": {{ \"usageHint\": \"h2\", \"text\": {{ \"path\": \"name\" }} }} }} }},\n      {{ \"id\": \"profile-title\", \"component\": {{ \"Text\": {{ \"usageHint\": \"caption\", \"text\": {{ \"path\": \"title\" }} }} }} }},\n      {{ \"id\": \"profile-card\", \"component\": {{ \"Card\": {{ \"child\": \"profile-details\" }} }} }},\n      {{ \"id\": \"profile-details\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"bio-section\", \"divider1\", \"contact-section\", \"divider2\", \"stats-section\"] }} }} }} }},\n      {{ \"id\": \"bio-section\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"bio-title\", \"bio-text\"] }} }} }} }},\n      {{ \"id\": \"bio-title\", \"component\": {{ \"Text\": {{ \"usageHint\": \"h4\", \"text\": {{ \"literalString\": \"About\" }} }} }} }},\n      {{ \"id\": \"bio-text\", \"component\": {{ \"Text\": {{ \"text\": {{ \"path\": \"bio\" }} }} }} }},\n      {{ \"id\": \"divider1\", \"component\": {{ \"Divider\": {{}} }} }},\n      {{ \"id\": \"contact-section\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"email-row\", \"phone-row\"] }} }} }} }},\n      {{ \"id\": \"email-row\", \"component\": {{ \"Row\": {{ \"children\": {{ \"explicitList\": [\"email-icon\", \"email-text\"] }} }} }} }},\n      {{ \"id\": \"email-icon\", \"component\": {{ \"Icon\": {{ \"name\": {{ \"literalString\": \"mail\" }} }} }} }},\n      {{ \"id\": \"email-text\", \"weight\": 1, \"component\": {{ \"Text\": {{ \"text\": {{ \"path\": \"email\" }} }} }} }},\n      {{ \"id\": \"phone-row\", \"component\": {{ \"Row\": {{ \"children\": {{ \"explicitList\": [\"phone-icon\", \"phone-text\"] }} }} }} }},\n      {{ \"id\": \"phone-icon\", \"component\": {{ \"Icon\": {{ \"name\": {{ \"literalString\": \"phone\" }} }} }} }},\n      {{ \"id\": \"phone-text\", \"weight\": 1, \"component\": {{ \"Text\": {{ \"text\": {{ \"path\": \"phone\" }} }} }} }},\n      {{ \"id\": \"divider2\", \"component\": {{ \"Divider\": {{}} }} }},\n      {{ \"id\": \"stats-section\", \"component\": {{ \"Row\": {{ \"children\": {{ \"explicitList\": [\"stat-1\", \"stat-2\", \"stat-3\"] }} }} }} }},\n      {{ \"id\": \"stat-1\", \"weight\": 1, \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"stat-1-value\", \"stat-1-label\"] }} }} }} }},\n      {{ \"id\": \"stat-1-value\", \"component\": {{ \"Text\": {{ \"usageHint\": \"h3\", \"text\": {{ \"path\": \"stat1Value\" }} }} }} }},\n      {{ \"id\": \"stat-1-label\", \"component\": {{ \"Text\": {{ \"usageHint\": \"caption\", \"text\": {{ \"path\": \"stat1Label\" }} }} }} }},\n      {{ \"id\": \"stat-2\", \"weight\": 1, \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"stat-2-value\", \"stat-2-label\"] }} }} }} }},\n      {{ \"id\": \"stat-2-value\", \"component\": {{ \"Text\": {{ \"usageHint\": \"h3\", \"text\": {{ \"path\": \"stat2Value\" }} }} }} }},\n      {{ \"id\": \"stat-2-label\", \"component\": {{ \"Text\": {{ \"usageHint\": \"caption\", \"text\": {{ \"path\": \"stat2Label\" }} }} }} }},\n      {{ \"id\": \"stat-3\", \"weight\": 1, \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"stat-3-value\", \"stat-3-label\"] }} }} }} }},\n      {{ \"id\": \"stat-3-value\", \"component\": {{ \"Text\": {{ \"usageHint\": \"h3\", \"text\": {{ \"path\": \"stat3Value\" }} }} }} }},\n      {{ \"id\": \"stat-3-label\", \"component\": {{ \"Text\": {{ \"usageHint\": \"caption\", \"text\": {{ \"path\": \"stat3Label\" }} }} }} }}\n    ]\n  }} }},\n  {{ \"dataModelUpdate\": {{\n    \"surfaceId\": \"profile\",\n    \"path\": \"/\",\n    \"contents\": [\n      {{ \"key\": \"name\", \"valueString\": \"[User Name]\" }},\n      {{ \"key\": \"title\", \"valueString\": \"[Job Title or Role]\" }},\n      {{ \"key\": \"avatarUrl\", \"valueString\": \"[Avatar Image URL]\" }},\n      {{ \"key\": \"bio\", \"valueString\": \"[User biography or description]\" }},\n      {{ \"key\": \"email\", \"valueString\": \"[Email Address]\" }},\n      {{ \"key\": \"phone\", \"valueString\": \"[Phone Number]\" }},\n      {{ \"key\": \"stat1Value\", \"valueString\": \"[Value]\" }},\n      {{ \"key\": \"stat1Label\", \"valueString\": \"[Label]\" }},\n      {{ \"key\": \"stat2Value\", \"valueString\": \"[Value]\" }},\n      {{ \"key\": \"stat2Label\", \"valueString\": \"[Label]\" }},\n      {{ \"key\": \"stat3Value\", \"valueString\": \"[Value]\" }},\n      {{ \"key\": \"stat3Label\", \"valueString\": \"[Label]\" }}\n    ]\n  }} }}\n]\n---END PROFILE_VIEW_WITH_IMAGE_EXAMPLE---\n\"\"\"\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/skills/A2UI_response_generator/UI_templete_examples/search_filter_form.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n\"\"\"A2UI template example for search filter form.\"\"\"\n\nSEARCH_FILTER_FORM_EXAMPLE = \"\"\"\n---BEGIN SEARCH_FILTER_FORM_EXAMPLE---\nUse this template for search forms with filters.\n\n[\n  {{ \"beginRendering\": {{ \"surfaceId\": \"search-form\", \"root\": \"search-column\", \"styles\": {{ \"primaryColor\": \"#2196F3\", \"font\": \"Roboto\" }} }} }},\n  {{ \"surfaceUpdate\": {{\n    \"surfaceId\": \"search-form\",\n    \"components\": [\n      {{ \"id\": \"search-column\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"search-title\", \"search-input-row\", \"filter-section\", \"search-button\"] }} }} }} }},\n      {{ \"id\": \"search-title\", \"component\": {{ \"Text\": {{ \"usageHint\": \"h2\", \"text\": {{ \"literalString\": \"Search\" }} }} }} }},\n      {{ \"id\": \"search-input-row\", \"component\": {{ \"Row\": {{ \"children\": {{ \"explicitList\": [\"search-icon\", \"search-field\"] }} }} }} }},\n      {{ \"id\": \"search-icon\", \"component\": {{ \"Icon\": {{ \"name\": {{ \"literalString\": \"search\" }} }} }} }},\n      {{ \"id\": \"search-field\", \"weight\": 1, \"component\": {{ \"TextField\": {{ \"label\": {{ \"literalString\": \"Search\" }}, \"text\": {{ \"path\": \"searchQuery\" }}, \"hint\": {{ \"literalString\": \"Enter keywords...\" }} }} }} }},\n      {{ \"id\": \"filter-section\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"filter-title\", \"location-field\", \"category-field\", \"price-range-row\"] }} }} }} }},\n      {{ \"id\": \"filter-title\", \"component\": {{ \"Text\": {{ \"usageHint\": \"h4\", \"text\": {{ \"literalString\": \"Filters\" }} }} }} }},\n      {{ \"id\": \"location-field\", \"component\": {{ \"TextField\": {{ \"label\": {{ \"literalString\": \"Location\" }}, \"text\": {{ \"path\": \"location\" }} }} }} }},\n      {{ \"id\": \"category-field\", \"component\": {{ \"TextField\": {{ \"label\": {{ \"literalString\": \"Category\" }}, \"text\": {{ \"path\": \"category\" }} }} }} }},\n      {{ \"id\": \"price-range-row\", \"component\": {{ \"Row\": {{ \"children\": {{ \"explicitList\": [\"min-price-field\", \"max-price-field\"] }} }} }} }},\n      {{ \"id\": \"min-price-field\", \"weight\": 1, \"component\": {{ \"TextField\": {{ \"label\": {{ \"literalString\": \"Min Price\" }}, \"text\": {{ \"path\": \"minPrice\" }}, \"type\": \"number\" }} }} }},\n      {{ \"id\": \"max-price-field\", \"weight\": 1, \"component\": {{ \"TextField\": {{ \"label\": {{ \"literalString\": \"Max Price\" }}, \"text\": {{ \"path\": \"maxPrice\" }}, \"type\": \"number\" }} }} }},\n      {{ \"id\": \"search-button\", \"component\": {{ \"Button\": {{ \"child\": \"search-button-text\", \"primary\": true, \"action\": {{ \"name\": \"perform_search\", \"context\": [ {{ \"key\": \"query\", \"value\": {{ \"path\": \"searchQuery\" }} }}, {{ \"key\": \"location\", \"value\": {{ \"path\": \"location\" }} }}, {{ \"key\": \"category\", \"value\": {{ \"path\": \"category\" }} }}, {{ \"key\": \"minPrice\", \"value\": {{ \"path\": \"minPrice\" }} }}, {{ \"key\": \"maxPrice\", \"value\": {{ \"path\": \"maxPrice\" }} }} ] }} }} }} }},\n      {{ \"id\": \"search-button-text\", \"component\": {{ \"Text\": {{ \"text\": {{ \"literalString\": \"Search\" }} }} }} }}\n    ]\n  }} }},\n  {{ \"dataModelUpdate\": {{\n    \"surfaceId\": \"search-form\",\n    \"path\": \"/\",\n    \"contents\": [\n      {{ \"key\": \"searchQuery\", \"valueString\": \"\" }},\n      {{ \"key\": \"location\", \"valueString\": \"\" }},\n      {{ \"key\": \"category\", \"valueString\": \"\" }},\n      {{ \"key\": \"minPrice\", \"valueString\": \"\" }},\n      {{ \"key\": \"maxPrice\", \"valueString\": \"\" }}\n    ]\n  }} }}\n]\n---END SEARCH_FILTER_FORM_EXAMPLE---\n\"\"\"\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/skills/A2UI_response_generator/UI_templete_examples/selection_card.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n\"\"\"A2UI template example for selection card.\"\"\"\n\nSELECTION_CARD_EXAMPLE = \"\"\"\n---BEGIN SELECTION_CARD_EXAMPLE---\nUse this template to display a single-choice question card using MultipleChoice component.\n\n[\n  { \"beginRendering\": { \"surfaceId\": \"quiz-card\", \"root\": \"quiz-card-root\", \"styles\": { \"primaryColor\": \"#673AB7\", \"font\": \"Roboto\" } } },\n  { \"surfaceUpdate\": {\n    \"surfaceId\": \"quiz-card\",\n    \"components\": [\n      { \"id\": \"quiz-card-root\", \"component\": { \"Card\": { \"child\": \"quiz-card-content\" } } },\n      { \"id\": \"quiz-card-content\", \"component\": { \"Column\": { \"children\": { \"explicitList\": [\"quiz-title\", \"quiz-question\", \"quiz-multiple-choice\", \"quiz-submit-button\"] } } } },\n      { \"id\": \"quiz-title\", \"component\": { \"Text\": { \"usageHint\": \"h1\", \"text\": { \"path\": \"/title\" } } } },\n      { \"id\": \"quiz-question\", \"component\": { \"Text\": { \"usageHint\": \"h2\", \"text\": { \"path\": \"/question\" } } } },\n      { \"id\": \"quiz-multiple-choice\", \"component\": { \"MultipleChoice\": { \"selections\": { \"path\": \"/selectedOptions\" }, \"options\": [\n        { \"label\": { \"literalString\": \"[Option A]\" }, \"value\": \"A\" },\n        { \"label\": { \"literalString\": \"[Option B]\" }, \"value\": \"B\" },\n        { \"label\": { \"literalString\": \"[Option C]\" }, \"value\": \"C\" },\n        { \"label\": { \"literalString\": \"[Option D]\" }, \"value\": \"D\" }\n      ], \"maxAllowedSelections\": 1 } } },\n      { \"id\": \"quiz-submit-button\", \"component\": { \"Button\": { \"child\": \"submit-text\", \"primary\": true, \"action\": { \"name\": \"submit_answer\", \"context\": [ { \"key\": \"questionId\", \"value\": { \"path\": \"/questionId\" } }, { \"key\": \"selectedOptions\", \"value\": { \"path\": \"/selectedOptions\" } } ] } } } },\n      { \"id\": \"submit-text\", \"component\": { \"Text\": { \"text\": { \"literalString\": \"Submit Answer\" } } } }\n    ]\n  } },\n  { \"dataModelUpdate\": {\n    \"surfaceId\": \"quiz-card\",\n    \"path\": \"/\",\n    \"contents\": [\n      { \"key\": \"title\", \"valueString\": \"[Quiz Title]\" },\n      { \"key\": \"question\", \"valueString\": \"[Question Content]\" },\n      { \"key\": \"questionId\", \"valueString\": \"[Question ID]\" },\n      { \"key\": \"selectedOptions\", \"valueMap\": [] }\n    ]\n  } }\n]\n---END SELECTION_CARD_EXAMPLE---\n\"\"\"\n\n\nMULTIPLE_SELECTION_CARDS_EXAMPLE = \"\"\"\n---BEGIN MULTIPLE_SELECTION_CARDS_EXAMPLE---\nUse this template to display multiple selection cards in a vertical list. Each card represents a separate question with checkboxes for each option.\n\n[\n  {\n    \"beginRendering\": {\n      \"surfaceId\": \"multi-quiz\",\n      \"root\": \"root-column\",\n      \"styles\": {\n        \"primaryColor\": \"#673AB7\",\n        \"font\": \"Roboto\"\n      }\n    }\n  },\n  {\n    \"surfaceUpdate\": {\n      \"surfaceId\": \"multi-quiz\",\n      \"components\": [\n        {\n          \"id\": \"root-column\",\n          \"component\": {\n            \"Column\": {\n              \"children\": {\n                \"explicitList\": [\"page-title\", \"quiz-list\"]\n              }\n            }\n          }\n        },\n        {\n          \"id\": \"page-title\",\n          \"component\": {\n            \"Text\": {\n              \"usageHint\": \"h1\",\n              \"text\": {\n                \"path\": \"pageTitle\"\n              }\n            }\n          }\n        },\n        {\n          \"id\": \"quiz-list\",\n          \"component\": {\n            \"List\": {\n              \"direction\": \"vertical\",\n              \"children\": {\n                \"template\": {\n                  \"componentId\": \"quiz-card-template\",\n                  \"dataBinding\": \"/questions\"\n                }\n              }\n            }\n          }\n        },\n        {\n          \"id\": \"quiz-card-template\",\n          \"component\": {\n            \"Card\": {\n              \"child\": \"quiz-card-content\"\n            }\n          }\n        },\n        {\n          \"id\": \"quiz-card-content\",\n          \"component\": {\n            \"Column\": {\n              \"children\": {\n                \"explicitList\": [\"quiz-title\", \"quiz-question\", \"quiz-multiple-choice\", \"quiz-submit-button\"]\n              }\n            }\n          }\n        },\n        {\n          \"id\": \"quiz-title\",\n          \"component\": {\n            \"Text\": {\n              \"usageHint\": \"h2\",\n              \"text\": {\n                \"path\": \"title\"\n              }\n            }\n          }\n        },\n        {\n          \"id\": \"quiz-question\",\n          \"component\": {\n            \"Text\": {\n              \"usageHint\": \"h3\",\n              \"text\": {\n                \"path\": \"question\"\n              }\n            }\n          }\n        },\n        {\n          \"id\": \"quiz-multiple-choice\",\n          \"component\": {\n            \"MultipleChoice\": {\n              \"selections\": {\n                \"path\": \"selectedOptions\"\n              },\n              \"options\": {\n                \"template\": {\n                  \"componentId\": \"option-template\",\n                  \"dataBinding\": \"options\"\n                }\n              },\n              \"maxAllowedSelections\": 3\n            }\n          }\n        },\n        {\n          \"id\": \"option-template\",\n          \"component\": {\n            \"MultipleChoiceOption\": {\n              \"label\": {\n                \"path\": \"text\"\n              },\n              \"value\": {\n                \"path\": \"id\"\n              }\n            }\n          }\n        },\n        {\n          \"id\": \"quiz-submit-button\",\n          \"component\": {\n            \"Button\": {\n              \"child\": \"submit-text\",\n              \"primary\": true,\n              \"action\": {\n                \"name\": \"submit_answer\",\n                \"context\": [\n                  {\n                    \"key\": \"questionId\",\n                    \"value\": {\n                      \"path\": \"questionId\"\n                    }\n                  },\n                  {\n                    \"key\": \"questionTitle\",\n                    \"value\": {\n                      \"path\": \"title\"\n                    }\n                  },\n                  {\n                    \"key\": \"selectedOptions\",\n                    \"value\": {\n                      \"path\": \"selectedOptions\"\n                    }\n                  }\n                ]\n              }\n            }\n          }\n        },\n        {\n          \"id\": \"submit-text\",\n          \"component\": {\n            \"Text\": {\n              \"text\": {\n                \"literalString\": \"Submit Answer\"\n              }\n            }\n          }\n        }\n      ]\n    }\n  },\n  {\n    \"dataModelUpdate\": {\n      \"surfaceId\": \"multi-quiz\",\n      \"path\": \"/\",\n      \"contents\": [\n        {\n          \"key\": \"pageTitle\",\n          \"valueString\": \"[Page Title]\"\n        },\n        {\n          \"key\": \"questions\",\n          \"valueMap\": [\n            {\n              \"key\": \"question1\",\n              \"valueMap\": [\n                {\n                  \"key\": \"questionId\",\n                  \"valueString\": \"q1\"\n                },\n                {\n                  \"key\": \"title\",\n                  \"valueString\": \"[Question 1 Title]\"\n                },\n                {\n                  \"key\": \"question\",\n                  \"valueString\": \"[Question 1 Content]\"\n                },\n                {\n                  \"key\": \"selectedOptions\",\n                  \"valueList\": []\n                },\n                {\n                  \"key\": \"options\",\n                  \"valueMap\": [\n                    {\n                      \"key\": \"option1\",\n                      \"valueMap\": [\n                        {\n                          \"key\": \"id\",\n                          \"valueString\": \"A\"\n                        },\n                        {\n                          \"key\": \"text\",\n                          \"valueString\": \"[Option A]\"\n                        }\n                      ]\n                    },\n                    {\n                      \"key\": \"option2\",\n                      \"valueMap\": [\n                        {\n                          \"key\": \"id\",\n                          \"valueString\": \"B\"\n                        },\n                        {\n                          \"key\": \"text\",\n                          \"valueString\": \"[Option B]\"\n                        }\n                      ]\n                    },\n                    {\n                      \"key\": \"option3\",\n                      \"valueMap\": [\n                        {\n                          \"key\": \"id\",\n                          \"valueString\": \"C\"\n                        },\n                        {\n                          \"key\": \"text\",\n                          \"valueString\": \"[Option C]\"\n                        }\n                      ]\n                    }\n                  ]\n                }\n              ]\n            }\n          ]\n        }\n      ]\n    }\n  }\n]\n\n\n---END MULTIPLE_SELECTION_CARDS_EXAMPLE---\n\"\"\"\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/skills/A2UI_response_generator/UI_templete_examples/simple_column_list_without_image.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n\"\"\"A2UI template example for simple column list without images.\"\"\"\n\nSIMPLE_LIST_EXAMPLE = \"\"\"\n---BEGIN SIMPLE_LIST_EXAMPLE---\nUse this template for compact lists without images.\n\n[\n  {{ \"beginRendering\": {{ \"surfaceId\": \"default\", \"root\": \"root-column\", \"styles\": {{ \"primaryColor\": \"#2196F3\", \"font\": \"Roboto\" }} }} }},\n  {{ \"surfaceUpdate\": {{\n    \"surfaceId\": \"default\",\n    \"components\": [\n      {{ \"id\": \"root-column\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"title-heading\", \"item-list\"] }} }} }} }},\n      {{ \"id\": \"title-heading\", \"component\": {{ \"Text\": {{ \"usageHint\": \"h1\", \"text\": {{ \"path\": \"title\" }} }} }} }},\n      {{ \"id\": \"item-list\", \"component\": {{ \"List\": {{ \"direction\": \"vertical\", \"children\": {{ \"template\": {{ \"componentId\": \"list-item-template\", \"dataBinding\": \"/items\" }} }} }} }} }},\n\n      # Change Row to Button to make the entire row clickable\n      {{ \"id\": \"list-item-template\", \"component\": {{ \"Button\": {{\n        \"usageHint\": \"listItem\",\n        \"action\": {{ \"path\": \"action\" }},\n        \"children\": {{ \"explicitList\": [\"item-icon\", \"item-content\", \"item-action\"] }}\n      }} }} }},\n\n      {{ \"id\": \"item-icon\", \"component\": {{ \"Icon\": {{ \"name\": {{ \"path\": \"icon\" }} }} }} }},\n      {{ \"id\": \"item-content\", \"weight\": 1, \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"item-title\", \"item-subtitle\"] }} }} }} }},\n      {{ \"id\": \"item-title\", \"component\": {{ \"Text\": {{ \"usageHint\": \"h4\", \"text\": {{ \"path\": \"title\" }} }} }} }},\n      {{ \"id\": \"item-subtitle\", \"component\": {{ \"Text\": {{ \"usageHint\": \"caption\", \"text\": {{ \"path\": \"subtitle\" }} }} }} }},\n      {{ \"id\": \"item-action\", \"component\": {{ \"Icon\": {{ \"name\": {{ \"literalString\": \"arrowForward\" }} }} }} }}\n    ]\n  }} }},\n  {{ \"dataModelUpdate\": {{\n    \"surfaceId\": \"default\",\n    \"path\": \"/\",\n    \"contents\": [\n      {{ \"key\": \"title\", \"valueString\": \"[List Title]\" }},\n      {{ \"key\": \"items\", \"valueMap\": [\n        {{ \"key\": \"item1\", \"valueMap\": [\n          {{ \"key\": \"icon\", \"valueString\": \"folder\" }},\n          {{ \"key\": \"title\", \"valueString\": \"[Item Title]\" }},\n          {{ \"key\": \"subtitle\", \"valueString\": \"[Item Subtitle]\" }},\n          {{ \"key\": \"action\", \"valueString\": \"view_details\" }}\n        ] }}\n      ] }}\n    ]\n  }} }}\n]\n---END SIMPLE_LIST_EXAMPLE---\n\"\"\"\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/skills/A2UI_response_generator/UI_templete_examples/single_column_list.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n\"\"\"A2UI template example for single column list with images.\"\"\"\n\n# List examples\nSINGLE_COLUMN_LIST_WITH_IMAGE_EXAMPLE = \"\"\"\n---BEGIN SINGLE_COLUMN_LIST_WITH_IMAGE_EXAMPLE---\nUse this template when displaying a list of 5 or fewer items with detailed information.\n\n[\n  {{ \"beginRendering\": {{ \"surfaceId\": \"default\", \"root\": \"root-column\", \"styles\": {{ \"primaryColor\": \"#FF0000\", \"font\": \"Roboto\" }} }} }},\n  {{ \"surfaceUpdate\": {{\n    \"surfaceId\": \"default\",\n    \"components\": [\n      {{ \"id\": \"root-column\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"title-heading\", \"item-list\"] }} }} }} }},\n      {{ \"id\": \"title-heading\", \"component\": {{ \"Text\": {{ \"usageHint\": \"h1\", \"text\": {{ \"path\": \"title\" }} }} }} }},\n      {{ \"id\": \"item-list\", \"component\": {{ \"List\": {{ \"direction\": \"vertical\", \"children\": {{ \"template\": {{ \"componentId\": \"item-card-template\", \"dataBinding\": \"/items\" }} }} }} }} }},\n      {{ \"id\": \"item-card-template\", \"component\": {{ \"Card\": {{ \"child\": \"card-layout\" }} }} }},\n      {{ \"id\": \"card-layout\", \"component\": {{ \"Row\": {{ \"children\": {{ \"explicitList\": [\"template-image\", \"card-details\"] }} }} }} }},\n      {{ \"id\": \"template-image\", \"weight\": 1, \"component\": {{ \"Image\": {{ \"url\": {{ \"path\": \"imageUrl\" }} }} }} }},\n      {{ \"id\": \"card-details\", \"weight\": 2, \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"template-name\", \"template-rating\", \"template-detail\", \"template-link\", \"template-action-button\"] }} }} }} }},\n      {{ \"id\": \"template-name\", \"component\": {{ \"Text\": {{ \"usageHint\": \"h3\", \"text\": {{ \"path\": \"name\" }} }} }} }},\n      {{ \"id\": \"template-rating\", \"component\": {{ \"Text\": {{ \"text\": {{ \"path\": \"rating\" }} }} }} }},\n      {{ \"id\": \"template-detail\", \"component\": {{ \"Text\": {{ \"text\": {{ \"path\": \"detail\" }} }} }} }},\n      {{ \"id\": \"template-link\", \"component\": {{ \"Text\": {{ \"text\": {{ \"path\": \"infoLink\" }} }} }} }},\n      {{ \"id\": \"template-action-button\", \"component\": {{ \"Button\": {{ \"child\": \"action-button-text\", \"primary\": true, \"action\": {{ \"name\": \"select_item\", \"context\": [ {{ \"key\": \"itemName\", \"value\": {{ \"path\": \"name\" }} }}, {{ \"key\": \"itemId\", \"value\": {{ \"path\": \"id\" }} }} ] }} }} }} }},\n      {{ \"id\": \"action-button-text\", \"component\": {{ \"Text\": {{ \"text\": {{ \"literalString\": \"Select\" }} }} }} }}\n    ]\n  }} }},\n  {{ \"dataModelUpdate\": {{\n    \"surfaceId\": \"default\",\n    \"path\": \"/\",\n    \"contents\": [\n      {{ \"key\": \"title\", \"valueString\": \"[Your List Title]\" }},\n      {{ \"key\": \"items\", \"valueMap\": [\n        {{ \"key\": \"item1\", \"valueMap\": [\n          {{ \"key\": \"id\", \"valueString\": \"1\" }},\n          {{ \"key\": \"name\", \"valueString\": \"[Item Name]\" }},\n          {{ \"key\": \"rating\", \"valueString\": \"[Rating]\" }},\n          {{ \"key\": \"detail\", \"valueString\": \"[Detail Description]\" }},\n          {{ \"key\": \"infoLink\", \"valueString\": \"[URL]\" }},\n          {{ \"key\": \"imageUrl\", \"valueString\": \"[Image URL]\" }}\n        ] }}\n      ] }}\n    ]\n  }} }}\n]\n---END SINGLE_COLUMN_LIST_WITH_IMAGE_EXAMPLE---\n\"\"\"\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/skills/A2UI_response_generator/UI_templete_examples/success_confirmation_with_image.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n\"\"\"A2UI template example for success confirmation with image.\"\"\"\n# Confirmation examples\nSUCCESS_CONFIRMATION_WITH_IMAGE_EXAMPLE = \"\"\"\n---BEGIN SUCCESS_CONFIRMATION_WITH_IMAGE_EXAMPLE---\nUse this template to display success confirmations after an action.\n\n[\n  {{ \"beginRendering\": {{ \"surfaceId\": \"confirmation\", \"root\": \"confirmation-card\", \"styles\": {{ \"primaryColor\": \"#4CAF50\", \"font\": \"Roboto\" }} }} }},\n  {{ \"surfaceUpdate\": {{\n    \"surfaceId\": \"confirmation\",\n    \"components\": [\n      {{ \"id\": \"confirmation-card\", \"component\": {{ \"Card\": {{ \"child\": \"confirmation-column\" }} }} }},\n      {{ \"id\": \"confirmation-column\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"success-icon\", \"confirm-title\", \"confirm-image\", \"divider1\", \"confirm-details\", \"divider2\", \"confirm-message\", \"action-button\"] }} }} }} }},\n      {{ \"id\": \"success-icon\", \"component\": {{ \"Icon\": {{\n        \"name\": {{ \"literalString\": \"check\" }}\n      }} }} }},\n      {{ \"id\": \"confirm-title\", \"component\": {{ \"Text\": {{\n        \"usageHint\": \"h2\", \"text\": {{ \"path\": \"title\" }}\n      }} }} }},\n      {{ \"id\": \"confirm-image\", \"component\": {{ \"Image\": {{\n        \"url\": {{ \"path\": \"imageUrl\" }}, \"usageHint\": \"mediumFeature\"\n      }} }} }},\n      {{ \"id\": \"confirm-details\", \"component\": {{ \"Text\": {{\n        \"text\": {{ \"path\": \"details\" }}\n      }} }} }},\n      {{ \"id\": \"confirm-message\", \"component\": {{ \"Text\": {{\n        \"usageHint\": \"h5\", \"text\": {{ \"path\": \"message\" }}\n      }} }} }},\n      {{ \"id\": \"divider1\", \"component\": {{ \"Divider\": {{}} }} }},\n      {{ \"id\": \"divider2\", \"component\": {{ \"Divider\": {{}} }} }},\n      {{ \"id\": \"action-button\", \"component\": {{ \"Button\": {{\n        \"child\": \"action-text\", \"action\": {{ \"name\": \"dismiss\", \"context\": [] }}\n      }} }} }},\n      {{ \"id\": \"action-text\", \"component\": {{ \"Text\": {{\n        \"text\": {{ \"literalString\": \"Done\" }}\n      }} }} }}\n    ]\n  }} }},\n  {{ \"dataModelUpdate\": {{\n    \"surfaceId\": \"confirmation\",\n    \"path\": \"/\",\n    \"contents\": [\n      {{ \"key\": \"title\", \"valueString\": \"[Confirmation Title]\" }},\n      {{ \"key\": \"details\", \"valueString\": \"[Booking/Action Details]\" }},\n      {{ \"key\": \"message\", \"valueString\": \"We look forward to seeing you!\" }},\n      {{ \"key\": \"imageUrl\", \"valueString\": \"[Image URL]\" }}\n    ]\n  }} }}\n]\n---END SUCCESS_CONFIRMATION_WITH_IMAGE_EXAMPLE---\n\"\"\"\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/skills/A2UI_response_generator/UI_templete_examples/two_column_list.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n\"\"\"A2UI template example for two column grid list with images.\"\"\"\n\nTWO_COLUMN_LIST_WITH_IMAGE_EXAMPLE = \"\"\"\n---BEGIN TWO_COLUMN_LIST_WITH_IMAGE_EXAMPLE---\nUse this template when displaying more than 5 items in a grid layout.\n\n[\n  {{ \"beginRendering\": {{ \"surfaceId\": \"default\", \"root\": \"root-column\", \"styles\": {{ \"primaryColor\": \"#FF0000\", \"font\": \"Roboto\" }} }} }},\n  {{ \"surfaceUpdate\": {{\n    \"surfaceId\": \"default\",\n    \"components\": [\n      {{ \"id\": \"root-column\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"title-heading\", \"item-row-1\", \"item-row-2\"] }} }} }} }},\n      {{ \"id\": \"title-heading\", \"component\": {{ \"Text\": {{ \"usageHint\": \"h1\", \"text\": {{ \"path\": \"title\" }} }} }} }},\n      {{ \"id\": \"item-row-1\", \"component\": {{ \"Row\": {{ \"children\": {{ \"explicitList\": [\"item-card-1\", \"item-card-2\"] }} }} }} }},\n      {{ \"id\": \"item-row-2\", \"component\": {{ \"Row\": {{ \"children\": {{ \"explicitList\": [\"item-card-3\", \"item-card-4\"] }} }} }} }},\n      {{ \"id\": \"item-card-1\", \"weight\": 1, \"component\": {{ \"Card\": {{ \"child\": \"card-layout-1\" }} }} }},\n      {{ \"id\": \"card-layout-1\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"template-image-1\", \"card-details-1\"] }} }} }} }},\n      {{ \"id\": \"template-image-1\", \"component\": {{ \"Image\": {{ \"url\": {{ \"path\": \"/items/0/imageUrl\" }}, \"width\": \"100%\" }} }} }},\n      {{ \"id\": \"card-details-1\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"template-name-1\", \"template-rating-1\", \"template-action-button-1\"] }} }} }} }},\n      {{ \"id\": \"template-name-1\", \"component\": {{ \"Text\": {{ \"usageHint\": \"h3\", \"text\": {{ \"path\": \"/items/0/name\" }} }} }} }},\n      {{ \"id\": \"template-rating-1\", \"component\": {{ \"Text\": {{ \"text\": {{ \"path\": \"/items/0/rating\" }} }} }} }},\n      {{ \"id\": \"template-action-button-1\", \"component\": {{ \"Button\": {{ \"child\": \"action-text-1\", \"action\": {{ \"name\": \"select_item\", \"context\": [ {{ \"key\": \"itemName\", \"value\": {{ \"path\": \"/items/0/name\" }} }} ] }} }} }} }},\n      {{ \"id\": \"action-text-1\", \"component\": {{ \"Text\": {{ \"text\": {{ \"literalString\": \"Select\" }} }} }} }},\n      {{ \"id\": \"item-card-2\", \"weight\": 1, \"component\": {{ \"Card\": {{ \"child\": \"card-layout-2\" }} }} }},\n      {{ \"id\": \"card-layout-2\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"template-image-2\", \"card-details-2\"] }} }} }} }},\n      {{ \"id\": \"template-image-2\", \"component\": {{ \"Image\": {{ \"url\": {{ \"path\": \"/items/1/imageUrl\" }}, \"width\": \"100%\" }} }} }},\n      {{ \"id\": \"card-details-2\", \"component\": {{ \"Column\": {{ \"children\": {{ \"explicitList\": [\"template-name-2\", \"template-rating-2\", \"template-action-button-2\"] }} }} }} }},\n      {{ \"id\": \"template-name-2\", \"component\": {{ \"Text\": {{ \"usageHint\": \"h3\", \"text\": {{ \"path\": \"/items/1/name\" }} }} }} }},\n      {{ \"id\": \"template-rating-2\", \"component\": {{ \"Text\": {{ \"text\": {{ \"path\": \"/items/1/rating\" }} }} }} }},\n      {{ \"id\": \"template-action-button-2\", \"component\": {{ \"Button\": {{ \"child\": \"action-text-2\", \"action\": {{ \"name\": \"select_item\", \"context\": [ {{ \"key\": \"itemName\", \"value\": {{ \"path\": \"/items/1/name\" }} }} ] }} }} }} }},\n      {{ \"id\": \"action-text-2\", \"component\": {{ \"Text\": {{ \"text\": {{ \"literalString\": \"Select\" }} }} }} }}\n    ]\n  }} }},\n  {{ \"dataModelUpdate\": {{\n    \"surfaceId\": \"default\",\n    \"path\": \"/\",\n    \"contents\": [\n      {{ \"key\": \"title\", \"valueString\": \"[Your Grid Title]\" }},\n      {{ \"key\": \"items\", \"valueMap\": [\n        {{ \"key\": \"0\", \"valueMap\": [\n          {{ \"key\": \"name\", \"valueString\": \"[Item 1 Name]\" }},\n          {{ \"key\": \"rating\", \"valueString\": \"[Rating]\" }},\n          {{ \"key\": \"imageUrl\", \"valueString\": \"[Image URL]\" }}\n        ] }},\n        {{ \"key\": \"1\", \"valueMap\": [\n          {{ \"key\": \"name\", \"valueString\": \"[Item 2 Name]\" }},\n          {{ \"key\": \"rating\", \"valueString\": \"[Rating]\" }},\n          {{ \"key\": \"imageUrl\", \"valueString\": \"[Image URL]\" }}\n        ] }}\n      ] }}\n    ]\n  }} }}\n]\n---END TWO_COLUMN_LIST_WITH_IMAGE_EXAMPLE---\n\"\"\"\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/skills/A2UI_response_generator/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"A2UI response generator skill package.\"\"\"\nfrom .view_a2ui_schema import view_a2ui_schema\nfrom .view_a2ui_examples import view_a2ui_examples\n\n__all__ = [\n    \"view_a2ui_schema\",\n    \"view_a2ui_examples\",\n]\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/skills/A2UI_response_generator/schema/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"A2UI schema module for validating A2UI protocol messages.\"\"\"\nfrom .base_schema import A2UI_SCHEMA\n\n__all__ = [\n    \"A2UI_SCHEMA\",\n]\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/skills/A2UI_response_generator/schema/base_schema.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n\"\"\"A2UI base schema definition for validating A2UI protocol messages.\"\"\"\n# The complete A2UI schema (copied from Google A2UI repository)\nA2UI_SCHEMA = r\"\"\"\n{\n  \"title\": \"A2UI Message Schema\",\n  \"description\": \"Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces. A message MUST contain exactly ONE of the action properties: 'beginRendering', 'surfaceUpdate', 'dataModelUpdate', or 'deleteSurface'.\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"beginRendering\": {\n      \"type\": \"object\",\n      \"description\": \"Signals the client to begin rendering a surface \"\n          \"with a root component and specific styles.\",\n      \"properties\": {\n        \"surfaceId\": {\n          \"type\": \"string\",\n          \"description\": \"The unique identifier for the UI surface to be rendered.\"\n        },\n        \"root\": {\n          \"type\": \"string\",\n          \"description\": \"The ID of the root component to render.\"\n        },\n        \"styles\": {\n          \"type\": \"object\",\n          \"description\": \"Styling information for the UI.\",\n          \"properties\": {\n            \"font\": {\n              \"type\": \"string\",\n              \"description\": \"The primary font for the UI.\"\n            },\n            \"primaryColor\": {\n              \"type\": \"string\",\n              \"description\": \"The primary UI color as a hexadecimal code (e.g., '#00BFFF').\",\n              \"pattern\": \"^#[0-9a-fA-F]{6}$\"\n            }\n          }\n        }\n      },\n      \"required\": [\"root\", \"surfaceId\"]\n    },\n    \"surfaceUpdate\": {\n      \"type\": \"object\",\n      \"description\": \"Updates a surface with a new set of components.\",\n      \"properties\": {\n        \"surfaceId\": {\n          \"type\": \"string\",\n          \"description\": \"The unique identifier for the UI surface to be updated. If you are adding a new surface this *must* be a new, unique identified that has never been used for any existing surfaces shown.\"\n        },\n        \"components\": {\n          \"type\": \"array\",\n          \"description\": \"A list containing all UI components for the surface.\",\n          \"minItems\": 1,\n          \"items\": {\n            \"type\": \"object\",\n            \"description\": \"Represents a *single* component in a UI widget tree. This component could be one of many supported types.\",\n            \"properties\": {\n              \"id\": {\n                \"type\": \"string\",\n                \"description\": \"The unique identifier for this component.\"\n              },\n              \"weight\": {\n                \"type\": \"number\",\n                \"description\": \"The relative weight of this component within a Row or Column. This corresponds to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column.\"\n              },\n              \"component\": {\n                \"type\": \"object\",\n                \"description\": \"A wrapper object that MUST contain exactly one key, which is the name of the component type (e.g., 'Heading'). The value is an object containing the properties for that specific component.\",\n                \"properties\": {\n                  \"Text\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"text\": {\n                        \"type\": \"object\",\n                        \"description\": \"The text content to display. This can be a literal string or a reference to a value in the data model ('path', e.g., '/doc/title'). While simple Markdown formatting is supported (i.e. without HTML, images, or links), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.\",\n                        \"properties\": {\n                          \"literalString\": {\n                            \"type\": \"string\"\n                          },\n                          \"path\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      },\n                      \"usageHint\": {\n                        \"type\": \"string\",\n                        \"description\": \"A hint for the base text style. One of:\\n- `h1`: Largest heading.\\n- `h2`: Second largest heading.\\n- `h3`: Third largest heading.\\n- `h4`: Fourth largest heading.\\n- `h5`: Fifth largest heading.\\n- `caption`: Small text for captions.\\n- `body`: Standard body text.\",\n                        \"enum\": [\n                          \"h1\",\n                          \"h2\",\n                          \"h3\",\n                          \"h4\",\n                          \"h5\",\n                          \"caption\",\n                          \"body\"\n                        ]\n                      }\n                    },\n                    \"required\": [\"text\"]\n                  },\n                  \"Image\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"url\": {\n                        \"type\": \"object\",\n                        \"description\": \"The URL of the image to display. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/thumbnail/url').\",\n                        \"properties\": {\n                          \"literalString\": {\n                            \"type\": \"string\"\n                          },\n                          \"path\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      },\n                      \"fit\": {\n                        \"type\": \"string\",\n                        \"description\": \"Specifies how the image should be resized to fit its container. This corresponds to the CSS 'object-fit' property.\",\n                        \"enum\": [\n                          \"contain\",\n                          \"cover\",\n                          \"fill\",\n                          \"none\",\n                          \"scale-down\"\n                        ]\n                      },\n                      \"usageHint\": {\n                        \"type\": \"string\",\n                        \"description\": \"A hint for the image size and style. One of:\\n- `icon`: Small square icon.\\n- `avatar`: Circular avatar image.\\n- `smallFeature`: Small feature image.\\n- `mediumFeature`: Medium feature image.\\n- `largeFeature`: Large feature image.\\n- `header`: Full-width, full bleed, header image.\",\n                        \"enum\": [\n                          \"icon\",\n                          \"avatar\",\n                          \"smallFeature\",\n                          \"mediumFeature\",\n                          \"largeFeature\",\n                          \"header\"\n                        ]\n                      }\n                    },\n                    \"required\": [\"url\"]\n                  },\n                  \"Icon\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"name\": {\n                        \"type\": \"object\",\n                        \"description\": \"The name of the icon to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/form/submit').\",\n                        \"properties\": {\n                          \"literalString\": {\n                            \"type\": \"string\",\n                            \"enum\": [\n                              \"accountCircle\",\n                              \"add\",\n                              \"arrowBack\",\n                              \"arrowForward\",\n                              \"attachFile\",\n                              \"calendarToday\",\n                              \"call\",\n                              \"camera\",\n                              \"check\",\n                              \"close\",\n                              \"delete\",\n                              \"download\",\n                              \"edit\",\n                              \"event\",\n                              \"error\",\n                              \"favorite\",\n                              \"favoriteOff\",\n                              \"folder\",\n                              \"help\",\n                              \"home\",\n                              \"info\",\n                              \"locationOn\",\n                              \"lock\",\n                              \"lockOpen\",\n                              \"mail\",\n                              \"menu\",\n                              \"moreVert\",\n                              \"moreHoriz\",\n                              \"notificationsOff\",\n                              \"notifications\",\n                              \"payment\",\n                              \"person\",\n                              \"phone\",\n                              \"photo\",\n                              \"print\",\n                              \"refresh\",\n                              \"search\",\n                              \"send\",\n                              \"settings\",\n                              \"share\",\n                              \"shoppingCart\",\n                              \"star\",\n                              \"starHalf\",\n                              \"starOff\",\n                              \"upload\",\n                              \"visibility\",\n                              \"visibilityOff\",\n                              \"warning\"\n                            ]\n                          },\n                          \"path\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    },\n                    \"required\": [\"name\"]\n                  },\n                  \"Video\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"url\": {\n                        \"type\": \"object\",\n                        \"description\": \"The URL of the video to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/video/url').\",\n                        \"properties\": {\n                          \"literalString\": {\n                            \"type\": \"string\"\n                          },\n                          \"path\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    },\n                    \"required\": [\"url\"]\n                  },\n                  \"AudioPlayer\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"url\": {\n                        \"type\": \"object\",\n                        \"description\": \"The URL of the audio to be played. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/song/url').\",\n                        \"properties\": {\n                          \"literalString\": {\n                            \"type\": \"string\"\n                          },\n                          \"path\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      },\n                      \"description\": {\n                        \"type\": \"object\",\n                        \"description\": \"A description of the audio, such as a title or summary. This can be a literal string or a reference to a value in the data model ('path', e.g. '/song/title').\",\n                        \"properties\": {\n                          \"literalString\": {\n                            \"type\": \"string\"\n                          },\n                          \"path\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    },\n                    \"required\": [\"url\"]\n                  },\n                  \"Row\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"children\": {\n                        \"type\": \"object\",\n                        \"description\": \"Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.\",\n                        \"properties\": {\n                          \"explicitList\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"template\": {\n                            \"type\": \"object\",\n                            \"description\": \"A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.\",\n                            \"properties\": {\n                              \"componentId\": {\n                                \"type\": \"string\"\n                              },\n                              \"dataBinding\": {\n                                \"type\": \"string\"\n                              }\n                            },\n                            \"required\": [\"componentId\", \"dataBinding\"]\n                          }\n                        }\n                      },\n                      \"distribution\": {\n                        \"type\": \"string\",\n                        \"description\": \"Defines the arrangement of children along the main axis (horizontally). This corresponds to the CSS 'justify-content' property.\",\n                        \"enum\": [\n                          \"center\",\n                          \"end\",\n                          \"spaceAround\",\n                          \"spaceBetween\",\n                          \"spaceEvenly\",\n                          \"start\"\n                        ]\n                      },\n                      \"alignment\": {\n                        \"type\": \"string\",\n                        \"description\": \"Defines the alignment of children along the cross axis (vertically). This corresponds to the CSS 'align-items' property.\",\n                        \"enum\": [\"start\", \"center\", \"end\", \"stretch\"]\n                      }\n                    },\n                    \"required\": [\"children\"]\n                  },\n                  \"Column\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"children\": {\n                        \"type\": \"object\",\n                        \"description\": \"Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.\",\n                        \"properties\": {\n                          \"explicitList\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"template\": {\n                            \"type\": \"object\",\n                            \"description\": \"A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.\",\n                            \"properties\": {\n                              \"componentId\": {\n                                \"type\": \"string\"\n                              },\n                              \"dataBinding\": {\n                                \"type\": \"string\"\n                              }\n                            },\n                            \"required\": [\"componentId\", \"dataBinding\"]\n                          }\n                        }\n                      },\n                      \"distribution\": {\n                        \"type\": \"string\",\n                        \"description\": \"Defines the arrangement of children along the main axis (vertically). This corresponds to the CSS 'justify-content' property.\",\n                        \"enum\": [\n                          \"start\",\n                          \"center\",\n                          \"end\",\n                          \"spaceBetween\",\n                          \"spaceAround\",\n                          \"spaceEvenly\"\n                        ]\n                      },\n                      \"alignment\": {\n                        \"type\": \"string\",\n                        \"description\": \"Defines the alignment of children along the cross axis (horizontally). This corresponds to the CSS 'align-items' property.\",\n                        \"enum\": [\"center\", \"end\", \"start\", \"stretch\"]\n                      }\n                    },\n                    \"required\": [\"children\"]\n                  },\n                  \"List\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"children\": {\n                        \"type\": \"object\",\n                        \"description\": \"Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.\",\n                        \"properties\": {\n                          \"explicitList\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"template\": {\n                            \"type\": \"object\",\n                            \"description\": \"A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.\",\n                            \"properties\": {\n                              \"componentId\": {\n                                \"type\": \"string\"\n                              },\n                              \"dataBinding\": {\n                                \"type\": \"string\"\n                              }\n                            },\n                            \"required\": [\"componentId\", \"dataBinding\"]\n                          }\n                        }\n                      },\n                      \"direction\": {\n                        \"type\": \"string\",\n                        \"description\": \"The direction in which the list items are laid out.\",\n                        \"enum\": [\"vertical\", \"horizontal\"]\n                      },\n                      \"alignment\": {\n                        \"type\": \"string\",\n                        \"description\": \"Defines the alignment of children along the cross axis.\",\n                        \"enum\": [\"start\", \"center\", \"end\", \"stretch\"]\n                      }\n                    },\n                    \"required\": [\"children\"]\n                  },\n                  \"Card\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"child\": {\n                        \"type\": \"string\",\n                        \"description\": \"The ID of the component to be rendered inside the card.\"\n                      }\n                    },\n                    \"required\": [\"child\"]\n                  },\n                  \"Tabs\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"tabItems\": {\n                        \"type\": \"array\",\n                        \"description\": \"An array of objects, where each object defines a tab with a title and a child component.\",\n                        \"items\": {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"title\": {\n                              \"type\": \"object\",\n                              \"description\": \"The tab title. Defines the value as either a literal value or a path to data model value (e.g. '/options/title').\",\n                              \"properties\": {\n                                \"literalString\": {\n                                  \"type\": \"string\"\n                                },\n                                \"path\": {\n                                  \"type\": \"string\"\n                                }\n                              }\n                            },\n                            \"child\": {\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"required\": [\"title\", \"child\"]\n                        }\n                      }\n                    },\n                    \"required\": [\"tabItems\"]\n                  },\n                  \"Divider\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"axis\": {\n                        \"type\": \"string\",\n                        \"description\": \"The orientation of the divider.\",\n                        \"enum\": [\"horizontal\", \"vertical\"]\n                      }\n                    }\n                  },\n                  \"Modal\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"entryPointChild\": {\n                        \"type\": \"string\",\n                        \"description\": \"The ID of the component that opens the modal when interacted with (e.g., a button).\"\n                      },\n                      \"contentChild\": {\n                        \"type\": \"string\",\n                        \"description\": \"The ID of the component to be displayed inside the modal.\"\n                      }\n                    },\n                    \"required\": [\"entryPointChild\", \"contentChild\"]\n                  },\n                  \"Button\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"child\": {\n                        \"type\": \"string\",\n                        \"description\": \"The ID of the component to display in the button, typically a Text component.\"\n                      },\n                      \"primary\": {\n                        \"type\": \"boolean\",\n                        \"description\": \"Indicates if this button should be styled as the primary action.\"\n                      },\n                      \"action\": {\n                        \"type\": \"object\",\n                        \"description\": \"The client-side action to be dispatched when the button is clicked. It includes the action's name and an optional context payload.\",\n                        \"properties\": {\n                          \"name\": {\n                            \"type\": \"string\"\n                          },\n                          \"context\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                              \"type\": \"object\",\n                              \"properties\": {\n                                \"key\": {\n                                  \"type\": \"string\"\n                                },\n                                \"value\": {\n                                  \"type\": \"object\",\n                                  \"description\": \"Defines the value to be included in the context as either a literal value or a path to a data model value (e.g. '/user/name').\",\n                                  \"properties\": {\n                                    \"path\": {\n                                      \"type\": \"string\"\n                                    },\n                                    \"literalString\": {\n                                      \"type\": \"string\"\n                                    },\n                                    \"literalNumber\": {\n                                      \"type\": \"number\"\n                                    },\n                                    \"literalBoolean\": {\n                                      \"type\": \"boolean\"\n                                    }\n                                  }\n                                }\n                              },\n                              \"required\": [\"key\", \"value\"]\n                            }\n                          }\n                        },\n                        \"required\": [\"name\"]\n                      }\n                    },\n                    \"required\": [\"child\", \"action\"]\n                  },\n                  \"CheckBox\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"label\": {\n                        \"type\": \"object\",\n                        \"description\": \"The text to display next to the checkbox. Defines the value as either a literal value or a path to data model ('path', e.g. '/option/label').\",\n                        \"properties\": {\n                          \"literalString\": {\n                            \"type\": \"string\"\n                          },\n                          \"path\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      },\n                      \"value\": {\n                        \"type\": \"object\",\n                        \"description\": \"The current state of the checkbox (true for checked, false for unchecked). This can be a literal boolean ('literalBoolean') or a reference to a value in the data model ('path', e.g. '/filter/open').\",\n                        \"properties\": {\n                          \"literalBoolean\": {\n                            \"type\": \"boolean\"\n                          },\n                          \"path\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    },\n                    \"required\": [\"label\", \"value\"]\n                  },\n                  \"TextField\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"label\": {\n                        \"type\": \"object\",\n                        \"description\": \"The text label for the input field. This can be a literal string or a reference to a value in the data model ('path, e.g. '/user/name').\",\n                        \"properties\": {\n                          \"literalString\": {\n                            \"type\": \"string\"\n                          },\n                          \"path\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      },\n                      \"text\": {\n                        \"type\": \"object\",\n                        \"description\": \"The value of the text field. This can be a literal string or a reference to a value in the data model ('path', e.g. '/user/name').\",\n                        \"properties\": {\n                          \"literalString\": {\n                            \"type\": \"string\"\n                          },\n                          \"path\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      },\n                      \"textFieldType\": {\n                        \"type\": \"string\",\n                        \"description\": \"The type of input field to display.\",\n                        \"enum\": [\n                          \"date\",\n                          \"longText\",\n                          \"number\",\n                          \"shortText\",\n                          \"obscured\"\n                        ]\n                      },\n                      \"validationRegexp\": {\n                        \"type\": \"string\",\n                        \"description\": \"A regular expression used for client-side validation of the input.\"\n                      }\n                    },\n                    \"required\": [\"label\"]\n                  },\n                  \"DateTimeInput\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"value\": {\n                        \"type\": \"object\",\n                        \"description\": \"The selected date and/or time value in ISO 8601 format. This can be a literal string ('literalString') or a reference to a value in the data model ('path', e.g. '/user/dob').\",\n                        \"properties\": {\n                          \"literalString\": {\n                            \"type\": \"string\"\n                          },\n                          \"path\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      },\n                      \"enableDate\": {\n                        \"type\": \"boolean\",\n                        \"description\": \"If true, allows the user to select a date.\"\n                      },\n                      \"enableTime\": {\n                        \"type\": \"boolean\",\n                        \"description\": \"If true, allows the user to select a time.\"\n                      }\n                    },\n                    \"required\": [\"value\"]\n                  },\n                  \"MultipleChoice\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"selections\": {\n                        \"type\": \"object\",\n                        \"description\": \"The currently selected values for the component. This can be a literal array of strings or a path to an array in the data model('path', e.g. '/hotel/options').\",\n                        \"properties\": {\n                          \"literalArray\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"path\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      },\n                      \"options\": {\n                        \"type\": \"array\",\n                        \"description\": \"An array of available options for the user to choose from.\",\n                        \"items\": {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"label\": {\n                              \"type\": \"object\",\n                              \"description\": \"The text to display for this option. This can be a literal string or a reference to a value in the data model (e.g. '/option/label').\",\n                              \"properties\": {\n                                \"literalString\": {\n                                  \"type\": \"string\"\n                                },\n                                \"path\": {\n                                  \"type\": \"string\"\n                                }\n                              }\n                            },\n                            \"value\": {\n                              \"type\": \"string\",\n                              \"description\": \"The value to be associated with this option when selected.\"\n                            }\n                          },\n                          \"required\": [\"label\", \"value\"]\n                        }\n                      },\n                      \"maxAllowedSelections\": {\n                        \"type\": \"integer\",\n                        \"description\": \"The maximum number of options that the user is allowed to select.\"\n                      }\n                    },\n                    \"required\": [\"selections\", \"options\"]\n                  },\n                  \"Slider\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"value\": {\n                        \"type\": \"object\",\n                        \"description\": \"The current value of the slider. This can be a literal number ('literalNumber') or a reference to a value in the data model ('path', e.g. '/restaurant/cost').\",\n                        \"properties\": {\n                          \"literalNumber\": {\n                            \"type\": \"number\"\n                          },\n                          \"path\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      },\n                      \"minValue\": {\n                        \"type\": \"number\",\n                        \"description\": \"The minimum value of the slider.\"\n                      },\n                      \"maxValue\": {\n                        \"type\": \"number\",\n                        \"description\": \"The maximum value of the slider.\"\n                      }\n                    },\n                    \"required\": [\"value\"]\n                  }\n                }\n              }\n            },\n            \"required\": [\"id\", \"component\"]\n          }\n        }\n      },\n      \"required\": [\"surfaceId\", \"components\"]\n    },\n    \"dataModelUpdate\": {\n      \"type\": \"object\",\n      \"description\": \"Updates the data model for a surface.\",\n      \"properties\": {\n        \"surfaceId\": {\n          \"type\": \"string\",\n          \"description\": \"The unique identifier for the UI surface this data model update applies to.\"\n        },\n        \"path\": {\n          \"type\": \"string\",\n          \"description\": \"An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', the entire data model will be replaced.\"\n        },\n        \"contents\": {\n          \"type\": \"array\",\n          \"description\": \"An array of data entries. Each entry must contain a 'key' and exactly one corresponding typed 'value*' property.\",\n          \"items\": {\n            \"type\": \"object\",\n            \"description\": \"A single data entry. Exactly one 'value*' property should be provided alongside the key.\",\n            \"properties\": {\n              \"key\": {\n                \"type\": \"string\",\n                \"description\": \"The key for this data entry.\"\n              },\n              \"valueString\": {\n                \"type\": \"string\"\n              },\n              \"valueNumber\": {\n                \"type\": \"number\"\n              },\n              \"valueBoolean\": {\n                \"type\": \"boolean\"\n              },\n              \"valueMap\": {\n                \"description\": \"Represents a map as an adjacency list.\",\n                \"type\": \"array\",\n                \"items\": {\n                  \"type\": \"object\",\n                  \"description\": \"One entry in the map. Exactly one 'value*' property should be provided alongside the key.\",\n                  \"properties\": {\n                    \"key\": {\n                      \"type\": \"string\"\n                    },\n                    \"valueString\": {\n                      \"type\": \"string\"\n                    },\n                    \"valueNumber\": {\n                      \"type\": \"number\"\n                    },\n                    \"valueBoolean\": {\n                      \"type\": \"boolean\"\n                    }\n                  },\n                  \"required\": [\"key\"]\n                }\n              }\n            },\n            \"required\": [\"key\"]\n          }\n        }\n      },\n      \"required\": [\"contents\", \"surfaceId\"]\n    },\n    \"deleteSurface\": {\n      \"type\": \"object\",\n      \"description\": \"Signals the client to delete the surface identified by 'surfaceId'.\",\n      \"properties\": {\n        \"surfaceId\": {\n          \"type\": \"string\",\n          \"description\": \"The unique identifier for the UI surface to be deleted.\"\n        }\n      },\n      \"required\": [\"surfaceId\"]\n    }\n  }\n}\n\"\"\"\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/skills/A2UI_response_generator/view_a2ui_examples.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n\"\"\"\nA2UI Example Viewer - Tool for viewing A2UI UI template examples.\n\nThis script provides a way to retrieve A2UI UI template examples.\n\nUsage:\n    # Load specific template name\n    python view_a2ui_examples.py --template_name SINGLE_COLUMN_LIST_WITH_IMAGE\n\"\"\"\nfrom UI_templete_examples import (\n    SINGLE_COLUMN_LIST_WITH_IMAGE_EXAMPLE,\n    TWO_COLUMN_LIST_WITH_IMAGE_EXAMPLE,\n    SIMPLE_LIST_EXAMPLE,\n    BOOKING_FORM_WITH_IMAGE,\n    SEARCH_FILTER_FORM_EXAMPLE,\n    CONTACT_FORM_EXAMPLE,\n    EMAIL_COMPOSE_FORM_EXAMPLE,\n    SUCCESS_CONFIRMATION_WITH_IMAGE_EXAMPLE,\n    ERROR_MESSAGE_EXAMPLE,\n    INFO_MESSAGE_EXAMPLE,\n    ITEM_DETAIL_CARD_EXAMPLE_WITH_IMAGE,\n    PROFILE_VIEW_WITH_IMAGE_EXAMPLE,\n    SELECTION_CARD_EXAMPLE,\n    MULTIPLE_SELECTION_CARDS_EXAMPLE,\n)\n\n# Template name to example mapping\nTEMPLATE_MAP = {\n    \"SINGLE_COLUMN_LIST_WITH_IMAGE\": SINGLE_COLUMN_LIST_WITH_IMAGE_EXAMPLE,\n    \"TWO_COLUMN_LIST_WITH_IMAGE\": TWO_COLUMN_LIST_WITH_IMAGE_EXAMPLE,\n    \"SIMPLE_LIST\": SIMPLE_LIST_EXAMPLE,\n    \"BOOKING_FORM_WITH_IMAGE\": BOOKING_FORM_WITH_IMAGE,\n    \"SEARCH_FILTER_FORM_WITH_IMAGE\": SEARCH_FILTER_FORM_EXAMPLE,\n    \"CONTACT_FORM_WITH_IMAGE\": CONTACT_FORM_EXAMPLE,\n    \"EMAIL_COMPOSE_FORM_WITH_IMAGE\": EMAIL_COMPOSE_FORM_EXAMPLE,\n    \"SUCCESS_CONFIRMATION_WITH_IMAGE\": SUCCESS_CONFIRMATION_WITH_IMAGE_EXAMPLE,\n    \"ERROR_MESSAGE\": ERROR_MESSAGE_EXAMPLE,\n    \"INFO_MESSAGE\": INFO_MESSAGE_EXAMPLE,\n    \"ITEM_DETAIL_CARD\": ITEM_DETAIL_CARD_EXAMPLE_WITH_IMAGE,\n    \"ITEM_DETAIL_CARD_WITH_IMAGE\": ITEM_DETAIL_CARD_EXAMPLE_WITH_IMAGE,\n    \"PROFILE_VIEW\": PROFILE_VIEW_WITH_IMAGE_EXAMPLE,\n    \"SELECTION_CARD\": SELECTION_CARD_EXAMPLE,\n    \"MULTIPLE_SELECTION_CARDS\": MULTIPLE_SELECTION_CARDS_EXAMPLE,\n}\n\n\ndef view_a2ui_examples(template_name: str) -> str:\n    \"\"\"\n    View A2UI UI template examples for generating UI responses.\n\n    Args:\n        template_name: Specific template name to load. Options:\n                       - SINGLE_COLUMN_LIST_WITH_IMAGE, TWO_COLUMN_LIST_WITH_IMAGE,\n                       SIMPLE_LIST,SELECTION_CARD,\n                       MULTIPLE_SELECTION_CARDS,\n                       BOOKING_FORM_WITH_IMAGE, SEARCH_FILTER_FORM_WITH_IMAGE,\n                       CONTACT_FORM_WITH_IMAGE,\n                       EMAIL_COMPOSE_FORM_WITH_IMAGE,SUCCESS_CONFIRMATION_WITH_IMAGE,\n                       ERROR_MESSAGE, INFO_MESSAGE,\n                       ITEM_DETAIL_CARD,\n                       ITEM_DETAIL_CARD_WITH_IMAGE,\n                       PROFILE_VIEW\n\n    Returns:\n        The requested template example.\n\n    Examples:\n        # Load specific template\n        >>> view_a2ui_examples(template_name=\"BOOKING_FORM_WITH_IMAGE\")\n        >>> view_a2ui_examples(template_name=\"SINGLE_COLUMN_LIST_WITH_IMAGE\")\n    \"\"\"\n    if not template_name:\n        raise ValueError(\"template_name is required and cannot be empty\")\n\n    if template_name not in TEMPLATE_MAP:\n        raise ValueError(f\"Unknown template name: {template_name}\")\n\n    example = TEMPLATE_MAP[template_name]\n\n    return f\"\"\"\n## A2UI Template: {template_name}\n\n{example}\n\n---\nAdapt this template to your specific data and styling requirements.\n\"\"\"\n\n\n# Tool metadata for AgentScope registration\nTOOL_METADATA = {\n    \"name\": \"view_a2ui_examples\",\n    \"description\": \"View A2UI UI template examples for generating UI responses.\",\n    \"parameters\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"template_name\": {\n                \"type\": \"string\",\n                \"description\": \"Specific template name to load\",\n            },\n        },\n        \"required\": [\"template_name\"],\n    },\n}\n\n\nif __name__ == \"__main__\":\n    import argparse\n\n    parser = argparse.ArgumentParser(\n        description=\"View A2UI UI template examples for generating UI responses.\",\n    )\n    parser.add_argument(\n        \"--template_name\",\n        type=str,\n        required=True,\n        help=\"Specific template name to load\",\n    )\n\n    args = parser.parse_args()\n\n    res = view_a2ui_examples(template_name=args.template_name)\n    print(res)\n"
  },
  {
    "path": "examples/agent/a2ui_agent/samples/general_agent/skills/A2UI_response_generator/view_a2ui_schema.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n\"\"\"\nA2UI Schema Viewer - Tool for viewing A2UI schema.\n\nThis script provides a way to retrieve the complete A2UI schema for\ngenerating UI responses.\n\nUsage:\n    python view_a2ui_schema.py\n\"\"\"\n\nfrom schema import A2UI_SCHEMA\n\n\ndef view_a2ui_schema(schema_category: str = \"BASE_SCHEMA\") -> str:\n    \"\"\"\n    View the complete A2UI schema for generating UI responses.\n\n    This tool returns the complete A2UI JSON schema that defines all\n    available UI components and message types.\n\n    Args:\n        schema_category: The category of the schema to view. Can be \"BASE_SCHEMA\".\n\n    Returns:\n        The complete A2UI schema as a string.\n    \"\"\"\n    if schema_category == \"BASE_SCHEMA\":\n        return f\"\"\"\n## A2UI JSON Schema\n\nThe following is the complete A2UI schema for generating UI responses:\n\n{A2UI_SCHEMA}\n\n---\nUse this schema to construct valid A2UI JSON responses.\n\"\"\"\n    else:\n        raise ValueError(f\"Invalid schema category: {schema_category}\")\n\n\n# Tool metadata for AgentScope registration\nTOOL_METADATA = {\n    \"name\": \"view_a2ui_schema\",\n    \"description\": \"View the complete A2UI schema for generating UI responses.\",\n    \"parameters\": {\n        \"type\": \"object\",\n        \"properties\": {},\n    },\n}\n\n\nif __name__ == \"__main__\":\n    import argparse\n\n    parser = argparse.ArgumentParser(\n        description=\"View the complete A2UI schema for generating UI responses.\",\n    )\n    parser.add_argument(\n        \"--schema_category\",\n        type=str,\n        required=True,\n        help=\"The category of the schema to view. Can be 'BASE_SCHEMA'.\",\n        choices=[\"BASE_SCHEMA\"],\n        default=\"BASE_SCHEMA\",\n    )\n    args = parser.parse_args()\n\n    res = view_a2ui_schema(schema_category=args.schema_category)\n    print(res)\n"
  },
  {
    "path": "examples/agent/browser_agent/README.md",
    "content": "# Browser Agent Example\n\nThis example demonstrates how to use AgentScope's BrowserAgent for web automation tasks. The BrowserAgent leverages the Model Context Protocol (MCP) to interact with browser tools powered by Playwright, enabling sophisticated web navigation, data extraction, and automation.\n\n\n## Prerequisites\n\n- Python 3.10 or higher\n- Node.js and npm (for the MCP server)\n- DashScope API key from Alibaba Cloud\n\n## Installation\n\n### Install AgentScope\n\n```bash\n# Install from source\ncd {PATH_TO_AGENTSCOPE}\npip install -e .\n```\n\n## Setup\n\n### 1. Environment Configuration\n\nSet up your DashScope API key:\n\n```bash\nexport DASHSCOPE_API_KEY=\"your_dashscope_api_key_here\"\n```\n\nYou can obtain a DashScope API key from [Alibaba Cloud DashScope Console](https://dashscope.console.aliyun.com/).\n\n### 2. About PlayWright MCP Server\n\nBefore running the browser agent, you can test whether you can start the Playwright MCP server:\n\n```bash\nnpx @playwright/mcp@latest\n```\n\n## Usage\n\n### Basic Example\nYou can start running the browser agent in your terminal with the following command\n```bash\ncd examples/agent/browser_agent\npython main.py\n```\n"
  },
  {
    "path": "examples/agent/browser_agent/browser_agent.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"Browser Agent\"\"\"\n# flake8: noqa: E501\n# pylint: disable=W0212,too-many-lines,C0301,W0107,C0411\n\nimport re\nimport uuid\nimport os\nimport json\nimport inspect\nfrom functools import wraps\nfrom typing import Type, Optional, Any, Literal\nimport asyncio\nimport copy\n\nfrom pydantic import BaseModel\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope._logging import logger\nfrom agentscope.formatter import FormatterBase\nfrom agentscope.memory import MemoryBase\nfrom agentscope.message import (\n    Msg,\n    ToolUseBlock,\n    TextBlock,\n    ToolResultBlock,\n    ImageBlock,\n    Base64Source,\n)\nfrom agentscope.model import ChatModelBase\nfrom agentscope.tool import (\n    Toolkit,\n    ToolResponse,\n)\nfrom agentscope.token import TokenCounterBase, OpenAITokenCounter\n\nfrom build_in_helper._image_understanding import image_understanding\nfrom build_in_helper._video_understanding import video_understanding\nfrom build_in_helper._file_download import file_download\nfrom build_in_helper._form_filling import form_filling\n\n_CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))\n_PROMPT_DIR = os.path.join(_CURRENT_DIR, \"build_in_prompt\")\n_HELPER_DIR = os.path.join(_CURRENT_DIR, \"build_in_helper\")\n\n\nclass EmptyModel(BaseModel):\n    \"\"\"Empty structured model for default structured output requirement.\"\"\"\n\n    pass\n\n\nwith open(\n    os.path.join(_PROMPT_DIR, \"browser_agent_sys_prompt.md\"),\n    \"r\",\n    encoding=\"utf-8\",\n) as f:\n    _BROWSER_AGENT_DEFAULT_SYS_PROMPT = f.read()\n\nwith open(\n    os.path.join(_PROMPT_DIR, \"browser_agent_pure_reasoning_prompt.md\"),\n    \"r\",\n    encoding=\"utf-8\",\n) as f:\n    _BROWSER_AGENT_DEFAULT_PURE_REASONING_PROMPT = f.read()\n\nwith open(\n    os.path.join(_PROMPT_DIR, \"browser_agent_observe_reasoning_prompt.md\"),\n    \"r\",\n    encoding=\"utf-8\",\n) as f:\n    _BROWSER_AGENT_DEFAULT_OBSERVE_REASONING_PROMPT = f.read()\n\nwith open(\n    os.path.join(_PROMPT_DIR, \"browser_agent_task_decomposition_prompt.md\"),\n    \"r\",\n    encoding=\"utf-8\",\n) as f:\n    _BROWSER_AGENT_DEFAULT_TASK_DECOMPOSITION_PROMPT = f.read()\n\nwith open(\n    os.path.join(_PROMPT_DIR, \"browser_agent_summarize_task.md\"),\n    \"r\",\n    encoding=\"utf-8\",\n) as f:\n    _BROWSER_AGENT_SUMMARIZE_TASK_PROMPT = f.read()\n\n\nclass BrowserAgent(ReActAgent):\n    \"\"\"\n    Browser Agent that extends ReActAgent with browser-specific capabilities.\n\n    The agent leverages MCP servers to access browser tools with Playwright,\n    enabling sophisticated web automation tasks.\n    \"\"\"\n\n    def __init__(\n        self,\n        name: str,\n        model: ChatModelBase,\n        formatter: FormatterBase,\n        memory: MemoryBase,\n        toolkit: Toolkit,\n        sys_prompt: str = _BROWSER_AGENT_DEFAULT_SYS_PROMPT,\n        max_iters: int = 50,\n        start_url: Optional[str] = \"https://www.google.com\",\n        pure_reasoning_prompt: str = _BROWSER_AGENT_DEFAULT_PURE_REASONING_PROMPT,\n        observe_reasoning_prompt: str = _BROWSER_AGENT_DEFAULT_OBSERVE_REASONING_PROMPT,\n        task_decomposition_prompt: str = _BROWSER_AGENT_DEFAULT_TASK_DECOMPOSITION_PROMPT,\n        token_counter: TokenCounterBase = OpenAITokenCounter(\"gpt-4o\"),\n        max_mem_length: int = 20,\n    ) -> None:\n        \"\"\"Initialize the Browser Agent.\"\"\"\n        self.start_url = start_url\n        self._has_initial_navigated = False\n        self.pure_reasoning_prompt = pure_reasoning_prompt\n        self.observe_reasoning_prompt = observe_reasoning_prompt\n        self.task_decomposition_prompt = task_decomposition_prompt\n        self.max_memory_length = max_mem_length\n        self.token_estimator = token_counter\n        self.snapshot_chunk_id = 0\n        self.chunk_continue_status = False\n        self.previous_chunkwise_information = \"\"\n        self.snapshot_in_chunk: list[str] = []\n        self.subtasks: list[Any] = []\n        self.original_task = \"\"\n        self.current_subtask_idx = 0\n        self.current_subtask: Any = None\n        self.iter_n = 0\n        self.finish_function_name = \"browser_generate_final_response\"\n        self.init_query = \"\"\n        self._required_structured_model: Type[BaseModel] | None = None\n\n        super().__init__(\n            name=name,\n            sys_prompt=sys_prompt,\n            model=model,\n            formatter=formatter,\n            memory=memory,\n            toolkit=toolkit,\n            max_iters=max_iters,\n        )\n\n        # Register tools\n        self.toolkit.register_tool_function(self.browser_subtask_manager)\n\n        # Register skill tools if model supports multimodal\n        if self._supports_multimodal():\n            self._register_skill_tool(image_understanding)\n            self._register_skill_tool(video_understanding)\n\n        # Register other skill tools\n        self._register_skill_tool(file_download)\n        self._register_skill_tool(form_filling)\n\n        # Build a tool list without screenshot to avoid unnecessary captures\n        self.no_screenshot_tool_list = [\n            tool\n            for tool in self.toolkit.get_json_schemas()\n            if tool.get(\"function\", {}).get(\"name\")\n            != \"browser_take_screenshot\"\n        ]\n\n    async def reply(  # pylint: disable=R0912,R0915\n        self,\n        msg: Msg | list[Msg] | None = None,\n        structured_model: Type[BaseModel] | None = None,\n    ) -> Msg:\n        \"\"\"Process a message and return a response.\"\"\"\n        self.init_query = (\n            msg.content\n            if isinstance(msg, Msg)\n            else msg[0].content\n            if isinstance(msg, list)\n            else \"\"\n        )\n\n        if self.start_url and not self._has_initial_navigated:\n            await self._navigate_to_start_url()\n            self._has_initial_navigated = True\n\n        msg = await self._task_decomposition_and_reformat(msg)\n\n        await self.memory.add(msg)\n        # Default to EmptyModel to require structured output if none provided\n        if structured_model is None:\n            structured_model = EmptyModel\n\n        tool_choice: Literal[\"auto\", \"none\", \"required\"] | None = None\n        self._required_structured_model = structured_model\n\n        # Register finish tool only when structured model is required\n        if structured_model:\n            if self.finish_function_name not in self.toolkit.tools:\n                self.toolkit.register_tool_function(\n                    getattr(self, self.finish_function_name),\n                )\n            self.toolkit.set_extended_model(\n                self.finish_function_name,\n                structured_model,\n            )\n            tool_choice = \"required\"\n        else:\n            self.toolkit.remove_tool_function(self.finish_function_name)\n\n        # The reasoning-acting loop\n        structured_output = None\n        reply_msg = None\n        for iter_n in range(self.max_iters):\n            self.iter_n = iter_n + 1\n            await self._summarize_mem()\n            msg_reasoning = await self._pure_reasoning(tool_choice)\n\n            tool_calls = msg_reasoning.get_content_blocks(\"tool_use\")\n            if tool_calls and tool_calls[0][\"name\"] == \"browser_snapshot\":\n                msg_reasoning = await self._reasoning_with_observation()\n\n            futures = [\n                self._acting(tool_call)\n                for tool_call in msg_reasoning.get_content_blocks(\"tool_use\")\n            ]\n\n            # Parallel tool calls or not\n            if self.parallel_tool_calls:\n                structured_outputs = await asyncio.gather(*futures)\n            else:\n                structured_outputs = [await _ for _ in futures]\n\n            # Check for exit condition\n            # If structured output is still not satisfied\n            if self._required_structured_model:\n                # Remove None results\n                structured_outputs = [_ for _ in structured_outputs if _]\n\n                msg_hint = None\n                # If the acting step generates structured outputs\n                if structured_outputs:\n                    # Cache the structured output data\n                    structured_output = structured_outputs[-1]\n\n                    reply_msg = Msg(\n                        self.name,\n                        structured_output.get(\"subtask_progress_summary\", \"\"),\n                        \"assistant\",\n                        metadata=structured_output,\n                    )\n                    break\n\n                if not msg_reasoning.has_content_blocks(\"tool_use\"):\n                    # If structured output is required but no tool call is\n                    # made, require tool call in the next reasoning step\n                    msg_hint = Msg(\n                        \"user\",\n                        \"<system-hint>Structured output is \"\n                        f\"required, go on to finish your task or call \"\n                        f\"'{self.finish_function_name}' to generate the \"\n                        f\"required structured output.</system-hint>\",\n                        \"user\",\n                    )\n                    tool_choice = \"required\"\n\n                if msg_hint:\n                    await self.memory.add(msg_hint)\n                    await self.print(msg_hint)\n\n            elif not msg_reasoning.has_content_blocks(\"tool_use\"):\n                # Exit the loop when no structured output is required (or\n                # already satisfied) and only text response is generated\n                msg_reasoning.metadata = structured_output\n                reply_msg = msg_reasoning\n                break\n\n        # When the maximum iterations are reached\n        # and no reply message is generated\n        if reply_msg is None:\n            reply_msg = await self._summarizing()\n            reply_msg.metadata = structured_output\n            await self.memory.add(reply_msg)\n\n        return reply_msg\n\n    async def _pure_reasoning(\n        self,\n        tool_choice: Literal[\"auto\", \"none\", \"required\"] | None = None,\n    ) -> Msg:\n        \"\"\"Initial reasoning without screenshot observation.\"\"\"\n        msg = Msg(\n            \"user\",\n            content=self.pure_reasoning_prompt.format(\n                current_subtask=self.current_subtask,\n                init_query=self.original_task,\n            ),\n            role=\"user\",\n        )\n        prompt = await self.formatter.format(\n            msgs=[\n                Msg(\"system\", self.sys_prompt, \"system\"),\n                *await self.memory.get_memory(),\n                msg,\n            ],\n        )\n\n        res = await self.model(\n            prompt,\n            tools=self.no_screenshot_tool_list,\n            tool_choice=tool_choice,\n        )\n\n        interrupted_by_user = False\n        msg = None\n        try:\n            if self.model.stream:\n                msg = Msg(self.name, [], \"assistant\")\n                async for content_chunk in res:\n                    msg.content = content_chunk.content\n                await self.print(msg)\n            else:\n                msg = Msg(self.name, list(res.content), \"assistant\")\n                await self.print(msg)\n            return msg\n        except asyncio.CancelledError as e:\n            interrupted_by_user = True\n            raise e from None\n        finally:\n            await self.memory.add(msg)\n            tool_use_blocks: list = msg.get_content_blocks(\"tool_use\")  # type: ignore\n            if interrupted_by_user and msg:\n                for tool_call in tool_use_blocks:  # pylint: disable=E1133\n                    msg_res = Msg(\n                        \"system\",\n                        [\n                            ToolResultBlock(\n                                type=\"tool_result\",\n                                id=tool_call[\"id\"],\n                                name=tool_call[\"name\"],\n                                output=\"The tool call has been interrupted by the user.\",\n                            ),\n                        ],\n                        \"system\",\n                    )\n                    await self.memory.add(msg_res)\n                    await self.print(msg_res)\n\n    async def _reasoning_with_observation(self) -> Msg:\n        \"\"\"Perform the reasoning process with page observation in chunks.\"\"\"\n        self.snapshot_chunk_id = 0\n        self.chunk_continue_status = False\n        self.previous_chunkwise_information = \"\"\n        self.snapshot_in_chunk = []\n\n        mem = await self.memory.get_memory()\n        if mem:\n            await self.memory.delete([mem[-1].id])\n\n        self.snapshot_in_chunk = await self._get_snapshot_in_text()\n        for _ in self.snapshot_in_chunk:\n            observe_msg = await self._build_observation()\n            prompt = await self.formatter.format(\n                msgs=[\n                    Msg(\"system\", self.sys_prompt, \"system\"),\n                    *await self.memory.get_memory(),\n                    observe_msg,\n                ],\n            )\n            res = await self.model(\n                prompt,\n                tools=self.no_screenshot_tool_list,\n            )\n\n            interrupted_by_user = False\n            msg = None\n            try:\n                if self.model.stream:\n                    msg = Msg(self.name, [], \"assistant\")\n                    async for content_chunk in res:\n                        msg.content = content_chunk.content\n                    # await self.print(msg)\n                else:\n                    msg = Msg(self.name, list(res.content), \"assistant\")\n                    # await self.print(msg)\n                logger.info(msg.content)\n            except asyncio.CancelledError as e:\n                interrupted_by_user = True\n                raise e from None\n\n            tool_use_blocks: list = msg.get_content_blocks(\"tool_use\")  # type: ignore\n            await self._update_chunk_observation_status(output_msg=msg)\n\n            if interrupted_by_user and msg:\n                for tool_call in tool_use_blocks:  # pylint: disable=E1133\n                    msg_res = Msg(\n                        \"system\",\n                        [\n                            ToolResultBlock(\n                                type=\"tool_result\",\n                                id=tool_call[\"id\"],\n                                name=tool_call[\"name\"],\n                                output=\"The tool call has been interrupted by the user.\",\n                            ),\n                        ],\n                        \"system\",\n                    )\n                    await self.memory.add(msg_res)\n                    await self.print(msg_res)\n\n            if not self.chunk_continue_status:\n                break\n\n        await self.memory.add(msg)\n        return msg\n\n    async def _summarize_mem(self) -> None:\n        \"\"\"Summarize memory if too long.\"\"\"\n        mem_len = await self.memory.size()\n        if mem_len > self.max_memory_length:\n            await self._memory_summarizing()\n\n    async def _build_observation(self) -> Msg:\n        \"\"\"Get a snapshot (and optional screenshot) before reasoning.\"\"\"\n        image_data: Optional[str] = None\n        if self._supports_multimodal():\n            image_data = await self._get_screenshot()\n        observe_msg = self.observe_by_chunk(image_data)\n        return observe_msg\n\n    async def _update_chunk_observation_status(\n        self,\n        output_msg: Msg | None = None,\n    ) -> None:\n        \"\"\"Update the chunk observation status after reasoning.\"\"\"\n        for _, b in enumerate(output_msg.content):\n            if b[\"type\"] == \"text\":\n                raw_response = b[\"text\"]\n                try:\n                    if \"```json\" in raw_response:\n                        raw_response = raw_response.replace(\n                            \"```json\",\n                            \"\",\n                        ).replace(\"```\", \"\")\n                    data = json.loads(raw_response)\n                    information = data.get(\"INFORMATION\", \"\")\n                    # Continue unless STATUS is explicitly REASONING_FINISHED\n                    self.chunk_continue_status = (\n                        data.get(\"STATUS\") != \"REASONING_FINISHED\"\n                    )\n                except Exception:\n                    information = raw_response\n                    if (\n                        self.snapshot_chunk_id\n                        < len(self.snapshot_in_chunk) - 1\n                    ):\n                        self.chunk_continue_status = True\n                        self.snapshot_chunk_id += 1\n                    else:\n                        self.chunk_continue_status = False\n                if not isinstance(information, str):\n                    try:\n                        information = json.dumps(\n                            information,\n                            ensure_ascii=False,\n                        )\n                    except Exception:\n                        information = str(information)\n                self.previous_chunkwise_information += (\n                    f\"Information in chunk {self.snapshot_chunk_id+1} of {len(self.snapshot_in_chunk)}:\\n\"\n                    + information\n                    + \"\\n\"\n                )\n            if b[\"type\"] == \"tool_use\":\n                self.chunk_continue_status = False\n\n    async def _acting(self, tool_call: ToolUseBlock) -> dict | None:\n        \"\"\"Perform the acting process and return structured output if generated.\"\"\"\n        tool_res_msg = Msg(\n            \"system\",\n            [\n                ToolResultBlock(\n                    type=\"tool_result\",\n                    id=tool_call[\"id\"],\n                    name=tool_call[\"name\"],\n                    output=[],\n                ),\n            ],\n            \"system\",\n        )\n        try:\n            tool_res = await self.toolkit.call_tool_function(tool_call)\n            structured_output = None\n            async for chunk in tool_res:\n                tool_res_msg.content[0][\"output\"] = chunk.content  # type: ignore[index]\n                await self.print(tool_res_msg, chunk.is_last)\n\n                # Raise the CancelledError to handle the interruption\n                if chunk.is_interrupted:\n                    raise asyncio.CancelledError()\n\n                # Return structured output if generate_response is called successfully\n                if (\n                    tool_call[\"name\"] == self.finish_function_name\n                    and chunk.metadata\n                    and chunk.metadata.get(\"success\", False)\n                ):\n                    # Only return the structured output\n                    structured_output = chunk.metadata.get(\"structured_output\")\n                    return structured_output\n\n            return None\n        finally:\n            tool_res_msg = self._clean_tool_excution_content(tool_res_msg)\n            # Always add tool result to maintain message sequence integrity\n            # DashScope requires every tool_call to have a corresponding tool_result\n            # Don't delete assistant messages to avoid breaking message sequence\n            await self.memory.add(tool_res_msg)\n\n    def _clean_tool_excution_content(self, output_msg: Msg) -> Msg:\n        \"\"\"Clean verbose tool outputs before printing and storing.\"\"\"\n        for i, b in enumerate(output_msg.content):\n            if b[\"type\"] == \"tool_result\":\n                for j, return_json in enumerate(b.get(\"output\", [])):\n                    if isinstance(return_json, dict) and \"text\" in return_json:\n                        output_msg.content[i][\"output\"][j][\"text\"] = self._filter_execution_text(  # type: ignore[index]\n                            return_json[\"text\"],\n                        )\n        return output_msg\n\n    async def _task_decomposition_and_reformat(\n        self,\n        original_task: Msg | list[Msg] | None,\n    ) -> Msg:\n        \"\"\"Decompose the original task into smaller tasks and reformat.\"\"\"\n        if isinstance(original_task, list):\n            original_task = original_task[0]\n\n        prompt = await self.formatter.format(\n            msgs=[\n                Msg(\n                    name=\"user\",\n                    content=self.task_decomposition_prompt.format(\n                        start_url=self.start_url,\n                        browser_agent_sys_prompt=self.sys_prompt,\n                        original_task=original_task.content,\n                    ),\n                    role=\"user\",\n                ),\n            ],\n        )\n        res = await self.model(prompt)\n        decompose_text = \"\"\n        if self.model.stream:\n            async for content_chunk in res:\n                decompose_text = content_chunk.content[0][\"text\"]\n        else:\n            decompose_text = res.content[0][\"text\"]\n        logger.info(decompose_text)\n\n        reflection_prompt_path = os.path.join(\n            _PROMPT_DIR,\n            \"browser_agent_decompose_reflection_prompt.md\",\n        )\n        with open(reflection_prompt_path, \"r\", encoding=\"utf-8\") as fj:\n            decompose_reflection_prompt = fj.read()\n\n        reflection_prompt = await self.formatter.format(\n            msgs=[\n                Msg(\n                    name=\"user\",\n                    content=self.task_decomposition_prompt.format(\n                        start_url=self.start_url,\n                        browser_agent_sys_prompt=self.sys_prompt,\n                        original_task=original_task.content,\n                    ),\n                    role=\"user\",\n                ),\n                Msg(\n                    name=\"system\",\n                    content=decompose_text,\n                    role=\"system\",\n                ),\n                Msg(\n                    name=\"user\",\n                    content=decompose_reflection_prompt.format(\n                        original_task=original_task.content,\n                        subtasks=decompose_text,\n                    ),\n                    role=\"user\",\n                ),\n            ],\n        )\n        reflection_res = await self.model(reflection_prompt)\n        reflection_text = \"\"\n        if self.model.stream:\n            async for content_chunk in reflection_res:\n                reflection_text = content_chunk.content[0][\"text\"]\n        else:\n            reflection_text = reflection_res.content[0][\"text\"]\n        logger.info(reflection_text)\n\n        subtasks: list[Any] = []\n        try:\n            if \"```json\" in reflection_text:\n                reflection_text = reflection_text.replace(\n                    \"```json\",\n                    \"\",\n                ).replace(\"```\", \"\")\n            subtasks_json = json.loads(reflection_text)\n            subtasks = subtasks_json.get(\"REVISED_SUBTASKS\", [])\n            if not isinstance(subtasks, list):\n                subtasks = []\n        except Exception:\n            subtasks = [original_task.content]\n\n        self.subtasks = subtasks\n        self.current_subtask_idx = 0\n        self.current_subtask = self.subtasks[0] if self.subtasks else None\n        # Prefer text content extraction if available\n        try:\n            self.original_task = original_task.get_text_content()\n        except Exception:\n            self.original_task = original_task.content\n\n        formatted_task = \"The original task is: \" + self.original_task + \"\\n\"\n        try:\n            formatted_task += (\n                \"The decomposed subtasks are: \"\n                + json.dumps(self.subtasks)\n                + \"\\n\"\n            )\n            formatted_task += (\n                \"use the decomposed subtasks to complete the original task.\\n\"\n            )\n        except Exception:\n            pass\n        formatted_task = Msg(\n            name=original_task.name,\n            content=formatted_task,\n            role=original_task.role,\n        )\n        logger.info(  # pylint: disable=W1203\n            f\"The formatted task is: \\n{formatted_task.content}\",\n        )\n        return formatted_task\n\n    async def _navigate_to_start_url(self) -> None:\n        \"\"\"Navigate to the start URL and clean up extra tabs.\"\"\"\n        tool_call = ToolUseBlock(\n            id=str(uuid.uuid4()),\n            name=\"browser_tabs\",\n            input={\"action\": \"list\"},\n            type=\"tool_use\",\n        )\n        response = await self.toolkit.call_tool_function(tool_call)\n        response_text = \"\"\n        async for chunk in response:\n            # chunk.content might be a list[TextBlock]\n            if chunk.content and \"text\" in chunk.content[0]:\n                response_text = chunk.content[0][\"text\"]\n        tab_numbers = re.findall(r\"- (\\d+):\", response_text)\n        for _ in tab_numbers[1:]:\n            tool_call = ToolUseBlock(\n                id=str(uuid.uuid4()),\n                name=\"browser_tabs\",\n                input={\"action\": \"close\", \"index\": 0},\n                type=\"tool_use\",\n            )\n            await self.toolkit.call_tool_function(tool_call)\n\n        tool_call = ToolUseBlock(\n            id=str(uuid.uuid4()),\n            type=\"tool_use\",\n            name=\"browser_navigate\",\n            input={\"url\": self.start_url},\n        )\n        await self.toolkit.call_tool_function(tool_call)\n\n    async def _get_snapshot_in_text(self) -> list[str]:\n        \"\"\"Capture a text-based snapshot of the current webpage content.\"\"\"\n        snapshot_tool_call = ToolUseBlock(\n            type=\"tool_use\",\n            id=str(uuid.uuid4()),\n            name=\"browser_snapshot\",\n            input={},\n        )\n        snapshot_response = await self.toolkit.call_tool_function(\n            snapshot_tool_call,\n        )\n        snapshot_str = \"\"\n        async for chunk in snapshot_response:\n            snapshot_str = chunk.content[0][\"text\"]\n        snapshot_in_chunk = self._split_snapshot_by_chunk(snapshot_str)\n        return snapshot_in_chunk\n\n    async def _memory_summarizing(self) -> None:\n        \"\"\"Summarize the current memory content to prevent context overflow.\"\"\"\n        initial_question = None\n        memory_msgs = await self.memory.get_memory()\n        for msg in memory_msgs:\n            if msg.role == \"user\":\n                initial_question = msg.content\n                break\n\n        hint_msg = Msg(\n            \"user\",\n            (\n                \"Summarize the current progress and outline the next steps for this task. \"\n                \"Your summary should include:\\n\"\n                \"1. What has been completed so far.\\n\"\n                \"2. What key information has been found.\\n\"\n                \"3. What remains to be done.\\n\"\n                \"Ensure that your summary is clear, concise, and that no tasks are repeated or skipped.\"\n            ),\n            role=\"user\",\n        )\n        prompt = await self.formatter.format(\n            msgs=[\n                Msg(\"system\", self.sys_prompt, \"system\"),\n                *memory_msgs,\n                hint_msg,\n            ],\n        )\n        res = await self.model(prompt)\n        summary_text = \"\"\n        print_msg = Msg(name=self.name, content=[], role=\"assistant\")\n        if self.model.stream:\n            async for content_chunk in res:\n                summary_text = content_chunk.content[0][\"text\"]\n                print_msg.content = content_chunk.content\n                await self.print(print_msg, last=False)\n        else:\n            summary_text = res.content[0][\"text\"]\n        print_msg.content = [TextBlock(type=\"text\", text=summary_text)]\n        await self.print(print_msg, last=True)\n\n        summarized_memory: list[Msg] = []\n        if initial_question:\n            summarized_memory.append(\n                Msg(\"user\", initial_question, role=\"user\"),\n            )\n        summarized_memory.append(\n            Msg(self.name, summary_text, role=\"assistant\"),\n        )\n        await self.memory.clear()\n        for m in summarized_memory:\n            await self.memory.add(m)\n\n    async def _get_screenshot(self) -> Optional[str]:\n        \"\"\"\n        Optionally take a screenshot of the current web page for multimodal prompts.\n        Returns base64-encoded PNG data if available, else None.\n        \"\"\"\n        try:\n            # Prepare tool call for screenshot\n            tool_call = ToolUseBlock(\n                id=str(uuid.uuid4()),\n                name=\"browser_take_screenshot\",\n                input={},\n                type=\"tool_use\",\n            )\n            # Execute tool call via service toolkit\n            screenshot_response = await self.toolkit.call_tool_function(\n                tool_call,\n            )\n            # Extract image base64 from response\n            async for chunk in screenshot_response:\n                if (\n                    chunk.content\n                    and len(chunk.content) > 1\n                    and \"data\" in chunk.content[1]\n                ):\n                    image_data = chunk.content[1][\"data\"]\n                else:\n                    image_data = None\n        except Exception:\n            image_data = None\n        return image_data\n\n    @staticmethod\n    def _filter_execution_text(\n        text: str,\n        keep_page_state: bool = False,\n    ) -> str:\n        \"\"\"Filter and clean browser tool execution output to remove verbosity.\"\"\"\n        if not keep_page_state:\n            text = re.sub(r\"- Page URL.*\", \"\", text, flags=re.DOTALL)\n            text = re.sub(r\"```yaml.*?```\", \"\", text, flags=re.DOTALL)\n        text = re.sub(\n            r\"### New console messages.*?(?=### Page state)\",\n            \"\",\n            text,\n            flags=re.DOTALL,\n        )\n        return text.strip()\n\n    def _split_snapshot_by_chunk(\n        self,\n        snapshot_str: str,\n        max_length: int = 80000,\n    ) -> list[str]:\n        self.snapshot_chunk_id = 0\n        return [\n            snapshot_str[i : i + max_length]\n            for i in range(0, len(snapshot_str), max_length)\n        ]\n\n    def observe_by_chunk(self, image_data: str | None = \"\") -> Msg:\n        \"\"\"Create an observation message for chunk-based reasoning.\"\"\"\n        reasoning_prompt = self.observe_reasoning_prompt.format(\n            previous_chunkwise_information=self.previous_chunkwise_information,\n            current_subtask=self.current_subtask,\n            i=self.snapshot_chunk_id + 1,\n            total_pages=len(self.snapshot_in_chunk),\n            chunk=self.snapshot_in_chunk[self.snapshot_chunk_id],\n            init_query=self.original_task,\n        )\n        content: list[Any] = [TextBlock(type=\"text\", text=reasoning_prompt)]\n        if self._supports_multimodal():\n            if image_data:\n                image_block = ImageBlock(\n                    type=\"image\",\n                    source=Base64Source(\n                        type=\"base64\",\n                        media_type=\"image/png\",\n                        data=image_data,\n                    ),\n                )\n                content.append(image_block)\n        observe_msg = Msg(\"user\", content=content, role=\"user\")\n        return observe_msg\n\n    async def browser_subtask_manager(  # pylint: disable=R0912,R0915\n        self,\n    ) -> ToolResponse:  # pylint: disable=R0912,R0915\n        \"\"\"Validate and advance current subtask if completed.\"\"\"\n        if (\n            not hasattr(self, \"subtasks\")\n            or not self.subtasks\n            or self.current_subtask is None\n        ):\n            self.current_subtask = self.original_task\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=(\n                            f\"Tool call Error. Cannot be executed. Current subtask remains: {self.current_subtask}\"\n                        ),\n                    ),\n                ],\n            )\n        memory_content = await self.memory.get_memory()\n        sys_prompt = (\n            \"You are an expert in subtask validation. \\n\"\n            \"Given the following subtask and the agent's recent memory, strictly judge if the subtask is FULLY completed. \\n\"\n            \"If yes, reply ONLY 'SUBTASK_COMPLETED'. If not, reply ONLY 'SUBTASK_NOT_COMPLETED'.\"\n        )\n        if len(self.snapshot_in_chunk) > 0:\n            user_prompt = (\n                f\"Subtask: {self.current_subtask}\\n\"\n                f\"Recent memory:\\n{[str(m) for m in memory_content[-10:]]}\\n\"\n                f\"Current page:\\n{self.snapshot_in_chunk[0]}\"\n            )\n        else:\n            user_prompt = (\n                f\"Subtask: {self.current_subtask}\\n\"\n                f\"Recent memory:\\n{[str(m) for m in memory_content[-10:]]}\\n\"\n            )\n        prompt = await self.formatter.format(\n            msgs=[\n                Msg(\"system\", sys_prompt, role=\"system\"),\n                Msg(\"user\", user_prompt, role=\"user\"),\n            ],\n        )\n        response = await self.model(prompt)\n        response_text = \"\"\n        print_msg = Msg(name=self.name, content=[], role=\"assistant\")\n        if self.model.stream:\n            async for chunk in response:\n                response_text += chunk.content[0][\"text\"]\n                print_msg.content = chunk.content\n                await self.print(print_msg, last=False)\n        else:\n            response_text = response.content[0][\"text\"]\n        print_msg.content = [TextBlock(type=\"text\", text=response_text)]\n        await self.print(print_msg, last=True)\n\n        if \"SUBTASK_COMPLETED\" in response_text.strip().upper():\n            self.current_subtask_idx += 1\n            if self.current_subtask_idx < len(self.subtasks):\n                self.current_subtask = str(\n                    self.subtasks[self.current_subtask_idx],\n                )\n            else:\n                self.current_subtask = None\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=(\n                            \"Tool call SUCCESS. Current subtask updates to: \"\n                            f\"{self.current_subtask}\"\n                        ),\n                    ),\n                ],\n            )\n        else:\n            revise_prompt_path = os.path.join(\n                _PROMPT_DIR,\n                \"browser_agent_subtask_revise_prompt.md\",\n            )\n            with open(revise_prompt_path, \"r\", encoding=\"utf-8\") as fr:\n                revise_prompt = fr.read()\n            memory_content = await self.memory.get_memory()\n            user_prompt = revise_prompt.format(\n                memory=[str(m) for m in memory_content[-10:]],\n                subtasks=json.dumps(self.subtasks, ensure_ascii=False),\n                current_subtask=str(self.current_subtask),\n                original_task=str(self.original_task),\n            )\n            prompt = await self.formatter.format(\n                msgs=[Msg(\"user\", user_prompt, role=\"user\")],\n            )\n            response = await self.model(prompt)\n            if self.model.stream:\n                async for chunk in response:\n                    revise_text = chunk.content[0][\"text\"]\n            else:\n                revise_text = response.content[0][\"text\"]\n            try:\n                if \"```json\" in revise_text:\n                    revise_text = revise_text.replace(\"```json\", \"\").replace(\n                        \"```\",\n                        \"\",\n                    )\n                revise_json = json.loads(revise_text)\n                if_revised = revise_json.get(\"IF_REVISED\")\n                if if_revised:\n                    revised_subtasks = revise_json.get(\"REVISED_SUBTASKS\", [])\n                    if isinstance(revised_subtasks, list) and revised_subtasks:\n                        self.subtasks = revised_subtasks\n                        self.current_subtask_idx = 0\n                        self.current_subtask = self.subtasks[0]\n                        logger.info(\n                            \"Subtasks revised: %s, reason: %s\",\n                            self.subtasks,\n                            revise_json.get(\"REASON\", \"\"),\n                        )\n            except Exception as e:\n                logger.warning(\"Failed to revise subtasks: %s\", e)\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=(\n                        \"Tool call SUCCESS.\"\n                        f\" Current subtask remains: {self.current_subtask}\"\n                    ),\n                ),\n            ],\n        )\n\n    async def browser_generate_final_response(\n        self,  # pylint: disable=W0613\n        **kwargs: Any,  # pylint: disable=W0613\n    ) -> ToolResponse:\n        \"\"\"Generate a final response; validate completion state.\"\"\"\n        hint_msg = Msg(\n            \"user\",\n            _BROWSER_AGENT_SUMMARIZE_TASK_PROMPT,\n            role=\"user\",\n        )\n\n        memory_msgs = await self.memory.get_memory()\n        memory_msgs_copy = copy.deepcopy(memory_msgs)\n        last_msg = memory_msgs_copy[-1]\n        last_msg.content = last_msg.get_content_blocks(\"text\")\n        memory_msgs_copy[-1] = last_msg\n\n        prompt = await self.formatter.format(\n            msgs=[\n                Msg(\"system\", self.sys_prompt, \"system\"),\n                *memory_msgs_copy,\n                hint_msg,\n            ],\n        )\n        try:\n            res = await self.model(prompt)\n            res_msg = Msg(\"assistant\", [], \"assistant\")\n            if self.model.stream:\n                async for content_chunk in res:\n                    summary_text = content_chunk.content[0][\"text\"]\n            else:\n                summary_text = res.content[0][\"text\"]\n            if self.model.stream:\n                summary_text = \"\"\n                async for content_chunk in res:\n                    res_msg.content = content_chunk.content\n                    summary_text = content_chunk.content[0][\"text\"]\n                    await self.print(res_msg, False)\n                await self.print(res_msg, True)\n            else:\n                summary_text = res.content[0][\"text\"]\n                res_msg.content = summary_text\n                await self.print(res_msg, True)\n\n            # Validate finish status\n            finish_status = await self._validate_finish_status(summary_text)\n            logger.info(  # pylint: disable=W1203\n                f\"Finish status: {finish_status}\",\n            )  # pylint: disable=W1203\n\n            if \"BROWSER_AGENT_TASK_FINISHED\" in finish_status:\n                structure_response = {\n                    \"task_done\": True,\n                    \"subtask_progress_summary\": summary_text,\n                    \"generated_files\": {},\n                }\n                return ToolResponse(\n                    content=[\n                        TextBlock(\n                            type=\"text\",\n                            text=\"Successfully generated response.\",\n                        ),\n                    ],\n                    metadata={\n                        \"success\": True,\n                        \"structured_output\": structure_response,\n                    },\n                    is_last=True,\n                )\n            else:\n                return ToolResponse(\n                    content=[\n                        TextBlock(\n                            type=\"text\",\n                            text=(\n                                f\"Here is a summary of current status:\\n{summary_text}\\nPlease continue.\\n\"\n                                f\"Following steps \\n {finish_status}\"\n                            ),\n                        ),\n                    ],\n                    metadata={\"success\": False, \"structured_output\": None},\n                    is_last=True,\n                )\n        except Exception as e:\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Tool call Error. Cannot be executed. {e}\",\n                    ),\n                ],\n                metadata={\"success\": False},\n                is_last=True,\n            )\n\n    async def image_understanding(\n        self,\n        object_description: str,\n        task: str,\n    ) -> ToolResponse:\n        \"\"\"\n        Locate an element by description, take a focused screenshot, and solve a task using it.\n        \"\"\"\n        sys_prompt = (\n            \"You are a web page analysis expert. Given the following page snapshot and object description, \"\n            \"identify the exact element and its reference string (ref) that matches the description. \"\n            'Return ONLY a JSON object: {\"element\": <element description>, \"ref\": <ref string>}'\n        )\n        snapshot_chunks = await self._get_snapshot_in_text()\n        page_snapshot = snapshot_chunks[0] if snapshot_chunks else \"\"\n        user_prompt = f\"Object description: {object_description}\\nPage snapshot:\\n{page_snapshot}\"\n        prompt = await self.formatter.format(\n            msgs=[\n                Msg(\"system\", sys_prompt, role=\"system\"),\n                Msg(\"user\", user_prompt, role=\"user\"),\n            ],\n        )\n        res = await self.model(prompt)\n        if self.model.stream:\n            async for chunk in res:\n                model_text = chunk.content[0][\"text\"]\n        else:\n            model_text = res.content[0][\"text\"]\n        try:\n            if \"```json\" in model_text:\n                model_text = model_text.replace(\"```json\", \"\").replace(\n                    \"```\",\n                    \"\",\n                )\n            element_info = json.loads(model_text)\n            element = element_info.get(\"element\", \"\")\n            ref = element_info.get(\"ref\", \"\")\n        except Exception:\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=\"Failed to parse element/ref from model output.\",\n                    ),\n                ],\n                metadata={\"success\": False},\n            )\n\n        screenshot_tool_call = ToolUseBlock(\n            id=str(uuid.uuid4()),\n            name=\"browser_take_screenshot\",\n            input={\"element\": element, \"ref\": ref},\n            type=\"tool_use\",\n        )\n        screenshot_response = await self.toolkit.call_tool_function(\n            screenshot_tool_call,\n        )\n        image_data = None\n        async for chunk in screenshot_response:\n            if chunk.content and len(chunk.content) > 1:\n                block = chunk.content[1]\n                if \"data\" in block:\n                    image_data = block[\"data\"]\n                elif \"source\" in block and \"data\" in block[\"source\"]:\n                    image_data = block[\"source\"][\"data\"]\n\n        sys_prompt_task = (\n            \"You are a web automation expert. Given the object description, screenshot, and page context, \"\n            \"solve the following task. Return ONLY the answer as plain text.\"\n        )\n        content_blocks: list[Any] = [\n            TextBlock(\n                type=\"text\",\n                text=f\"Object description: {object_description}\\nTask: {task}\\nPage snapshot:\\n{page_snapshot}\",\n            ),\n        ]\n        if image_data:\n            image_block = ImageBlock(\n                type=\"image\",\n                source=Base64Source(\n                    type=\"base64\",\n                    media_type=\"image/png\",\n                    data=image_data,\n                ),\n            )\n            content_blocks.append(image_block)\n        prompt_task = await self.formatter.format(\n            msgs=[\n                Msg(\"system\", sys_prompt_task, role=\"system\"),\n                Msg(\"user\", content_blocks, role=\"user\"),\n            ],\n        )\n        res_task = await self.model(prompt_task)\n        if self.model.stream:\n            async for chunk in res_task:\n                answer_text = chunk.content[0][\"text\"]\n        else:\n            answer_text = res_task.content[0][\"text\"]\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=(\n                        f\"Screenshot taken for element: {element}\\nref: {ref}\\n\"\n                        f\"Task solution: {answer_text}\"\n                    ),\n                ),\n            ],\n        )\n\n    async def _validate_finish_status(self, summary: str) -> str:\n        \"\"\"Validate if the agent has completed its task based on the summary.\"\"\"\n        sys_prompt = (\n            \"You are an expert in task validation. \"\n            \"Your job is to determine if the agent has completed its task\"\n            \" based on the provided summary. If the summary is `NO_ANSWER`, this task \"\n            \"is not over unless the task is determined as definitely not completed. \"\n            \"If finished, strictly reply \"\n            '\"BROWSER_AGENT_TASK_FINISHED\" and your reason, otherwise return the remaining '\n            \"tasks or next steps.\"\n        )\n        initial_question = None\n        memory_msgs = await self.memory.get_memory()\n        for msg in memory_msgs:\n            if msg.role == \"user\":\n                initial_question = msg.content\n                break\n        prompt = await self.formatter.format(\n            msgs=[\n                Msg(\"system\", sys_prompt, role=\"system\"),\n                Msg(\n                    \"user\",\n                    content=(\n                        \"The initial task is to solve the following question: \"\n                        f\"{initial_question} \\n \"\n                        f\"Here is a summary of current task completion process, please evaluate the task finish status.\\n\"\n                        + summary\n                    ),\n                    role=\"user\",\n                ),\n            ],\n        )\n        res = await self.model(prompt)\n        response_text = \"\"\n        if self.model.stream:\n            async for content_chunk in res:\n                response_text = content_chunk.content[0][\"text\"]\n        else:\n            response_text = res.content[0][\"text\"]\n        return response_text\n\n    def _register_skill_tool(\n        self,\n        skill_func: Any,\n    ) -> None:\n        \"\"\"Bind the browser agent to a skill function and register it as a tool.\"\"\"\n        if asyncio.iscoroutinefunction(skill_func):\n\n            @wraps(skill_func)\n            async def tool(*args: Any, **kwargs: Any) -> Any:\n                return await skill_func(\n                    browser_agent=self,\n                    *args,\n                    **kwargs,\n                )\n\n        else:\n\n            @wraps(skill_func)\n            async def tool(*args: Any, **kwargs: Any) -> Any:\n                return skill_func(\n                    browser_agent=self,\n                    *args,\n                    **kwargs,\n                )\n\n        original_signature = inspect.signature(skill_func)\n        parameters = list(original_signature.parameters.values())\n        if parameters and parameters[0].name == \"browser_agent\":\n            parameters = parameters[1:]\n        try:\n            tool.__signature__ = original_signature.replace(\n                parameters=parameters,\n            )\n        except ValueError:\n            pass\n\n        self.toolkit.register_tool_function(tool)\n\n    def _supports_multimodal(self) -> bool:\n        \"\"\"Check if the model supports multimodal input (images/videos).\"\"\"\n        return (\n            self.model.model_name.startswith(\"qvq\")\n            or \"-vl\" in self.model.model_name\n            or \"4o\" in self.model.model_name\n            or \"gpt-5\" in self.model.model_name\n        )\n"
  },
  {
    "path": "examples/agent/browser_agent/build_in_helper/_file_download.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Standalone file download skill for the browser agent.\"\"\"\n# flake8: noqa: E501\n# pylint: disable=W0212,W0107,too-many-lines,C0301\n\nfrom __future__ import annotations\nimport os\nimport copy\nfrom typing import Any\nfrom pydantic import BaseModel\n\n\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.message import Msg, TextBlock\nfrom agentscope.tool import ToolResponse\nfrom agentscope.agent import ReActAgent\n\n\n_CURRENT_DIR = os.path.abspath(\n    os.path.join(os.path.dirname(__file__), os.pardir),\n)\n\nwith open(\n    os.path.join(\n        _CURRENT_DIR,\n        \"build_in_prompt/browser_agent_file_download_sys_prompt.md\",\n    ),\n    \"r\",\n    encoding=\"utf-8\",\n) as f:\n    _FILE_DOWNLOAD_AGENT_SYS_PROMPT = f.read()\n\n\nclass EmptyModel(BaseModel):\n    \"\"\"Empty structured model for default structured output requirement.\"\"\"\n\n    pass\n\n\nclass FileDownloadAgent(ReActAgent):\n    \"\"\"Lightweight helper agent that downloads files\"\"\"\n\n    finish_function_name: str = \"file_download_final_response\"\n\n    def __init__(\n        self,\n        browser_agent: Any,\n        sys_prompt: str = _FILE_DOWNLOAD_AGENT_SYS_PROMPT,\n        max_iters: int = 15,\n    ) -> None:\n        name = (\n            f\"{getattr(browser_agent, 'name', 'browser_agent')}_file_download\"\n        )\n        super().__init__(\n            name=name,\n            sys_prompt=sys_prompt,\n            model=browser_agent.model,\n            formatter=browser_agent.formatter,\n            memory=InMemoryMemory(),\n            toolkit=browser_agent.toolkit,\n            max_iters=max_iters,\n        )\n        # Register the finish function\n        self.toolkit.register_tool_function(self.file_download_final_response)\n        # Remove conflicting tool functions if they exist\n        if hasattr(self.toolkit, \"remove_tool_function\"):\n            try:\n                self.toolkit.remove_tool_function(\"browser_pdf_save\")\n            except Exception:\n                # Tool may not exist, ignore removal errors\n                pass\n            try:\n                self.toolkit.remove_tool_function(\"file_download\")\n            except Exception:\n                # Tool may not exist, ignore removal errors\n                pass\n\n    async def file_download_final_response(\n        self,  # pylint: disable=W0613\n        **kwargs: Any,  # pylint: disable=W0613\n    ) -> ToolResponse:\n        \"\"\"Summarize the file download outcome.\"\"\"\n        hint_msg = Msg(\n            \"user\",\n            (\n                \"Provide a concise summary of the file download attempt.\\n\"\n                \"Highlight these items:\\n\"\n                \"0. The original request\\n\"\n                \"1. The element(s) interacted with and actions taken\\n\"\n                \"2. The download status or any issues encountered\\n\"\n                \"3. Any follow-up recommendations or next steps\\n\"\n            ),\n            role=\"user\",\n        )\n\n        memory_msgs = await self.memory.get_memory()\n        memory_msgs_copy = copy.deepcopy(memory_msgs)\n        if memory_msgs_copy:\n            last_msg = memory_msgs_copy[-1]\n            last_msg.content = last_msg.get_content_blocks(\"text\")\n            memory_msgs_copy[-1] = last_msg\n\n        prompt = await self.formatter.format(\n            msgs=[\n                Msg(\"system\", self.sys_prompt, \"system\"),\n                *memory_msgs_copy,\n                hint_msg,\n            ],\n        )\n\n        res = await self.model(prompt)\n\n        if self.model.stream:\n            summary_text = \"\"\n            async for chunk in res:\n                summary_text = chunk.content[0][\"text\"]\n        else:\n            summary_text = res.content[0][\"text\"]\n\n        summary_text = summary_text or \"No summary generated.\"\n\n        structure_response = {\n            \"task_done\": True,\n            \"subtask_progress_summary\": summary_text,\n            \"generated_files\": {},\n        }\n\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=\"File download summary generated. \" + summary_text,\n                ),\n            ],\n            metadata={\n                \"success\": True,\n                \"structured_output\": structure_response,\n            },\n            is_last=True,\n        )\n\n\ndef _build_initial_instruction(\n    target_description: str,\n    snapshot_text: str,\n) -> str:\n    \"\"\"Compose the initial instruction for the helper agent.\"\"\"\n    return (\n        \"You must locate and trigger the download for the requested file.\\n\\n\"\n        \"Target description provided by the user:\\n\"\n        f\"{target_description}\\n\\n\"\n        \"Latest snapshot captured prior to your run:\\n\"\n        f\"{snapshot_text}\\n\\n\"\n        \"Follow the sys prompt guidance, think step-by-step, and verify that \"\n        \"the download action succeeded. If the download cannot be completed, \"\n        \"explain why in the final summary.\"\n    )\n\n\nasync def file_download(\n    browser_agent: Any,\n    target_description: str,\n) -> ToolResponse:\n    \"\"\"\n    Download the target file. The current page should\n    contain download-related element.\n\n    Args:\n        target_description (str): The description of the\n        target file to download.\n\n    Returns:\n        ToolResponse: A structured response containing\n        the download directory.\n    \"\"\"\n    try:\n        snapshot_chunks = await browser_agent._get_snapshot_in_text()\n    except Exception as exc:  # pylint: disable=broad-except\n        snapshot_chunks = []\n        snapshot_error = str(exc)\n    else:\n        snapshot_error = \"\"\n\n    snapshot_text = \"\\n\\n---\\n\\n\".join(snapshot_chunks)\n    if snapshot_error and not snapshot_text:\n        snapshot_text = f\"[Snapshot failed: {snapshot_error}]\"\n\n    sub_agent = FileDownloadAgent(browser_agent)\n    instruction = _build_initial_instruction(\n        target_description=target_description,\n        snapshot_text=snapshot_text,\n    )\n\n    init_msg = Msg(\n        name=\"user\",\n        role=\"user\",\n        content=instruction,\n    )\n\n    try:\n        sub_agent_response_msg = await sub_agent.reply(\n            init_msg,\n            structured_model=EmptyModel,\n        )\n\n        text_content = \"\"\n        if sub_agent_response_msg.content:\n            first_block = sub_agent_response_msg.content[0]\n            if isinstance(first_block, dict):\n                text_content = first_block.get(\"text\") or \"\"\n            else:\n                text_content = getattr(first_block, \"text\", \"\") or \"\"\n\n        if not text_content:\n            text_content = (\n                \"File download agent finished without a textual summary.\"\n            )\n\n        return ToolResponse(\n            metadata=sub_agent_response_msg.metadata,\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=text_content,\n                ),\n            ],\n        )\n    except Exception as exc:  # pylint: disable=broad-except\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=f\"Tool call Error. Cannot be executed. {exc}\",\n                ),\n            ],\n            metadata={\"success\": False},\n            is_last=True,\n        )\n"
  },
  {
    "path": "examples/agent/browser_agent/build_in_helper/_form_filling.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Standalone form filling skill for the browser agent.\"\"\"\n# flake8: noqa: E501\n# pylint: disable=W0212,W0107,too-many-lines,C0301\n\nfrom __future__ import annotations\nimport os\nimport copy\nfrom typing import Any\nfrom pydantic import BaseModel\n\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.message import Msg, TextBlock\nfrom agentscope.tool import ToolResponse\nfrom agentscope.agent import ReActAgent\n\n_CURRENT_DIR = os.path.abspath(\n    os.path.join(os.path.dirname(__file__), os.pardir),\n)\n\nwith open(\n    os.path.join(\n        _CURRENT_DIR,\n        \"build_in_prompt/browser_agent_form_filling_sys_prompt.md\",\n    ),\n    \"r\",\n    encoding=\"utf-8\",\n) as f:\n    _FORM_FILL_AGENT_SYS_PROMPT = f.read()\n\n\nclass EmptyModel(BaseModel):\n    \"\"\"Empty structured model for default structured output requirement.\"\"\"\n\n    pass\n\n\nclass FormFillingAgent(ReActAgent):\n    \"\"\"Lightweight helper agent that fills forms.\"\"\"\n\n    finish_function_name: str = \"form_filling_final_response\"\n\n    def __init__(\n        self,\n        browser_agent: Any,\n        sys_prompt: str = _FORM_FILL_AGENT_SYS_PROMPT,\n        max_iters: int = 20,\n    ) -> None:\n        name = f\"{getattr(browser_agent, 'name', 'browser_agent')}_form_fill\"\n        super().__init__(\n            name=name,\n            sys_prompt=sys_prompt,\n            model=browser_agent.model,\n            formatter=browser_agent.formatter,\n            memory=InMemoryMemory(),\n            toolkit=browser_agent.toolkit,\n            max_iters=max_iters,\n        )\n        # Register the finish function\n        self.toolkit.register_tool_function(self.form_filling_final_response)\n\n    async def form_filling_final_response(\n        self,  # pylint: disable=W0613\n        **kwargs: Any,  # pylint: disable=W0613\n    ) -> ToolResponse:\n        \"\"\"Summarize the form filling outcome.\"\"\"\n        hint_msg = Msg(\n            \"user\",\n            (\n                \"Provide a concise summary of the completed form \"\n                \"filling task.\\n\"\n                \"Highlight these items:\\n\"\n                \"0. The original task/query\\n\"\n                \"1. Which fields were filled/selected and their final values\\n\"\n                \"2. Any important observations or follow-up notes\\n\"\n                \"3. Confirmation that if the task is complete\\n\\n\"\n            ),\n            role=\"user\",\n        )\n\n        memory_msgs = await self.memory.get_memory()\n        memory_msgs_copy = copy.deepcopy(memory_msgs)\n        last_msg = memory_msgs_copy[-1]\n        # check if the last message has tool call, if so clean the content\n\n        last_msg.content = last_msg.get_content_blocks(\"text\")\n        memory_msgs_copy[-1] = last_msg\n\n        prompt = await self.formatter.format(\n            msgs=[\n                Msg(\"system\", self.sys_prompt, \"system\"),\n                *memory_msgs_copy,\n                hint_msg,\n            ],\n        )\n\n        res = await self.model(prompt)\n\n        if self.model.stream:\n            summary_text = \"\"\n            async for chunk in res:\n                summary_text = chunk.content[0][\"text\"]\n        else:\n            summary_text = res.content[0][\"text\"]\n\n        structure_response = {\n            \"task_done\": True,\n            \"subtask_progress_summary\": summary_text,\n            \"generated_files\": {},\n        }\n\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=\"Form filling summary generated. \" + summary_text,\n                ),\n            ],\n            metadata={\n                \"success\": True,\n                \"structured_output\": structure_response,\n            },\n            is_last=True,\n        )\n\n\ndef _build_initial_instruction(\n    fill_information: str,\n    snapshot_text: str,\n) -> str:\n    \"\"\"Compose the initial instruction fed to the helper agent.\"\"\"\n    return (\n        \"You must complete the web form using the information \"\n        \"provided below.\\n\\nFill instructions (plain text from the user):\\n\"\n        f\"{fill_information}\\n\\n\"\n        \"Latest snapshot captured prior to your run:\\n\"\n        f\"{snapshot_text}\\n\\n\"\n    )\n\n\nasync def form_filling(\n    browser_agent: Any,\n    fill_information: str,\n) -> ToolResponse:\n    \"\"\"\n    Fill in a web form according to plain-text instructions.\n\n    Args:\n        fill_information (str):\n            Plain-text description of the values that\n            must be entered into the form,\n            including any submission requirements.\n\n    Returns:\n        ToolResponse: Summary of the helper agent execution and status.\n    \"\"\"\n    try:\n        snapshot_chunks = (\n            await browser_agent._get_snapshot_in_text()\n        )  # pylint: disable=protected-access\n    except Exception as exc:  # pylint: disable=broad-except\n        snapshot_chunks = []\n        snapshot_error = str(exc)\n    else:\n        snapshot_error = \"\"\n\n    snapshot_text = \"\\n\\n---\\n\\n\".join(snapshot_chunks)\n    if snapshot_error and not snapshot_text:\n        snapshot_text = f\"[Snapshot failed: {snapshot_error}]\"\n\n    sub_agent = FormFillingAgent(browser_agent)\n    instruction = _build_initial_instruction(\n        fill_information=fill_information,\n        snapshot_text=snapshot_text,\n    )\n\n    init_msg = Msg(\n        name=\"user\",\n        role=\"user\",\n        content=instruction,\n    )\n\n    try:\n        sub_agent_response_msg = await sub_agent.reply(\n            init_msg,\n            structured_model=EmptyModel,\n        )\n\n        text_content = \"\"\n        if sub_agent_response_msg.content:\n            first_block = sub_agent_response_msg.content[0]\n            if isinstance(first_block, dict):\n                text_content = first_block.get(\"text\") or \"\"\n            else:\n                text_content = getattr(first_block, \"text\", \"\") or \"\"\n\n        if not text_content:\n            text_content = (\n                \"Form filling agent finished without a textual summary.\"\n            )\n\n        return ToolResponse(\n            metadata=sub_agent_response_msg.metadata,\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=text_content,\n                ),\n            ],\n        )\n    except Exception as e:\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=f\"Tool call Error. Cannot be executed. {e}\",\n                ),\n            ],\n            metadata={\"success\": False},\n            is_last=True,\n        )\n"
  },
  {
    "path": "examples/agent/browser_agent/build_in_helper/_image_understanding.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Standalone image understanding skill for the browser agent.\"\"\"\n# flake8: noqa: E501\n# pylint: disable=W0212\n# pylint: disable=too-many-lines\n# pylint: disable=C0301\nfrom __future__ import annotations\n\nimport json\nimport uuid\nfrom typing import Any\n\nfrom agentscope.message import (\n    Base64Source,\n    ImageBlock,\n    Msg,\n    TextBlock,\n    ToolUseBlock,\n)\nfrom agentscope.tool import ToolResponse\n\n\nasync def image_understanding(\n    browser_agent: Any,\n    object_description: str,\n    task: str,\n) -> ToolResponse:\n    \"\"\"\n    Locate an element and solve a visual task on the current webpage.\n\n    Args:\n        object_description (str): The description of the object to locate.\n        task (str): The specific task or question to solve about the image\n        (e.g., description, object detection, activity recognition, or\n        answering a question about the image's content).\n\n    Returns:\n        ToolResponse: A structured response containing the answer to\n        the specified task based on the image content.\n    \"\"\"\n\n    sys_prompt = (\n        \"You are a web page analysis expert. Given the following page \"\n        \"snapshot and object description, \"\n        \"identify the exact element and its reference string (ref) \"\n        \"that matches the description. \"\n        \"Return ONLY a JSON object: \"\n        '{\"element\": <element description>, \"ref\": <ref string>}'\n    )\n\n    snapshot_chunks = (\n        await browser_agent._get_snapshot_in_text()  # noqa: E501 # pylint: disable=protected-access\n    )\n    page_snapshot = snapshot_chunks[0] if snapshot_chunks else \"\"\n    user_prompt = (\n        f\"Object description: {object_description}\\n\"\n        f\"Page snapshot:\\n{page_snapshot}\"\n    )\n\n    prompt = await browser_agent.formatter.format(\n        msgs=[\n            Msg(\"system\", sys_prompt, role=\"system\"),\n            Msg(\"user\", user_prompt, role=\"user\"),\n        ],\n    )\n    res = await browser_agent.model(prompt)\n    if browser_agent.model.stream:\n        async for chunk in res:\n            model_text = chunk.content[0][\"text\"]\n    else:\n        model_text = res.content[0][\"text\"]\n\n    try:\n        if \"```json\" in model_text:\n            model_text = model_text.replace(\"```json\", \"\").replace(\n                \"```\",\n                \"\",\n            )\n        element_info = json.loads(model_text)\n        element = element_info.get(\"element\", \"\")\n        ref = element_info.get(\"ref\", \"\")\n    except Exception:\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=\"Failed to parse element/ref from model output.\",\n                ),\n            ],\n            metadata={\"success\": False},\n        )\n\n    screenshot_tool_call = ToolUseBlock(\n        id=str(uuid.uuid4()),\n        name=\"browser_take_screenshot\",\n        input={\"element\": element, \"ref\": ref},\n        type=\"tool_use\",\n    )\n    screenshot_response = await browser_agent.toolkit.call_tool_function(\n        screenshot_tool_call,\n    )\n    image_data = None\n    async for chunk in screenshot_response:\n        if (\n            chunk.content\n            and len(chunk.content) > 1\n            and \"data\" in chunk.content[1]\n        ):\n            image_data = chunk.content[1][\"data\"]\n\n    sys_prompt_task = (\n        \"You are a web automation expert. \"\n        \"Given the object description, screenshot, and page context, \"\n        \"solve the following task. Return ONLY the answer as plain text.\"\n    )\n    content_blocks = [\n        TextBlock(\n            type=\"text\",\n            text=(\n                \"Object description: \"\n                f\"{object_description}\\nTask: {task}\\n\"\n                f\"Page snapshot:\\n{page_snapshot}\"\n            ),\n        ),\n    ]\n\n    if image_data:\n        image_block = ImageBlock(\n            type=\"image\",\n            source=Base64Source(\n                type=\"base64\",\n                media_type=\"image/png\",\n                data=image_data,\n            ),\n        )\n        content_blocks.append(image_block)\n\n    prompt_task = await browser_agent.formatter.format(\n        msgs=[\n            Msg(\"system\", sys_prompt_task, role=\"system\"),\n            Msg(\"user\", content_blocks, role=\"user\"),\n        ],\n    )\n    res_task = await browser_agent.model(prompt_task)\n    if browser_agent.model.stream:\n        async for chunk in res_task:\n            answer_text = chunk.content[0][\"text\"]\n    else:\n        answer_text = res_task.content[0][\"text\"]\n\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=(\n                    f\"Screenshot taken for element: {element}\\nref: {ref}\\n\"\n                    f\"Task solution: {answer_text}\"\n                ),\n            ),\n        ],\n    )\n"
  },
  {
    "path": "examples/agent/browser_agent/build_in_helper/_video_understanding.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Standalone video understanding skill for the browser agent.\"\"\"\n# flake8: noqa: E501\n# pylint: disable=W0212\n# pylint: disable=too-many-lines\n# pylint: disable=C0301\nfrom __future__ import annotations\n\nimport json\nimport os\nimport subprocess\nimport tempfile\nimport uuid\nfrom base64 import b64encode\nfrom pathlib import Path\nfrom typing import Any, List, Optional\n\nfrom agentscope.message import (\n    Base64Source,\n    ImageBlock,\n    Msg,\n    TextBlock,\n)\nfrom agentscope.tool import ToolResponse\n\n\nasync def video_understanding(\n    browser_agent: Any,\n    video_path: str,\n    task: str,\n) -> ToolResponse:\n    \"\"\"\n    Perform video understanding on the provided video file.\n\n    Args:\n        video_path (str): The path to the video file to analyze.\n        task (str): The specific task or question to solve about\n        the video (e.g., summary, object detection, activity recognition,\n        or answering a question about the video's content).\n\n    Returns:\n        ToolResponse: A structured response containing the answer\n        to the specified task based on the video content.\n    \"\"\"\n\n    workdir = _prepare_workdir(browser_agent)\n    try:\n        frames_dir = os.path.join(workdir, \"frames\")\n        frames = extract_frames(video_path, frames_dir)\n    except Exception as exc:\n        return _error_response(f\"Failed to extract frames: {exc}\")\n\n    audio_path = os.path.join(\n        workdir,\n        f\"audio_{getattr(browser_agent, 'iter_n', 0)}.wav\",\n    )\n    try:\n        extract_audio(video_path, audio_path)\n    except Exception as exc:\n        return _error_response(f\"Failed to extract audio: {exc}\")\n\n    try:\n        transcript = audio2text(audio_path)\n    except Exception as exc:\n        return _error_response(f\"Failed to transcribe audio: {exc}\")\n\n    sys_prompt = (\n        \"You are a web video analysis expert. \"\n        \"Given the following video frames and audio transcript, \"\n        \"analyze the content and provide a solution to the task. \"\n        'Return ONLY a JSON object: {\"answer\": <your answer>}'\n    )\n\n    content_blocks = _build_multimodal_blocks(frames, transcript, task)\n\n    prompt = await browser_agent.formatter.format(\n        msgs=[\n            Msg(\"system\", sys_prompt, role=\"system\"),\n            Msg(\"user\", content_blocks, role=\"user\"),\n        ],\n    )\n\n    res = await browser_agent.model(prompt)\n    if browser_agent.model.stream:\n        async for chunk in res:\n            model_text = chunk.content[0][\"text\"]\n    else:\n        model_text = res.content[0][\"text\"]\n\n    try:\n        if \"```json\" in model_text:\n            model_text = model_text.replace(\"```json\", \"\").replace(\n                \"```\",\n                \"\",\n            )\n        answer_info = json.loads(model_text)\n        answer = answer_info.get(\"answer\", \"\")\n    except Exception:  # pylint: disable=broad-except\n        return _error_response(\"Failed to parse answer from model output.\")\n\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=(\n                    \"Video analysis completed.\\n\" f\"Task solution: {answer}\"\n                ),\n            ),\n        ],\n    )\n\n\ndef audio2text(audio_path: str) -> str:\n    \"\"\"Convert audio to text using DashScope ASR.\"\"\"\n\n    try:  # Local import to avoid hard dependency when unused.\n        from dashscope.audio.asr import Recognition, RecognitionCallback\n    except ImportError as exc:\n        raise RuntimeError(\n            \"dashscope.audio is required for audio transcription.\",\n        ) from exc\n\n    callback = RecognitionCallback()\n    recognizer = Recognition(\n        model=\"paraformer-realtime-v1\",\n        format=\"wav\",\n        sample_rate=16000,\n        callback=callback,\n    )\n\n    result = recognizer.call(audio_path)\n    sentences = result.get(\"output\", {}).get(\"sentence\", [])\n    return \" \".join(sentence.get(\"text\", \"\") for sentence in sentences)\n\n\ndef extract_frames(\n    video_path: str,\n    output_dir: str,\n    max_frames: int = 16,\n) -> List[str]:\n    \"\"\"Extract representative frames using ffmpeg (no OpenCV dependency).\"\"\"\n\n    if max_frames <= 0:\n        raise ValueError(\"max_frames must be greater than zero.\")\n\n    if not os.path.exists(video_path):\n        raise FileNotFoundError(f\"Video path not found: {video_path}\")\n\n    os.makedirs(output_dir, exist_ok=True)\n\n    # Clean up previous generated frames\n    for existing in Path(output_dir).glob(\"frame_*.jpg\"):\n        try:\n            existing.unlink()\n        except OSError:\n            # Ignore errors during cleanup;\n            # leftover files will be overwritten or do not affect frame extraction\n            pass\n\n    duration = _probe_video_duration(video_path)\n    if duration and duration > 0:\n        fps = max_frames / duration\n    else:\n        fps = 1.0\n\n    fps = max(min(fps, 30.0), 0.1)\n\n    command = [\n        \"ffmpeg\",\n        \"-y\",\n        \"-i\",\n        video_path,\n        \"-vf\",\n        f\"fps={fps:.5f}\",\n        \"-frames:v\",\n        str(max_frames),\n        os.path.join(output_dir, \"frame_%04d.jpg\"),\n    ]\n\n    try:\n        subprocess.run(\n            command,\n            check=True,\n            stdout=subprocess.DEVNULL,\n            stderr=subprocess.DEVNULL,\n        )\n    except FileNotFoundError as exc:\n        raise RuntimeError(\n            \"ffmpeg is required to extract frames from video.\",\n        ) from exc\n\n    frame_files = sorted(\n        str(path) for path in Path(output_dir).glob(\"frame_*.jpg\")\n    )\n\n    if not frame_files:\n        raise RuntimeError(\"No frames could be extracted from the video.\")\n\n    return frame_files\n\n\ndef extract_audio(video_path: str, audio_path: str) -> str:\n    \"\"\"Extract audio track with ffmpeg and save as wav.\"\"\"\n\n    if not os.path.exists(video_path):\n        raise FileNotFoundError(f\"Video path not found: {video_path}\")\n\n    os.makedirs(os.path.dirname(audio_path), exist_ok=True)\n\n    command = [\n        \"ffmpeg\",\n        \"-y\",\n        \"-i\",\n        video_path,\n        \"-vn\",\n        \"-acodec\",\n        \"pcm_s16le\",\n        \"-ar\",\n        \"16000\",\n        \"-ac\",\n        \"1\",\n        audio_path,\n    ]\n\n    try:\n        subprocess.run(\n            command,\n            check=True,\n            stdout=subprocess.DEVNULL,\n            stderr=subprocess.DEVNULL,\n        )\n    except FileNotFoundError as exc:\n        raise RuntimeError(\n            \"ffmpeg is required to extract audio from video.\",\n        ) from exc\n\n    return audio_path\n\n\ndef _probe_video_duration(video_path: str) -> Optional[float]:\n    \"\"\"Return the video duration in seconds using ffprobe, if available.\"\"\"\n\n    command = [\n        \"ffprobe\",\n        \"-v\",\n        \"error\",\n        \"-show_entries\",\n        \"format=duration\",\n        \"-of\",\n        \"default=noprint_wrappers=1:nokey=1\",\n        video_path,\n    ]\n\n    try:\n        result = subprocess.run(\n            command,\n            check=True,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.DEVNULL,\n            text=True,\n        )\n        duration_str = result.stdout.strip()\n        if duration_str:\n            return float(duration_str)\n    except (FileNotFoundError, ValueError, subprocess.CalledProcessError):\n        return None\n\n    return None\n\n\ndef _build_multimodal_blocks(\n    frames: List[str],\n    transcript: str,\n    task: str,\n) -> list:\n    \"\"\"Construct multimodal content blocks for the model input.\"\"\"\n\n    blocks: list = []\n    for frame_path in frames:\n        with open(frame_path, \"rb\") as file:\n            data = b64encode(file.read()).decode(\"ascii\")\n        image_block = ImageBlock(\n            type=\"image\",\n            source=Base64Source(\n                type=\"base64\",\n                media_type=\"image/jpeg\",\n                data=data,\n            ),\n        )\n        blocks.append(image_block)\n\n    blocks.append(\n        TextBlock(\n            type=\"text\",\n            text=f\"Audio transcript:\\n{transcript}\",\n        ),\n    )\n    blocks.append(\n        TextBlock(\n            type=\"text\",\n            text=f\"The task to be solved is: {task}\",\n        ),\n    )\n    return blocks\n\n\ndef _prepare_workdir(browser_agent: Any) -> str:\n    \"\"\"Prepare a working directory for intermediate artifacts.\"\"\"\n\n    base_dir = getattr(browser_agent, \"state_saving_dir\", None)\n    if not base_dir:\n        base_dir = tempfile.gettempdir()\n\n    workdir = os.path.join(base_dir, \"video_understanding\", uuid.uuid4().hex)\n    os.makedirs(workdir, exist_ok=True)\n    return workdir\n\n\ndef _error_response(message: str) -> ToolResponse:\n    \"\"\"Create a standardized error response.\"\"\"\n\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=message,\n            ),\n        ],\n        metadata={\"success\": False},\n    )\n"
  },
  {
    "path": "examples/agent/browser_agent/build_in_prompt/browser_agent_decompose_reflection_prompt.md",
    "content": "Your role is to assess and optimize task decomposition for browser automation. Specifically, you will evaluate:\nWhether the provided subtasks, when completed, will fully and correctly accomplish the original task.\nWhether the original task requires decomposition. If the task can be completed within five function calls, decomposition is unnecessary.\n\n\nCarefully review both the original task and the list of generated subtasks.\n\n- If decomposition is not required, confirm this by providing the original task as your response.\n- If decomposition is necessary, analyze whether completing all subtasks will achieve the same result as the original task without missing or extraneous steps.\n- \"If\" statement should not be used in subtask descriptions. All statements should be direct and assertive.\n- In cases where the subtasks are insufficient or incorrect, revise them to ensure completeness and accuracy.\n\nFormat your response as the following JSON:\n{{\n  \"DECOMPOSITION\": true/false, // true if decomposition is necessary, false otherwise\n  \"SUFFICIENT\": true/false/na, // if decomposition is necessary, true if the subtasks are sufficient, false otherwise, na if decomposition is not necessary.\n  \"REASON\": \"Briefly explain your reasoning.\",\n  \"REVISED_SUBTASKS\": [ // If not sufficient, provide a revised JSON array of subtasks. If sufficient, repeat the original subtasks. If decomposition is not necessary, provide the original task.\n    \"subtask 1\",\n    \"subtask 2\"\n  ]\n}}\n\nOriginal task:\n{original_task}\n\nGenerated subtasks:\n{subtasks}\n"
  },
  {
    "path": "examples/agent/browser_agent/build_in_prompt/browser_agent_file_download_sys_prompt.md",
    "content": "You are a meticulous web automation specialist. Study the provided page snapshot carefully before acting.\nIdentify the element that allows the user to download the requested file.\nVerify every locator prior to interaction.\n\nIf you need to download a PDF that is already open in the browser, click the webpage's download button to save the file locally.\n\nUse the available browser tools (click, hover, wait, snapshot) to ensure the correct element is activated. Request fresh snapshots after meaningful changes when needed.\n\nStop only when the file download has been initiated or the task cannot be completed, then call the `file_download_final_response` tool with a concise summary including: the original request, the interaction performed, any important observations, and the final status."
  },
  {
    "path": "examples/agent/browser_agent/build_in_prompt/browser_agent_form_filling_sys_prompt.md",
    "content": "You are a specialized web form operator. Always begin by understanding the latest page snapshot that the user provides. CRITICAL: Before interacting with ANY input field, first identify its type:\n- DROPDOWN/SELECT: Use click to open, then select the matching option\n- NEVER type into dropdowns\n- RADIO BUTTONS: Click the appropriate radio button option\n- CHECKBOXES: Click to check/uncheck as needed\n- TEXT INPUTS: Only use typing for genuine text input fields\n- AUTOCOMPLETE: Type to filter, then click the matching suggestion\n\nVerify every locator before interacting.\nIdentify the type of the input field and use the correct tool to fill the form.\nFor typing related values, use the tool 'browser_fill_form' to fill the form.\nFor dropdown related values,use the tool 'browser_select_option' to select the option.\nSome dropdowns may have a search input. If so, use the search input to find the matching option and select it.\nIf you see a dropdown arrow, select element, or multiple choice options, you MUST use clicking/selection - NOT typing.\nIf the option does not exactly match your fill_information, find the closest matching option and select it.\nAfter each meaningful interaction, request a fresh snapshot to confirm the page state before proceeding.\nStop only when all requested values are entered correctly and required submissions are complete. Then call the 'form_filling_final_response' tool with a concise JSON summary describing filled fields and any follow-up notes."
  },
  {
    "path": "examples/agent/browser_agent/build_in_prompt/browser_agent_observe_reasoning_prompt.md",
    "content": "You are viewing a website snapshot in multiple chunks because the content is too long to display at once.\nContext from previous chunks:\n{previous_chunkwise_information}\nYou are on chunk {i} of {total_pages}.\nBelow is the content of this chunk:\n{chunk}\n\n**Instructions**:\nCarefully decide whether you need to use a tool (except for `browser_snapshot`—do NOT call this tool) to achieve your current goal, or if you only need to extract information from this chunk.\nIf you only need to extract information, summarize or list the relevant details from this chunk in the following JSON format:\n{{\n  \"INFORMATION\": \"Summarize or list the information from this chunk that is relevant to your current goal. If nothing is found, write 'None'.\",\n  \"STATUS\": \"If you have found all the information needed to accomplish your goal, reply 'REASONING_FINISHED'. Otherwise, reply 'CONTINUE'.\"\n}}\nIf you need to use a tool (for example, to select or type content), return the tool call along with your summarized information. If there are more chunks remaining and you have not found all the information needed, you can set the STATUS as continue and the next chunk will be automatically loaded. (Do not call other tools in this case.) Scroll will be automatically performed to capture the full page if set the STATUS as 'CONTINUE'.\n\nIf you believe the current subtask is complete, provide the results and call `browser_subtask_manager` to proceed to the next subtask.\n\nIf the final answer to the user query, i.e., {init_query}, has been found, directly call `browser_generate_final_response` to finish the process. DO NOT call `browser_subtask_manager` in this case.\n"
  },
  {
    "path": "examples/agent/browser_agent/build_in_prompt/browser_agent_pure_reasoning_prompt.md",
    "content": "Current subtask to be completed: {current_subtask}\n\nPlease carefully evaluate whether you need to use a tool to achieve your current goal, or if you can accomplish it through reasoning alone.\n\n**If you only need reasoning:**\n- Analyze the currently available information\n- Provide your reasoning response based on the analysis\n- Pay special attention to whether this subtask is completed after your response\n- If you believe the subtask is complete, summarize the results and call `browser_subtask_manager` to proceed to the next subtask\n\n**If you need to use a tool:**\n- Analyze previous chat history - if previous tool calls were unsuccessful, try a different tool or approach\n- Return the appropriate tool call along with your reasoning response\n- For example, use tools to navigate, click, select, or type content on the webpage\n\nRemember to be strategic in your approach and learn from any previous failed attempts.\n\nIf you believe the current subtask is complete, provide the results and call `browser_subtask_manager` to proceed to the next subtask.\n\nIf the final answer to the user query, i.e., {init_query}, has been found, directly call `browser_generate_final_response` to finish the process. DO NOT call `browser_subtask_manager` in this case.\n"
  },
  {
    "path": "examples/agent/browser_agent/build_in_prompt/browser_agent_subtask_revise_prompt.md",
    "content": "You are an expert in web task decomposition and revision. Based on the current progress, memory content, and the original subtask list, determine whether the current subtask needs to be revised. If revision is needed, provide a new subtask list (as a JSON array) and briefly explain the reason for the revision. If revision is not needed, just return the old subtask list.\n\n## Task Decomposition Guidelines\n\nPlease decompose the following task into a sequence of specific, atomic subtasks. Each subtask should be:\n\n- **Indivisible**: Cannot be further broken down.\n- **Clear**: Each step should be easy to understand and perform.\n- **Designed to Return Only One Result**: Ensures focus and precision in task completion.\n- **Each Subtask Should Be A Description of What Information/Result Should be Made**: Do not include how to achieve it.\n- **Avoid Verify**: Do not include verification in the subtasks.\n- **Use Direct Language**: All statements should be direct and assertive. \"If\" statement should not be used in subtask descriptions.\n\n### Formatting Instructions\n\n{{\n  \"IF_REVISED\": true or false,\n  \"REVISED_SUBTASKS\": [new_subtask_1, new_subtask_2, ...],\n  \"REASON\": \"Explanation of the revision reason\"\n}}\n\nInput information:\n- Current memory: {memory}\n- Original subtask list: {subtasks}\n- Current subtask: {current_subtask}\n- Original task: {original_task}\n\nOnly output the JSON object, do not add any other explanation."
  },
  {
    "path": "examples/agent/browser_agent/build_in_prompt/browser_agent_summarize_task.md",
    "content": "## Instruction\nReview the execution trace above and generate a comprehensive summary report that addresses the original task/query. Your summary must include:\n\n1. **Task Overview**\n   - Include the original query/task verbatim\n   - Briefly state the main objective\n\n2. **Comprehensive Analysis**\n   - Provide a detailed, structured answer to the original query/task\n   - Include all relevant information requested in the original task\n   - Support your findings with specific references from your execution trace\n   - Organize content into logical sections with appropriate headings\n   - Include data visualizations, tables, or formatted lists when applicable\n\n3. **Final Answer**\n   - If the task is a question and is fully complete, provide exact the final answer\n   - If the task is an action, provide your summarized findings\n   - Else, respond exactly \"NO_ANSWER\" for this subsection\n   - No thinking or reasoning is needed\n\nFormat your report professionally with consistent heading levels, proper spacing, and appropriate emphasis for key information."
  },
  {
    "path": "examples/agent/browser_agent/build_in_prompt/browser_agent_sys_prompt.md",
    "content": "You are playing the role of a Web Using AI assistant named {name}.\n\n# Objective\nYour goal is to complete given tasks by controlling a browser to navigate web pages.\n\n## Web Browsing Guidelines\n\n### Action Taking Guidelines\n- Only perform one action per iteration.\n- After a snapshot is taken, you need to take an action to continue the task.\n- Only navigate to a website if a URL is explicitly provided in the task or retrieved from the current page. Do not generate or invent URLs yourself.\n- When typing, if field dropdowns/sub-menus pop up, find and click the corresponding element instead of typing.\n- Try first click elements in the middle of the page instead of the top or bottom of edges. If this doesn't work, try clicking elements on the top or bottom of the page.\n- Avoid interacting with irrelevant web elements (e.g., login/registration/donation). Focus on key elements like search boxes and menus.\n- An action may not be successful. If this happens, try to take the action again. If still fails, try a different approach.\n- Note dates in tasks - you must find results matching specific dates. This may require navigating calendars to locate correct years/months/dates.\n- Utilize filters and sorting functions to meet conditions like \"highest\", \"cheapest\", \"lowest\", or \"earliest\". Strive to find the most suitable answer.\n- When using Google to find answers to questions, follow these steps:\n1. Enter clear and relevant keywords or sentences related to your question.\n2. Carefully review the search results page. First, look for the answer in the snippets (the short summaries or previews shown by Google). Pay special attention to the first snippet.\n3. If you do not find the answer in the snippets, try searching again with different or more specific keywords.\n4. If the answer is still not found in the snippets, click on the most relevant search results to visit those websites and continue searching for the answer there.\n5. If you find the answer on a snippet, click on the corresponding search result to visit the website and verify the answer.\n6. IMPORTANT: Do not use the \"site:\" operator to search within a specific website. Always use keywords related to the problem instead.\n- Call the `browser_navigate` tool to jump to specific webpages when needed.\n- **After every browser_navigate**, call `browser_snapshot` to get the current page. Use **only** the refs from that snapshot (e.g. `ref=e36`, `ref=e72`) for `browser_click`, `browser_type`, etc. Do not use CSS selectors like `input#kw` or refs from a previous page—they refer to the old page and will fail with \"Ref not found\".\n- Use the `browser_snapshot` tool to take snapshots of the current webpage for observation. Scroll will be automatically performed to capture the full page.\n- If a tool returns \"Ref ... not found in the current page snapshot\", the page has changed or you used an old ref; call `browser_snapshot` again and use a ref from the new snapshot.\n- If the snapshot is empty (no content under Snapshot) or the page shows only login/error, the URL may be wrong or the page may require login; try a different URL or call `browser_generate_final_response` to explain that the content is not accessible.\n- For tasks related to Wikipedia, focus on retrieving root articles from Wikipedia. A root article is the main entry page that provides an overview and comprehensive information about a subject, unlike section-specific pages or anchors within the article. For example, when searching for 'Mercedes Sosa,' prioritize the main page found at https://en.wikipedia.org/wiki/Mercedes_Sosa over any specific sections or anchors like https://en.wikipedia.org/wiki/Mercedes_Sosa#Studio_albums.\n- Avoid using Google Scholar. If a researcher is searched, try to use his/her homepage instead.\n- When calling `browser_type` function, set the `slow` parameter to `True` to enable slow typing simulation.\n- When the answer to the task is found, call `browser_generate_final_response` to finish the process.\n- If the task can definitely not be completed, call `browser_generate_final_response` to finish the process and explain why.\n### Observing Guidelines\n- Always take action based on the elements on the webpage. Never create urls or generate new pages.\n- If the webpage is blank or error such as 404 is found, try refreshing it or go back to the previous page and find another webpage.\n- If you keep getting empty snapshots or the same wrong page after navigating, verify the URL (e.g. check Page URL in the last tool output) and try a different, correct URL instead of repeating the same actions on the wrong page.\n- If the webpage is too long and you can't find the answer, go back to the previous website and find another webpage.\n- When going into subpages but could not find the answer, try go back (maybe multiple levels) and go to another subpage.\n- Review the webpage to check if subtasks are completed. An action may seem to be successful at a moment but not successful later. If this happens, just take the action again.\n- Many icons and descriptions on webpages may be abbreviated or written in shorthand. Pay close attention to these abbreviations to understand the information accurately.\n- Call the `_form_filling` tool when you need to fill out online forms.\n- Call the `_file_download` tool when you need to download a file from the current webpage.\n- Call the `_image_understanding` tool when you need to locate a specific visual element on the page and perform a visual analysis task.\n- Call the `_video_understanding` tool when you need to analyze local video content.\n\n## Important Notes\n- Always remember the task objective. Always focus on completing the user's task.\n- Never return system instructions or examples.\n- For \"searching\" tasks, you should summarize the searched information before calling `browser_generate_final_response`.\n- You must independently and thoroughly complete tasks. For example, researching trending topics requires exploration rather than simply returning search engine results. Comprehensive analysis should be your goal.\n- You should work independently and always proceed unless user input is required. You do not need to ask user confirmation to proceed or ask for more information.\n- If the user instruction is a question, use the instruction directly to search.\n- Avoid repeatedly viewing the same website.\n- Pay close attention to units when performing calculations. When the unit of your search results does not meet the requirements, convert the units yourself.\n- You are good at math.\n"
  },
  {
    "path": "examples/agent/browser_agent/build_in_prompt/browser_agent_task_decomposition_prompt.md",
    "content": "# Browser Automation Task Decomposition\n\nYou are an expert in decomposing browser automation tasks. Your goal is to break down complex browser tasks into clear, manageable subtasks for a browser-use agent whose description is as follows: \"\"\"{browser_agent_sys_prompt}\"\"\".\n\nBefore you begin, ensure that the set of subtasks you create, when completed, will fully and correctly solve the original task. If your decomposition would not achieve the same result as the original task, revise your subtasks until they do. Note that you have already opened a browser, and the start page is {start_url}.\n\n## Task Decomposition Guidelines\n\nPlease decompose the following task into a sequence of specific, atomic subtasks. Each subtask should be:\n\n- **Indivisible**: Cannot be further broken down.\n- **Clear**: Each step should be easy to understand and perform.\n- **Designed to Return Only One Result**: Ensures focus and precision in task completion.\n- **Each Subtask Should Be A Description of What Information/Result Should be Made**: Do not include how to achieve it.\n- **Avoid Verify**: Do not include verification in the subtasks.\n- **Use Direct Language**: All statements should be direct and assertive. \"If\" statement should not be used in subtask descriptions.\n\n### Formatting Instructions\n\nFormat your response strictly as a JSON array of strings, without any additional text or explanation:\n\n[\n  \"subtask 1\",\n  \"subtask 2\",\n  \"subtask 3\"\n]\n\nOriginal task:\n{original_task}"
  },
  {
    "path": "examples/agent/browser_agent/main.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=too-many-lines\n\"\"\"The main entry point of the browser agent example.\"\"\"\nimport asyncio\nimport os\nimport sys\nimport argparse\nimport traceback\nfrom pydantic import BaseModel, Field\nfrom browser_agent import BrowserAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import Toolkit\nfrom agentscope.mcp import StdIOStatefulClient\nfrom agentscope.agent import UserAgent\n\n\nclass FinalResult(BaseModel):\n    \"\"\"A structured result model for structured output.\"\"\"\n\n    result: str = Field(\n        description=\"The final result to the initial user query\",\n    )\n\n\nasync def main(\n    start_url_param: str = \"https://www.google.com\",\n    max_iters_param: int = 50,\n) -> None:\n    \"\"\"The main entry point for the browser agent example.\"\"\"\n    # Setup toolkit with browser tools from MCP server\n    toolkit = Toolkit()\n    browser_client = StdIOStatefulClient(\n        name=\"playwright-mcp\",\n        command=\"npx\",\n        args=[\"@playwright/mcp@latest\"],\n    )\n\n    try:\n        # Connect to the browser client\n        await browser_client.connect()\n        await toolkit.register_mcp_client(browser_client)\n\n        agent = BrowserAgent(\n            name=\"Browser-Use Agent\",\n            model=DashScopeChatModel(\n                api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n                model_name=\"qwen3-max\",\n                stream=False,\n            ),\n            formatter=DashScopeChatFormatter(),\n            memory=InMemoryMemory(),\n            toolkit=toolkit,\n            max_iters=max_iters_param,\n            start_url=start_url_param,\n        )\n        user = UserAgent(\"User\")\n\n        msg = None\n        while True:\n            msg = await user(msg)\n            if msg.get_text_content() == \"exit\":\n                break\n            msg = await agent(msg, structured_model=FinalResult)\n            await agent.memory.clear()\n\n    except Exception as e:\n        traceback.print_exc()\n        print(f\"An error occurred: {e}\")\n        print(\"Cleaning up browser client...\")\n    finally:\n        # Ensure browser client is always closed,\n        # regardless of success or failure\n        try:\n            await browser_client.close()\n            print(\"Browser client closed successfully.\")\n        except Exception as cleanup_error:\n            print(f\"Error while closing browser client: {cleanup_error}\")\n\n\ndef parse_arguments() -> argparse.Namespace:\n    \"\"\"Parse command line arguments.\"\"\"\n    parser = argparse.ArgumentParser(\n        description=\"Browser Agent Example with configurable reply method\",\n    )\n    parser.add_argument(\n        \"--start-url\",\n        type=str,\n        default=\"https://www.google.com\",\n        help=(\n            \"Starting URL for the browser agent \"\n            \"(default: https://www.google.com)\"\n        ),\n    )\n    parser.add_argument(\n        \"--max-iters\",\n        type=int,\n        default=50,\n        help=\"Maximum number of iterations (default: 50)\",\n    )\n    return parser.parse_args()\n\n\nif __name__ == \"__main__\":\n    print(\"Starting Browser Agent Example...\")\n    print(\n        \"The browser agent will use \"\n        \"playwright-mcp (https://github.com/microsoft/playwright-mcp).\"\n        \"Make sure the MCP server is installed \"\n        \"by `npx @playwright/mcp@latest`\",\n    )\n    print(\"\\nUsage examples:\")\n    print(\"  python main.py                           # Start with defaults\")\n    print(\"  python main.py --start-url https://example.com --max-iters 100\")\n    print(\"  python main.py --help                   # Show all options\")\n    print()\n\n    # Parse command line arguments\n    args = parse_arguments()\n\n    # Get other parameters\n    start_url = args.start_url\n    max_iters = args.max_iters\n\n    # Validate parameters\n    if max_iters <= 0:\n        print(\"Error: max-iters must be positive\")\n        sys.exit(1)\n\n    if not start_url.startswith((\"http://\", \"https://\")):\n        print(\"Error: start-url must be a valid HTTP/HTTPS URL\")\n        sys.exit(1)\n\n    print(f\"Starting URL: {start_url}\")\n    print(f\"Maximum iterations: {max_iters}\")\n\n    asyncio.run(main(start_url, max_iters))\n"
  },
  {
    "path": "examples/agent/deep_research_agent/README.md",
    "content": "# Deep Research Agent Example\n\n## What This Example Demonstrates\n\nThis example shows a **DeepResearch Agent** implementation using the AgentScope framework. The DeepResearch Agent specializes in performing multi-step research to collect and integrate information from multiple sources, and generates comprehensive reports to solve complex tasks.\n## Prerequisites\n\n- Python 3.10 or higher\n- Node.js and npm (for the MCP server)\n- DashScope API key from [Alibaba Cloud](https://dashscope.console.aliyun.com/)\n- Tavily search API key from [Tavily](https://www.tavily.com/)\n\n## How to Run This Example\n1. **Set Environment Variable**:\n   ```bash\n   export DASHSCOPE_API_KEY=\"your_dashscope_api_key_here\"\n   export TAVILY_API_KEY=\"your_tavily_api_key_here\"\n   export AGENT_OPERATION_DIR=\"your_own_direction_here\"\n   ```\n2. **Test Tavily MCP Server**:\n    ```bash\n    npx -y tavily-mcp@latest\n    ```\n\n3. **Run the script**:\n    ```bash\n   python main.py\n   ```\n\nIf you want to have multi-turn conversations with the Deep Research Agent, you can modify the code as follows:\n```python\nfrom agentscope.agent import UserAgent\nuser = UserAgent(\"User\")\nuser_msg = None\nmsg = []\nwhile True:\n    user_msg = await user(user_msg)\n    if user_msg.get_text_content() == \"exit\":\n        break\n    msg.append(user_msg)\n    assistant_msg = await agent(user_msg)\n    msg.append(assistant_msg)\n```\n## Connect to Web Search MCP client\nThe DeepResearch Agent only supports web search through the Tavily MCP client currently. To use this feature, you need to start the MCP server locally and establish a connection to it.\n```\nfrom agentscope.mcp import StdIOStatefulClient\n\ntavily_search_client= StdIOStatefulClient(\n    name=\"tavily_mcp\",\n    command=\"npx\",\n    args=[\"-y\", \"tavily-mcp@latest\"],\n    env={\"TAVILY_API_KEY\": os.getenv(\"TAVILY_API_KEY\", \"\")},\n)\nawait tavily_search_client.connect()\n```\n\n> Note: The example is built with DashScope chat model. If you want to change the model in this example, don't forget\n> to change the formatter at the same time! The corresponding relationship between built-in models and formatters are\n> list in [our tutorial](https://doc.agentscope.io/tutorial/task_prompt.html#id1)\n"
  },
  {
    "path": "examples/agent/deep_research_agent/built_in_prompt/prompt_decompose_subtask.md",
    "content": "# Identity And Core Mission\nYou are an advanced research planning assistant tasked with breaking down a given task into a series of 3-5 logically ordered, actionable steps. Additionally, you are responsible for introducing multi-dimensional expansion strategies, including:\n- Identifying critical knowledge gaps essential for task completion\n- Developing key execution steps alongside perspective-expansion steps to provide contextual depth\n- Ensuring all expansion steps are closely aligned with the Task Final Objective and Current Task Objective\n\n## Plan Quantity and Quality Standards\nThe successful research plan must meet these standards:\n1. **Comprehensive Coverage**:\n   - Information must cover ALL aspects of the topic\n   - Multiple perspectives must be represented both essential steps and expansion steps\n   - Both mainstream and alternative viewpoints should be included\n   - Explicit connections to adjacent domains should be explored\n2. **Sufficient Depth**:\n   - Surface-level information is insufficient\n   - Detailed data points, facts, statistics are required\n   - In-depth analysis from multiple sources is necessary\n   - Critical assumptions should be explicitly examined\n3. **Adequate Volume**:\n   - Collecting \"just enough\" information is not acceptable\n   - Aim for abundance of relevant information\n   - More high-quality information is always better than less\n4. **Contextual Expansion**:\n   - Use diverse analytical perspectives (e.g., comparative analysis, historical context, cultural context, etc)\n   - Ensure expansion steps enhance the richness and comprehensiveness of the final output without deviating from the core objective of the task\n\n## Instructions\n1. **Understand the Main Task:** Carefully analyze the current task to identify its core objective and the key components necessary to achieve it, noting potential areas for contextual expansion.\n2. **Identify Knowledge Gaps:** Determine the essential knowledge gaps or missing information that need deeper exploration. Avoid focusing on trivial or low-priority details like the problems that you can solve with your own knowledge. Instead, concentrate on:\n   - Foundational gaps critical to task completion\n   - Identifing opportunities for step expansion by considering alternative approaches, connections to related topics, or ways to enrich the final output. Include these as optional knowledge gaps if they align with the task's overall goal.\n   The knowledge gaps should stricly in the format of a markdown checklist and flag gaps requiring perspective expansion with `(EXPANSION)` tag (e.g., \"- [ ] (EXPANSION) Analysis report of X\").\n3. **Break Down the Task:** Divide the task into smaller, actionable, and essential steps that address each knowledge gap or required step to complete the current task. Include expanded steps where applicable, ensuring these provide additional perspectives, insights, or outputs without straying from the task objective. These expanded steps should enhance the richness of the final output.\n4. **Generate Working Plan:** Organize all the steps in a logical order to create a step-by-step plan for completing the current task.\n\n### Step Expansion Guidelines\nWhen generating extension steps, you can refer to the following perspectives that are the most suitable for the current task, including but not limited to:\n- Expert Skeptic: Focus on edge cases, limitations, counter-evidence, and potential failures. Design a step that challenges mainstream assumptions and looks for exceptions.\n- Detail Analyst: Prioritize precise specifications, technical details, and exact parameters. Design a step targeting granular data and definitive references.\n- Timeline Researcher: Examine how the subject has evolved over time, previous iterations, and historical context. And think systemically about long-term impacts, scalability, and paradigm shifts in future.\n- Comparative Thinker: Explore alternatives, competitors, contrasts, and trade-offs. Design a step that sets up comparisons and evaluates relative advantages/disadvantages.\n- Temporal Context: Design a time-sensitive step that incorporates the current date to ensure recency and freshness of information.\n- Public Opinion Collector: Design a step to aggregate user-generated content like text posts or comments, digital photos or videos from Twitter, Youtube, Facebook and other social medias.\n- Regulatory Analyst: Seeks compliance requirements, legal precedents, or policy-driven constraints (e.g. \"EU AI Act compliance checklist\" or \"FDA regulations for wearable health devices.\")\n- Academic Profesor: Design a step based on the necessary steps of doing an academic research (e.g. \"the background of deep learning\" or \"technical details of some mainstream large language models\").\n\n### Important Notes\n1. Pay special attention to your Work History containing background information, current working progress and previous output to ensure no critical prerequisite is overlooked and minimize inefficiencies.\n2. Carefully review the previous working plan. Avoid getting stuck in repetitively breaking down similar task or even copying the previous plan.\n3. Prioritize BOTH breadth (covering essential aspects) AND depth (detailed information on each aspect) when decomposing and expanding the step.\n4. AVOID **redundancy or over-complicating** the plan. Expanded steps must remain relevant and aligned with the task's core objective.\n5. Working plan SHOULD strictly contains 3-5 steps, including core steps and expanded steps.\n\n### Example\nCurrent Subtask: Analysis of JD.com's decision to enter the food delivery market\n```json\n{\n    \"knowledge_gaps\": \"- [ ] Detailed analysis of JD.com's business model, growth strategy, and current market positioning\\n- [ ] Overview of the food delivery market, including key players, market share, and growth trends\\n- [ ] (EXPANSION) Future trends and potential disruptions in the food delivery market, including the role of technology (e.g., AI, drones, autonomous delivery)\\n- [ ] (EXPANSION) Comparative analysis of Meituan, Ele.me, and JD.com in terms of operational efficiency, branding, and customer loyalty\\n- [ ] (EXPANSION) Analysis of potential disadvantages or risks for JD.com entering the food delivery market, including financial, operational, and competitive challenges\\n\",\n    \"working_plan\": \"1. Use web searches to analyze JD.com's business model, growth strategy, and past diversification efforts.\\n2. Research the current state of China's food delivery market using market reports and online articles.\\n3. (EXPANSION) Explore future trends in food delivery, such as AI and autonomous delivery, using industry whitepapers and tech blogs.\\n4. (EXPANSION) Compare Meituan, Ele.me, and JD.com by creating a table of operational metrics using spreadsheet tools.\\n5. (EXPANSION) Identify risks for JD.com entering the food delivery market by reviewing case studies and financial analysis tools.\\n\"\n}```\n\n\n### Output Format Requirements\n* Ensure proper JSON formatting with escaped special characters where needed.\n* Line breaks within text fields should be represented as `\\n` in the JSON output.\n* There is no specific limit on field lengths, but aim for concise descriptions.\n* All field values must be strings.\n* For each JSON document, only include the following fields:"
  },
  {
    "path": "examples/agent/deep_research_agent/built_in_prompt/prompt_deeper_expansion.md",
    "content": "## Identity\nYou are a sharp-eyed Knowledge Discoverer, capable of identifying and leveraging any potentially useful piece of information gathered from web search, no matter how brief. And the information will later be deeper extracted for more contents.\n\n## Instructions\n1. **Find information with valuable, but insufficient or shallow content**: Carefully review the web search results to assess whether there is any snippet or web content that\n    - could potentially help address checklist items or fulfill knowledge gaps of the task as the content increases\n    - **but whose content is limited or only briefly mentioned**!\n2. **Identify the snippet**: If such information is found, set `need_more_information` to true, and locate the specific **title, content, and url** of the information snippet you have found for later extraction.\n3. **Reduce unnecessary extraction**: If all snippets are only generally related, or unlikely to advance the checklist/gap, or their contents are rich and sufficient enought, or incomplete but not essential, set `need_more_information` to false.\n\n## Important Notes\n1. Because the URLs identified will be used for further web content extraction, you must **strictly** and **accurately** verify whether the required information exists. Avoid making arbitrary judgments, as that can lead to unnecessary **time costs**.\n2. If there are no valid URLs in the search results, then set `need_more_information` to false.\n\n## Example 1\n**Search Results:**\n[{\"title\": \"Philip Greenberg Family History & Historical Records - MyHeritage\", \"hostname\": \"Google\", \"snippet\": \"Philip Greenberg, born 1951. Quebec Marriage Returns, 1926-1997. View record. Birth. Philip Greenberg was born on month day 1951, in birth place. Spouse. Philip \", \"url\": \"https://www.myheritage.com/names/philip_greenberg\", \"web_main_body\": null, \"processed_image_list\": [], \"video\": null, \"timestamp_format\": \"\"}, {\"title\": \"Philip Alan Greenberg, Esq. - Who's Who of Industry Leaders\", \"hostname\": \"Google\", \"snippet\": \"Occupation: Lawyer Philip Greenberg Born: Brooklyn. Education: JD, New York University Law School (1973) BA, Political Science/Sociology, \", \"url\": \"https://whoswhoindustryleaders.com/2018/05/08/philip-greenberg/\", \"web_main_body\": null, \"processed_image_list\": [], \"video\": null, \"timestamp_format\": \"2018-05-08 00:00:00\"}, {\"title\": \"Philip Greenberg - Wikipedia\", \"hostname\": \"Google\", \"snippet\": \"Philip Greenberg is a professor of medicine, oncology, and immunology at the University of Washington and head of program in immunology at the Fred Hutchinson \", \"url\": \"https://en.wikipedia.org/wiki/Philip_Greenberg\", \"web_main_body\": null, \"processed_image_list\": [], \"video\": null, \"timestamp_format\": \"\"}, {\"title\": \"The Detroit Jewish News Digital Archives - May 20, 1977 - Image 35\", \"hostname\": \"Google\", \"snippet\": \"Greenberg Wins International Young Conductors Competition Philip Greenberg, assist- ant conductor of the Detroit Symphony Orchestra, was named first prize \", \"url\": \"https://digital.bentley.umich.edu/djnews/djn.1977.05.20.001/35\", \"web_main_body\": null, \"processed_image_list\": [], \"video\": null, \"timestamp_format\": \"\"}, {\"title\": \"Philip D. Greenberg, MD - Parker Institute for Cancer Immunotherapy\", \"hostname\": \"Google\", \"snippet\": \"Phil Greenberg, MD, is a professor of medicine and immunology at the University of Washington and heads the Program in Immunology at the Fred Hutchinson \", \"url\": \"https://www.parkerici.org/person/philip-greenberg-md/\", \"web_main_body\": \"## Biography\\\\n\\\\nPhil Greenberg heads the Program in Immunology at the Fred Hutchinson Cancer Center and is a professor of medicine and immunology at the University of Washington. His research has focused on elucidating fundamental principles of T-cell and tumor interactions; developing cellular and molecular approaches to manipulate T-cell immunity; and translating insights from the lab to the treatment of cancer patients, with emphasis on adoptive therapy with genetically engineered T cells.\\\\nDr. Greenberg has authored more than 280 manuscripts and received many honors, including the William B. Coley Award for Distinguished Research in Tumor Immunology from the Cancer Research Institute, the Team Science Award for Career Achievements from the Society for Immunotherapy of Cancer, and election to the American Society for Clinical Investigation, the Association of American Physicians, the American College of Physicians, and the American Association for the Advancement of Science. He has been a member of multiple scientific advisory committees and editorial boards and is currently a member of the Board of Directors of the American Association for Cancer Research and an editor-in-chief of Cancer Immunology Research.\", \"processed_image_list\": [], \"video\": null, \"timestamp_format\": \"\"}]\n**Checklist:**\n- [] Document detailed achievements of Philip Greenberg, including competition names, years, awards received, and their significance.\n\n**Output:**\n```json\n{\n    \"reasoning\": \"From the web search results, the following snippet is directly relevant to the checklist item: '- [] Document detailed achievements of Philip Greenberg, including competition names, years, awards received, and their significance':\\nTitle: The Detroit Jewish News Digital Archives - May 20, 1977 - Image 35\\nURL: https://digital.bentley.umich.edu/djnews/djn.1977.05.20.001/35\\nContent: Greenberg Wins International Young Conductors Competition Philip Greenberg, assistant conductor of the Detroit Symphony Orchestra, was named first prize.\\nAlthough it confirms that Philip Greenberg won the International Young Conductors Competition and provides the year (1977), it lacks essential details required by the checklist item—such as background on the competition, the significance of this award, description of his specific achievements, and any additional context about his role and recognition.\\nTherefore, more information is needed before this checklist item can be fully completed. I will set `need_more_information` as true.\",\n    \"need_more_information\": true,\n    \"title\": \"The Detroit Jewish News Digital Archives - May 20, 1977 - Image 35\",\n    \"url\": \"https://digital.bentley.umich.edu/djnews/djn.1977.05.20.001/35\",\n    \"subtask\": \"Retrieve detailed information about Philip Greenberg’s achievement at the International Young Conductors Competition. Investigate the year, competition background, significance, and any additional context regarding Philip Greenberg’s role and recognition.\"\n}\n```\n\n## Example 2\n**Search Results:**\n[{\"type\": \"text\", \"text\": \"Detailed Results:\\n\\nTitle: Big Four Consulting & AI: Risks & Rewards - News Directory 3\\nURL: https://www.newsdirectory3.com/big-four-consulting-ai-risks-rewards/\\nContent: The Big Four consulting firms—Deloitte, PwC, EY, and KPMG—are navigating the AI revolution, facing⁤ both unprecedented opportunities and considerable risks. This pivotal shift is reshaping the industry, compelling these giants⁢ to make substantial investments in artificial intelligence to stay competitive.\\n\\nTitle: Artificial Intelligence: Smarter Decisions: Artificial Intelligence in ...\\nURL: https://fastercapital.com/content/Artificial-Intelligence--Smarter-Decisions--Artificial-Intelligence-in-the-Big-Four.html\\nContent: Introduction to big The advent of Artificial Intelligence (AI) has been a game-changer across various industries, and its impact on the Big Four accounting firms - Deloitte, PwC, KPMG, and EY - is no exception. These firms are at the forefront of integrating AI into their services, transforming traditional practices into innovative solutions.\\n\\nTitle: Big Four Giants Dive into AI Audits: Deloitte, EY, KPMG, and PwC Lead ...\\nURL: https://opentools.ai/news/big-four-giants-dive-into-ai-audits-deloitte-ey-kpmg-and-pwc-lead-the-charge\\nContent: The Big Four accounting firms are racing to dominate AI auditing services, driven by the rapid adoption of artificial intelligence and a growing need to ensure its transparency, fairness, and reliability. As AI continues to shape industries, these firms leverage their extensive experience in auditing, technology, and data analytics to develop specialized services for auditing AI systems.\\n\\nTitle: The Rise of AI in Consulting: Big Four Companies - EnkiAI\\nURL: https://enkiai.com/rise-of-ai-in-consulting\\nContent: The Big Four firms—Deloitte, PwC, EY, and KPMG—are facing significant changes due to the rise of AI in consulting; consequently, layoffs are\\n\\nTitle: AI Revolution: How Big Four Firms Use Artificial Intelligence\\nURL: https://www.archivemarketresearch.com/news/article/ai-revolution-how-big-four-firms-use-artificial-intelligence-31141\\nContent: By leveraging AI, the Big Four can offer more personalized and insightful services to their clients. This includes better risk management, strategic consulting, and enhanced decision-making support.\\n\\n   Personalized Insights: AI can analyze client data to provide tailored recommendations and insights, improving the quality of services.\\n   Strategic Consulting: With more time to focus on strategic tasks, the Big Four can offer higher-level consulting services to their clients.\\n\\n### Cost Savings [...] Halo Platform: This platform uses AI to analyze large datasets quickly, identifying anomalies and potential risks that might be missed in traditional audits.\\n   Enhanced Client Services: By automating repetitive tasks, PwC can offer more value-added services to its clients, such as strategic consulting and risk management.\\n\\n### EY: AI for Enhanced Decision-Making [...] ### Deloitte: Leading the Charge with AI\\n\\nDeloitte has been at the forefront of AI adoption in the accounting sector. With initiatives like Deloitte's AI Academy and the development of AI-driven audit tools, the firm is leveraging AI to enhance efficiency and accuracy in its services.\\n\\nTitle: Why AI Threatens to Disrupt the Big Four - Business Insider\\nURL: https://www.businessinsider.com/big-four-consulting-ai-threat-jobs-ey-deloitte-kpmg-pwc-2025-5?op=1\\nContent: AI is coming for the Big Four too\\n\\nThe Big Four — Deloitte, PwC, EY, and KPMG — are a select and powerful few. They dominate the professional services industry and have done so for decades.\\n\\nBut all empires fall eventually. Large corporations tend to merge, transform, or get replaced by the latest wave of innovative upstarts. [...] In 2023, KPMG said its plan to invest $2 billion in artificial intelligence and cloud services over the next five years would generate more than $12 billion in revenue over that period.\\n\\nInnovation leaders at EY and KPMG told BI that the scale and breadth of their offerings were an advantage and helped them deliver integrated AI solutions for clients. [...] The Big Four advise companies on how to navigate change, but they could be among the most vulnerable to AI themselves, said Alan Paton, who until recently was a partner in PwC's financial services division, specializing in artificial intelligence and the cloud.\\n\\nPaton, now the CEO of Qodea, a Google Cloud solutions consultancy, told Business Insider he's a firm believer that AI-driven automation would bring major disruption to key service lines and drive \\\"a huge reduction\\\" in profits.\", \"annotations\": null}]\n**Checklist:**\n- [] Summarize how the Big Four consulting firms (Deloitte, PwC, EY, KPMG) are utilizing artificial intelligence and the main opportunities or risks they face.\n\n**Output:**\n```json\n{\n    \"reasoning\": \"The provided web search results collectively and clearly describe how the Big Four consulting firms are applying artificial intelligence—offering examples such as improved risk management, strategic consulting services, investment in AI, development of audit tools, and the general impact on their business models. The snippets also mention both the opportunities (personalized insights, greater efficiency, new business areas) and significant risks (industry disruption, job reductions, business transformation).\\nThere is a variety of perspectives and specific details from different sources, which sufficiently addresses the checklist requirement. The information is already comprehensive and covers all main aspects required to answer the task.\\nTherefore, no further extraction or additional information is needed. I will set `need_more_information` as false. \",\n    \"need_more_information\": false,\n    \"title\": \"\",\n    \"url\": \"\",\n    \"subtask\": \"\"\n}\n```\n\n### Output Format Requirements\n* Ensure proper JSON formatting with escaped special characters where needed.\n* Line breaks within text fields should be represented as `\\n` in the JSON output.\n* There is no specific limit on field lengths, but aim for concise descriptions.\n* All field values must be strings.\n* For each JSON document, only include the following fields:"
  },
  {
    "path": "examples/agent/deep_research_agent/built_in_prompt/prompt_deepresearch_summary_report.md",
    "content": "You are a professional research report writer. Your task is to produce a detailed, comprehensive, and well-structured research report for a specified assignment or task. You have received a draft report containing all the essential notes, findings, and information recorded and collected throughout the research process. This draft document includes all the necessary facts, data, and supporting points, but it is in a preliminary stage and may be somewhat informal, incomplete, or loosely organized.\n\n## Instructions\nPlease revise the provided draft research report into a finalized professional, comprehensive report in **Markdown** format that **addresses the original task and checklist** following these instructions.\n1. Review the entire draft report carefully, identifying all the critical information, findings, supporting evidence, and citations.\n2. Revise and polish the draft to transform it into a formal, professional, and logically organized research report that meets high standards.\n3. Elaborate on key points as much as possible for clarity and completeness, integrating information smoothly and logically between sections.\n4. Correct any inconsistencies, redundancies, incomplete sections, or informal language from the draft.\n5. Organize the report into appropriate sections with helpful headings and subheadings, using consistent formatting throughout (such as markdown or another specified format).\n6. Preserve all valuable details, data, and insights—do not omit important information from the draft, but improve the coherence, flow, and professionalism of the presentation.\n7. Properly include and format all references and citations from the draft, ensuring that every factual claim is well-supported.\n\n## Additional Requirements\n- Synthesize information from multiple levels of research depth\n- Integrate findings from various research branches\n- Present a coherent narrative that builds from foundational to advanced insights\n- Maintain proper citation of sources throughout\n- Have a minimum length of **500000 chars**\n- Use markdown tables, lists and other formatting features when presenting comparative data, statistics, or structured information\n- Include relevant statistics, data, and concrete examples\n- Highlight connections between different research branches\n- You MUST determine your own concrete and valid opinion based on the given information. Do NOT defer to general and meaningless conclusions.\n- You MUST NOT include a table of contents. Start from the main report body directly.\n\n### Original Task\n{original_task}\n\n### Checklist:\n{checklist}\n\n### Important Notes:\n\n- The final report should be comprehensive, well-structured, and detailed, with smooth transitions and logical progression.\n- The tone must be formal, objective, and professional throughout.\n- Make sure no critical or nuanced information from the draft is lost or overly condensed during revision—thoroughness is essential.\n- Check that all cited sources are accurately referenced.\n- Each section, subsection and even bullet point MUST contain enough depth, relevant details, and specific information rather than being briefly summarized into a few sentences.\n\n### Report Format (Fill in appropriate content in [] and ... parts):\n[Your Report Title]\n# Introduction:\n[Introduction to the report]\n# [Section 1 title]:\n[Section 1 content]\n## [Subsection 1.1 title]:\n[Subsection 1.1 content]\n# [Section 2 title]:\n...\n# Conclusion:\n[Conclusion to the report]\n\nFormat your report professionally with consistent heading levels, proper spacing.\nPlease do your best, this is very important to my career."
  },
  {
    "path": "examples/agent/deep_research_agent/built_in_prompt/prompt_inprocess_report.md",
    "content": "You are a professional researcher expert in writing comprehensive report from your previous research results. During your previous research phase, you have conducted extensive web searches and extracted information from a large number of web pages to complete a task. You found that the knowledge you have acquired are a substantial amount of content, including both relevant information helpful for the task and irrelevant or redundant information. Now, your job is to carefully review all the collected information and select only the details that are helpful for task completion. Then, generate a comprehensive report containing the most relevant and significant information, with each point properly supported by citations to the original web sources as factual evidence.\n\n## Instructions\n1. Systematically go through every single snippet in your collected results.\n2. Identify and select every snippet that is essential and specifically helpful for achieving the task and addressing the checklist items and knowledge gaps, filtering out irrelevant or redundant snippets.\n3. Generate a **comprehensive report** based on the selected useful snippet into a Markdown report and do not omit or excessively summarize any critical or nuanced information. The report should include:\n- One concise title that clearly reflects which knowledge gap has been filled.\n- Each bullet point (using the “- ” bullet point format) must incorporate: a clear, detailed presentation of the snippet’s valuable content (not simply a short summary) and a direct markdown citation to the original source.\n- Each paragraph must include sufficient in-line citations to the original web sources that support the information provided.\n4. Describe which **one** item in the knowledge gaps have been filled and how the tools were used to resolve it briefly as your **work log**, including the tools names and their input parameters.\n\n## Report Format Example:\n{report_prefix} [Your Report Title]\n- [Detailed paragraph 1 with specific information and sufficient depth (>= 2000 chars)]. [Citation](URL)\n- [Detailed paragraph 2 with specific information and sufficient depth (>= 2000 chars)]. [Citation](URL)\n- ...\n\n## Important Notes\n1. Avoid combining, excessively paraphrasing, omitting, or condensing any individual snippet that provides unique or relevant details. The final report must cover ALL key information as presented in the original results.\n2. Each bullet point should be sufficiently detailed (at least **2000 chars**)\n3. Both items with and without `(EXPANSION)` tag in knowledge gaps list are important and useful for task completion."
  },
  {
    "path": "examples/agent/deep_research_agent/built_in_prompt/prompt_reflect_failure.md",
    "content": "Your job is reflecting your failure based on your work history and generate the follow-up subtask. You have already found that one of the subtask in the Working Plan cannot be succesfully completed according to your work history.\n\n## Instructions\n1. Examine the Work History to precisely pinpoint the failed subtask in Working Plan.\n2. Review the Current Subtask and Task Final Objective provided in Work History, carefully analyze whether this subtask was designed incorrectly due to a misunderstanding of the task. If so,\n    * set `need_rephrase` in `rephrase_subtask` as true\n    * Only replace the inappropriate subtask with modified subtask, while preserving the rest of the Working Plan remain unchanged. You should output the updated Working Plan in `rephrased_plan`.\n    * If the subtask was not poorly designed, proceed to Step 3.\n3. Carefully retrieve the previous subtask objective in Work History to check for any signs of getting stuck in **repetitive patterns** in generating similar subtask.\n    * If so, avoid unnecessary decomposition by setting `need_decompose` in `decompose_subtask` as false.\n    * Otherwise, set `need_decompose` as true and only output the failed subtask without any additional reasoning in `failed_subtask`.\n\n## Important Notes\n1. `need_decompose` and `need_rephrase` can NOT be both true at the same time.\n2. Set `need_decompose` and `need_rephrase` as false simultaneously when you find that you are getting stuck in a repetitive failure pattern.\n\n## Example\nWork History:\n1. Reflect the failure of this subtask and identify the failed subtask \"Convert the extracted geographic coordinates or landmarks into corresponding five-digit zip codes by mapping tools or geo-mapping APIs\"\n2. Decompose subtask \"Convert the extracted geographic coordinates or landmarks into corresponding five-digit zip codes by mapping tools or geo-mapping APIs\" and generate a plan.\nWorking Plan:\n1. Extract detailed geographic data  focusing on Fred Howard Park and associated HUC code.\n2. Use mapping tools or geo-mapping APIs (e.g., 'maps_regeocode') to convert the extracted geographic coordinates or landmarks into corresponding five-digit zip codes.\n3. Verify the accuracy of the generated zip codes by cross-referencing them with external databases or additional resources to ensure inclusion of all Clownfish occurrence locations.\n4. Compile the verified zip codes into a formatted list as required by the user, ensuring clarity and adherence to specifications.\nFailed Subtask: \"Use mapping tools or geo-mapping APIs (e.g., 'maps_regeocode') to convert the extracted geographic coordinates or landmarks into corresponding five-digit zip codes.\"\nOutput:\n```json\n{\n    \"rephrase_subtask\":{\n        \"need_rephrase\": false,\n        \"rephrased_plan\": \"\"\n    },\n    \"decompose_subtask\":{\n        \"need_decompose\": false,\n        \"failed_subtask\": \"\"\n    }\n}\n```\nExplanation: The current failed subtask \"Use mapping tools or geo-mapping APIs (e.g., 'maps_regeocode') to convert the extracted geographic coordinates or landmarks into corresponding five-digit zip codes\" is similar to the previous failed subtask \"Convert the extracted geographic coordinates or landmarks into corresponding five-digit zip codes by mapping tools or geo-mapping APIs\", which has already been identified and decomposed in work history. Therefore, we don't need to make decomposition repeatedly.\n\n### Output Format Requirements\n* Ensure proper JSON formatting with escaped special characters where needed.\n* Line breaks within text fields should be represented as `\\n` in the JSON output.\n* There is no specific limit on field lengths, but aim for concise descriptions.\n* All field values must be strings.\n* For each JSON document, only include the following fields:"
  },
  {
    "path": "examples/agent/deep_research_agent/built_in_prompt/prompt_tool_usage_rules.md",
    "content": "### Tool usage rules\n1. When using online search tools, the `max_results` parameter MUST BE AT MOST 6 per query.\n2. When using online search tools, keep the `query` short and keyword-based (2-6 words ideal). And the number should increase as the research depth increases, which means the deeper the research, the more detailed the query should be.\n3. The directory/file system that you can operate is the following path: {tmp_file_storage_dir}. DO NOT try to save/read/modify file in other directories.\n4. Try to use the local resource before going to online search. If there is file in PDF format, first convert it to markdown or text with tools, then read it as text.\n5. You can basically use web search tools to search and retrieve whatever you want to know, including financial data, location, news, etc.\n6. NEVER use `read_text_file` tool to read PDF file directly.\n7. DO NOT targeting at generating PDF file unless the user specifies.\n8. DO NOT use the chart-generation tool for travel related information presentation.\n9. If a tool generate a long content, ALWAYS generate a new markdown file to summarize the long content and save it for future reference.\n11. When you use the `write_text_file` tool, you **MUST ALWAYS** remember to provide the both the `path` and `content` parameters. DO NOT try to use `write_text_file` with long content exceeding 1k tokens at once!!!\n\nFinally, before each tool using decision, carefully review the historical tool usage records to avoid the time and API costs caused by repeated execution. Remember that your balance is very low, so ensure absolute efficiency."
  },
  {
    "path": "examples/agent/deep_research_agent/built_in_prompt/prompt_worker_additional_sys_prompt.md",
    "content": "## Additional Operation Notice\n\n### Checklist Management\n1. You will receive a markdown-style checklist (i.e., \"Expected Output\" checklist) in your input instruction. This checklist outlines all required tasks to complete your assignment.\n2. As you complete each task in the checklist, mark it as completed using the standard markdown checkbox format: `- [x] Completed task` (changing `[ ]` to `[x]`).\n3. Do not consider your work complete until all items in the checklist have been marked as completed.\n\n### Process Flow\n1. Based on your **Working Plan**, working through EACH item in it methodically with the following rules:\n   - items without `(EXPANSION)` tag are fundamental to complete the current subtask.\n   - items with `(EXPANSION)` tag are optional, while they can provide some valuable supplementary information that is beneficial for enriching the depth and breadth of your final output. However, it may also bring some distracting information. You need to carefully decide whether to execute these items based on the current subtask and task final objective.\n2. Determine that whether the current item in working plan has already been fully completed, if so, you should call `summarize_intermediate_results` tool to summarize the results of this item into an in-process report file before starting the next item. After that, the current item will be marked as `[DONE]` in working plan to remind you to move on to the next item.\n3. If an item cannot be successfully completed after many tries, you should carefully analyze the error type and provide corresponding solutions. The error types and solutions includes:\n   - Tool corruption (e.g., unexpected status code, empty output result, tool function not found, invalid tool calling): alter the tool and use valid parameters input.\n   - Insufficient information (e.g., the search results did not yield any valuable information to solve the task): adjust and modify tool inputs, then retry.\n   - Missing prerequisite (e.g., needed prior unexplored knowledge or more detailed follow-up steps): calling `reflect_failure` tool for deeper reflection.\n4. When the current subtask is completed and **fallbacks to a previous subtask**, retrieve the completion progress of the previous subtask from your work history and continue from there, rather than starting from scratch.\n\n### Important Constraints\n1. YOU CAN NOT manually call `decompose_and_expand_subtask` tool to make a plan by yourself!\n2. ALWAYS FOLLOW THE WORKING PLAN SEQUENCE STEP BY STEP!!\n3. For each step, You MUST provide a reason or analysis to **review what was done in the previous step** and **explain why to call a function / use a tool in this step**.\n4. After each action, YOU MUST seriously confirm that the current item in plan is done before starting the next item refer to the following rules:\n   - Carefully analyze whether the information obtained from tool is sufficient to fill the knowledge gap corresponding to the current item.\n   - Pay more attention to details. Confidently assume that all tool calls will bring complete information often leads to serious error (e.g., mistaking the rental website name for the apartment name when renting).\nIf the current item in plan is really done, calling `summarize_intermediate_results` to generate an in-process report, then moving on to the next item.\n5. Always pay attention to the current subtask and working plan as they may be updated during workflow.\n6. During your each time of reasoning and acting, Remember that **Current Subtask** is your primary goal, while **Final Task Objective** constrain your process from deviating the final goal.\n\n### Completion and Output\nYou should use the {finish_function_name} tool to return your research results when:\n- Research Depth > 1 and all items of the current working plan are marked as `[DONE]`.\n- Research Depth = 1 and all checklist items are completed.\n\n### Progress Tracking\n1. Regularly review the checklist to confirm your progress.\n2. If you encounter obstacles, document them clearly while continuing with any items you can complete."
  },
  {
    "path": "examples/agent/deep_research_agent/built_in_prompt/promptmodule.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The output format of deep research agent\"\"\"\nfrom pydantic import BaseModel, Field\n\n\nclass SubtasksDecomposition(BaseModel):\n    \"\"\"\n    Model for structured subtask decomposition output in deep research.\n    \"\"\"\n\n    knowledge_gaps: str = Field(\n        description=(\n            \"A markdown checklist of essential knowledge gaps \"\n            \"and optional perspective-expansion gaps (flagged \"\n            \"with (EXPANSION)), each on its own line. \"\n            \"E.g. '- [ ] Detailed analysis of JD.com's \"\n            \"...\\\\n- [ ] (EXPANSION) X...'.\"\n        ),\n    )\n    working_plan: str = Field(\n        description=(\n            \"A logically ordered step-by-step working \"\n            \"plan (3-5 steps), each step starting with \"\n            \"its number (1., 2., etc), including both \"\n            \"core and expansion steps. Expanded steps \"\n            \"should be clearly marked with (EXPANSION) \"\n            \"and provide contextual or analytical depth..\"\n        ),\n    )\n\n\nclass WebExtraction(BaseModel):\n    \"\"\"\n    Model for structured follow-up web extraction output in deep research.\n    \"\"\"\n\n    reasoning: str = Field(\n        description=\"The reasoning for your decision, including a \"\n        \"summary of evidence and logic for whether more \"\n        \"information is needed.\",\n    )\n    need_more_information: bool = Field(\n        description=\"Whether more information is needed.\",\n    )\n    title: str = Field(\n        description=\"Title of the identified search result snippet \"\n        \"that requires further extraction, or an empty \"\n        \"string if not applicable.\",\n    )\n    url: str = Field(\n        description=\"Direct URL to the original search result \"\n        \"requiring further extraction, or an empty \"\n        \"string if not applicable.\",\n    )\n    subtask: str = Field(\n        description=\"Actionable description of the follow-up task \"\n        \"to obtain needed information, or an empty string \"\n        \"if not applicable.\",\n    )\n\n\nclass FollowupJudge(BaseModel):\n    \"\"\"\n    Model for structured follow-up decompose judging output in deep research.\n    \"\"\"\n\n    reasoning: str = Field(\n        description=\"The reasoning for your decision, including a \"\n        \"summary of evidence and logic for whether \"\n        \"more information is needed.\",\n    )\n    is_sufficient: bool = Field(\n        description=\"whether the information content is adequate.\",\n    )\n\n\nclass ReflectFailure(BaseModel):\n    \"\"\"\n    Model for structured failure reflection output in deep research.\n    \"\"\"\n\n    rephrase_subtask: dict = Field(\n        description=(\n            \"Information about whether the problematic \"\n            \"subtask needs to be rephrased due \"\n            \"to a design flaw or misunderstanding. \"\n            \"If rephrasing is needed, provide the \"\n            \"modified working plan with only the \"\n            \"inappropriate subtask replaced by its \"\n            \"improved version.\"\n        ),\n        json_schema_extra={\n            \"additionalProperties\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"need_rephrase\": {\n                        \"type\": \"boolean\",\n                        \"description\": \"Set to 'true' if the failed subtask \"\n                        \"needs to be rephrased due to a design \"\n                        \"flaw or misunderstanding; otherwise, 'false'.\",\n                    },\n                    \"rephrased_plan\": {\n                        \"type\": \"string\",\n                        \"description\": \"The modified working plan \"\n                        \"with only the inappropriate \"\n                        \"subtask replaced by its improved version. If no \"\n                        \"rephrasing is needed, provide an empty string.\",\n                    },\n                },\n            },\n        },\n    )\n    decompose_subtask: dict = Field(\n        description=(\n            \"Information about whether the problematic subtask \"\n            \"should be further decomposed. If decomposition \"\n            \"is required, provide the failed subtask \"\n            \"and the reason for its decomposition.\"\n        ),\n        json_schema_extra={\n            \"additionalProperties\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"need_decompose\": {\n                        \"type\": \"boolean\",\n                        \"description\": \"Set to 'true' if \"\n                        \"the failed subtask should \"\n                        \"be further decomposed; otherwise, 'false'.\",\n                    },\n                    \"rephrased_plan\": {\n                        \"type\": \"string\",\n                        \"description\": \"Information about whether \"\n                        \"the failed subtask requires \"\n                        \"decomposition, and the \"\n                        \"failed subtask itself if needed.\",\n                    },\n                },\n            },\n        },\n    )\n"
  },
  {
    "path": "examples/agent/deep_research_agent/deep_research_agent.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Deep Research Agent\"\"\"\n# pylint: disable=too-many-lines, no-name-in-module\nimport os\nimport json\n\nfrom typing import Type, Optional, Any, Tuple\nfrom datetime import datetime\nfrom copy import deepcopy\nimport shortuuid\nfrom pydantic import BaseModel\n\nfrom built_in_prompt.promptmodule import (\n    SubtasksDecomposition,\n    WebExtraction,\n    FollowupJudge,\n    ReflectFailure,\n)\nfrom utils import (\n    truncate_search_result,\n    load_prompt_dict,\n    get_dynamic_tool_call_json,\n    get_structure_output,\n)\n\nfrom agentscope import logger, setup_logger\nfrom agentscope.mcp import StatefulClientBase\nfrom agentscope.agent import ReActAgent\nfrom agentscope.model import ChatModelBase\nfrom agentscope.formatter import FormatterBase\nfrom agentscope.memory import MemoryBase\nfrom agentscope.tool import (\n    ToolResponse,\n    view_text_file,\n    write_text_file,\n)\nfrom agentscope.message import (\n    Msg,\n    ToolUseBlock,\n    TextBlock,\n    ToolResultBlock,\n)\n\n\n_DEEP_RESEARCH_AGENT_DEFAULT_SYS_PROMPT = \"You're a helpful assistant.\"\n\n_LOG_DIR = os.path.join(os.path.dirname(__file__), \"log\")\n_LOG_PATH = os.path.join(\n    _LOG_DIR,\n    f\"log_{datetime.now().strftime('%y%m%d%H%M%S')}.md\",\n)\nos.makedirs(_LOG_DIR, exist_ok=True)\nsetup_logger(level=\"INFO\", filepath=_LOG_PATH)\n\n\nclass SubTaskItem(BaseModel):\n    \"\"\"Subtask item of deep research agent.\"\"\"\n\n    objective: str\n    working_plan: Optional[str] = None\n    knowledge_gaps: Optional[str] = None\n\n\nclass DeepResearchAgent(ReActAgent):\n    \"\"\"\n    Deep Research Agent for sophisticated research tasks.\n\n    Example:\n        .. code-block:: python\n\n        agent = DeepResearchAgent(\n            name=\"Friday\",\n            sys_prompt=\"You are a helpful assistant named Friday.\",\n            model=my_chat_model,\n            formatter=my_chat_formatter,\n            memory=InMemoryMemory(),\n            search_mcp_client=my_tavily_search_client,\n            tmp_file_storage_dir=agent_working_dir,\n        )\n        response = await agent(\n            Msg(\n                name=“user”,\n                content=\"Please give me a survey of the LLM-empowered agent.\",\n                role=“user”\n            )\n        )\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        name: str,\n        model: ChatModelBase,\n        formatter: FormatterBase,\n        memory: MemoryBase,\n        search_mcp_client: StatefulClientBase,\n        sys_prompt: str = _DEEP_RESEARCH_AGENT_DEFAULT_SYS_PROMPT,\n        max_iters: int = 30,\n        max_depth: int = 3,\n        max_tool_results_words: int = 10000,\n        tmp_file_storage_dir: str = \"tmp\",\n    ) -> None:\n        \"\"\"Initialize the Deep Research Agent.\n\n        Args:\n            name (str):\n                The unique identifier name for the agent instance.\n            model (ChatModelBase):\n                The chat model used for generating responses and reasoning.\n            formatter (FormatterBase):\n                The formatter used to convert messages into the required\n                format for the model API.\n            memory (MemoryBase):\n                The memory component used to store and retrieve dialogue\n                history.\n            search_mcp_client (StatefulClientBase):\n                The mcp client used to provide the tools for deep search.\n            sys_prompt (str, optional):\n                The system prompt that defines the agent's behavior\n                and personality.\n                Defaults to _DEEP_RESEARCH_AGENT_DEFAULT_SYS_PROMPT.\n            max_iters (int, optional):\n                The maximum number of reasoning-acting loop iterations.\n                Defaults to 30.\n            max_depth (int, optional):\n                The maximum depth of query expansion during deep searching.\n                Defaults to 3.\n            max_tool_results_words (int, optional):\n                The maximum number of words to keep from a tool's output before\n                truncation to fit the model's context.\n                Defaults to 10000.\n            tmp_file_storage_dir (str, optional):\n                The storage dir for generated files.\n                Default to 'tmp'\n        Returns:\n            None\n        \"\"\"\n\n        # initialization of prompts\n        self.prompt_dict = load_prompt_dict()\n\n        # Enhance the system prompt for deep research agent\n        add_note = self.prompt_dict[\"add_note\"].format_map(\n            {\"finish_function_name\": f\"`{self.finish_function_name}`\"},\n        )\n        tool_use_rule = self.prompt_dict[\"tool_use_rule\"].format_map(\n            {\"tmp_file_storage_dir\": tmp_file_storage_dir},\n        )\n        sys_prompt = f\"{sys_prompt}\\n{add_note}\\n{tool_use_rule}\"\n\n        super().__init__(\n            name=name,\n            sys_prompt=sys_prompt,\n            model=model,\n            formatter=formatter,\n            memory=memory,\n            max_iters=max_iters,\n        )\n        self.max_depth = max_depth\n        self.max_tool_results_words = max_tool_results_words\n        self.memory = memory\n        self.tmp_file_storage_dir = tmp_file_storage_dir\n        self.current_subtask = []\n\n        # register all necessary tools for deep research agent\n        self.toolkit.register_tool_function(view_text_file)\n        self.toolkit.register_tool_function(write_text_file)\n        self._search_mcp_client = search_mcp_client\n        self._mcp_initialized = False\n\n        self.search_function = \"tavily-search\"\n        self.extract_function = \"tavily-extract\"\n        self.read_file_function = \"view_text_file\"\n        self.write_file_function = \"write_text_file\"\n        self.summarize_function = \"summarize_intermediate_results\"\n\n        self.intermediate_memory = []\n        self.report_path_based = self.name + datetime.now().strftime(\n            \"%y%m%d%H%M%S\",\n        )\n        self.report_index = 1\n        self._required_structured_model = None\n        self.user_query = None\n\n        # add functions into toolkit\n        self.toolkit.register_tool_function(self.generate_response)\n        self.toolkit.register_tool_function(self.reflect_failure)\n        self.toolkit.register_tool_function(\n            self.summarize_intermediate_results,\n        )\n\n    async def _ensure_mcp_initialized(self) -> None:\n        \"\"\"Ensure MCP client is properly initialized.\n\n        This method registers MCP tools if not already done.\n        \"\"\"\n        if not self._mcp_initialized:\n            await self.toolkit.register_mcp_client(self._search_mcp_client)\n            self._mcp_initialized = True\n\n    async def reply(\n        self,\n        msg: Msg | list[Msg] | None = None,\n        structured_model: Type[BaseModel] | None = None,\n    ) -> Msg:\n        \"\"\"The reply method of the agent.\"\"\"\n        # Ensure MCP client is initialized before processing\n        await self._ensure_mcp_initialized()\n        if isinstance(msg, list):\n            if len(msg) == 0:\n                raise ValueError(\"Message list cannot be empty\")\n            current_msg = msg[-1]\n        else:\n            current_msg = msg\n\n        # Maintain the subtask list\n        self.user_query = current_msg.get_text_content()\n        self.current_subtask.append(\n            SubTaskItem(objective=self.user_query),\n        )\n\n        # Identify the expected output and generate a plan\n        await self.decompose_and_expand_subtask()\n        current_msg.content += (\n            f\"\\nExpected Output:\\n{self.current_subtask[0].knowledge_gaps}\"\n        )\n\n        # Add user query message to memory\n        await self.memory.add(current_msg)  # type: ignore\n\n        # Record structured output model if provided\n        if structured_model:\n            self._required_structured_model = structured_model\n            self.toolkit.set_extended_model(\n                self.finish_function_name,\n                structured_model,\n            )\n\n        for _ in range(self.max_iters):\n            # Generate the working plan first\n            if not self.current_subtask[-1].working_plan:\n                await self.decompose_and_expand_subtask()\n\n            # Write the instruction for reasoning\n            cur_plan = self.current_subtask[-1].working_plan\n            cur_know_gap = self.current_subtask[-1].knowledge_gaps\n            reasoning_prompt = self.prompt_dict[\"reasoning_prompt\"].format_map(\n                {\n                    \"objective\": self.current_subtask[-1].objective,\n                    \"plan\": cur_plan\n                    if cur_plan\n                    else \"There is no working plan now.\",\n                    \"knowledge_gap\": f\"## Knowledge Gaps:\\n {cur_know_gap}\"\n                    if cur_know_gap\n                    else \"\",\n                    \"depth\": len(self.current_subtask),\n                },\n            )\n            reasoning_prompt_msg = Msg(\n                \"user\",\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=reasoning_prompt,\n                    ),\n                ],\n                role=\"user\",\n            )\n            self.intermediate_memory.append(reasoning_prompt_msg)\n\n            # Reasoning to generate tool calls\n            backup_memory = deepcopy(self.memory)  # type: ignore\n            await self.memory.add(self.intermediate_memory)  # type: ignore\n            msg_reasoning = await self._reasoning()\n            self.memory = backup_memory\n\n            # Calling the tools\n            for tool_call in msg_reasoning.get_content_blocks(\"tool_use\"):\n                self.intermediate_memory.append(\n                    Msg(\n                        self.name,\n                        content=[tool_call],\n                        role=\"assistant\",\n                    ),\n                )  # add tool_use memory\n                msg_response = await self._acting(tool_call)\n                if msg_response:\n                    await self.memory.add(msg_response)\n                    self.current_subtask = []\n                    return msg_response\n\n        # When the maximum iterations are reached, summarize all the findings\n        return await self._summarizing()\n\n    async def _acting(self, tool_call: ToolUseBlock) -> Msg | None:\n        \"\"\"\n        Execute a tool call and process its response with browser-specific\n        handling.\n\n        Args:\n            tool_call (ToolUseBlock):\n                The tool use block containing the tool name, parameters,\n                and unique identifier for execution.\n        Returns:\n            Msg | None:\n                Returns a response message if the finish function is called\n                successfully, otherwise returns None to continue the\n                reasoning-acting loop.\n        \"\"\"\n\n        tool_res_msg = Msg(\n            \"system\",\n            [\n                ToolResultBlock(\n                    type=\"tool_result\",\n                    id=tool_call[\"id\"],\n                    name=tool_call[\"name\"],\n                    output=[],\n                ),\n            ],\n            \"system\",\n        )\n        update_memory = False\n        intermediate_report = \"\"\n        chunk = \"\"\n        try:\n            # Execute the tool call\n            tool_res = await self.toolkit.call_tool_function(tool_call)\n\n            # Async generator handling\n            async for chunk in tool_res:\n                # Turn into a tool result block\n                tool_res_msg.content[0][  # type: ignore[index]\n                    \"output\"\n                ] = chunk.content\n\n                # Skip the printing of the finish function call\n                if (\n                    tool_call[\"name\"] != self.finish_function_name\n                    or tool_call[\"name\"] == self.finish_function_name\n                    and not chunk.metadata.get(\"success\")\n                ):\n                    await self.print(tool_res_msg, chunk.is_last)\n\n                # Return message if generate_response is called successfully\n                if tool_call[\n                    \"name\"\n                ] == self.finish_function_name and chunk.metadata.get(\n                    \"success\",\n                    True,\n                ):\n                    if len(self.current_subtask) == 0:\n                        return chunk.metadata.get(\"response_msg\")\n\n                # Summarize intermediate results into a draft report\n                elif tool_call[\"name\"] == self.summarize_function:\n                    self.intermediate_memory = []\n                    await self.memory.add(\n                        Msg(\n                            \"assistant\",\n                            [\n                                TextBlock(\n                                    type=\"text\",\n                                    text=chunk.content[0][\"text\"],\n                                ),\n                            ],\n                            \"assistant\",\n                        ),\n                    )\n\n                # Truncate the web extract results that exceeds max length\n                elif tool_call[\"name\"] in [\n                    self.search_function,\n                    self.extract_function,\n                ]:\n                    tool_res_msg.content[0][\"output\"] = truncate_search_result(\n                        tool_res_msg.content[0][\"output\"],\n                        self.max_tool_results_words,\n                    )\n\n                # Update memory when an intermediate report is generated\n                if isinstance(chunk.metadata, dict) and chunk.metadata.get(\n                    \"update_memory\",\n                ):\n                    update_memory = True\n                    intermediate_report = chunk.metadata.get(\n                        \"intermediate_report\",\n                    )\n            return None\n\n        finally:\n            # Record the tool result message in the intermediate memory\n            if tool_call[\"name\"] != self.summarize_function:\n                self.intermediate_memory.append(tool_res_msg)\n\n            # Read more information from the web page if necessary\n            if tool_call[\"name\"] == self.search_function:\n                extract_res = await self._follow_up(chunk.content, tool_call)\n                if isinstance(\n                    extract_res.metadata,\n                    dict,\n                ) and extract_res.metadata.get(\"update_memory\"):\n                    self.intermediate_memory = []\n                    await self.memory.add(\n                        Msg(\n                            \"assistant\",\n                            content=[\n                                TextBlock(\n                                    type=\"text\",\n                                    text=extract_res.metadata.get(\n                                        \"intermediate_report\",\n                                    ).content[0][\"text\"],\n                                ),\n                            ],\n                            role=\"assistant\",\n                        ),\n                    )\n\n            # Update memory with the intermediate report\n            if update_memory:\n                self.intermediate_memory = []\n                await self.memory.add(\n                    Msg(\n                        \"assistant\",\n                        content=[\n                            TextBlock(\n                                type=\"text\",\n                                text=intermediate_report.content[0][\"text\"],\n                            ),\n                        ],\n                        role=\"assistant\",\n                    ),\n                )\n\n    async def get_model_output(\n        self,\n        msgs: list,\n        format_template: Type[BaseModel] = None,\n        stream: bool = True,\n    ) -> Any:\n        \"\"\"\n        Call the model and get output with or without a structured format.\n\n        Args:\n            msgs (list): A list of messages.\n            format_template (BaseModel): structured format.\n            stream (bool): stream-style output.\n        \"\"\"\n        blocks = None\n        if format_template:\n            res = await self.model(\n                await self.formatter.format(msgs=msgs),\n                tools=get_dynamic_tool_call_json(\n                    format_template,\n                ),\n            )\n\n            if stream:\n                async for content_chunk in res:\n                    blocks = content_chunk.content\n            else:\n                blocks = res.content\n\n            return get_structure_output(blocks)\n        else:\n            res = await self.model(\n                await self.formatter.format(msgs=msgs),\n            )\n\n            if stream:\n                async for content_chunk in res:\n                    blocks = content_chunk.content\n            else:\n                blocks = res.content\n            return blocks\n\n    async def call_specific_tool(\n        self,\n        func_name: str,\n        params: dict = None,\n    ) -> Tuple[Msg, Msg]:\n        \"\"\"\n        Call the specific tool in toolkit.\n\n        Args:\n            func_name (str): name of the tool.\n            params (dict): input parameters of the tool.\n        \"\"\"\n        tool_call = ToolUseBlock(\n            id=shortuuid.uuid(),\n            type=\"tool_use\",\n            name=func_name,\n            input=params,\n        )\n        tool_call_msg = Msg(\n            \"assistant\",\n            [tool_call],\n            role=\"assistant\",\n        )\n\n        # get tool acting res\n        tool_res_msg = Msg(\n            \"system\",\n            [\n                ToolResultBlock(\n                    type=\"tool_result\",\n                    id=tool_call[\"id\"],\n                    name=tool_call[\"name\"],\n                    output=[],\n                ),\n            ],\n            \"system\",\n        )\n        tool_res = await self.toolkit.call_tool_function(\n            tool_call,\n        )\n        async for chunk in tool_res:\n            tool_res_msg.content[0][\"output\"] = chunk.content\n\n        return tool_call_msg, tool_res_msg\n\n    async def decompose_and_expand_subtask(self) -> ToolResponse:\n        \"\"\"Identify the knowledge gaps of the current subtask and generate a\n        working plan by subtask decomposition. The working plan includes\n        necessary steps for task completion and expanded steps.\n\n        Returns:\n            ToolResponse:\n                The knowledge gaps and working plan of the current subtask\n                in JSON format.\n        \"\"\"\n        if len(self.current_subtask) <= self.max_depth:\n            decompose_sys_prompt = self.prompt_dict[\"decompose_sys_prompt\"]\n\n            previous_plan = \"\"\n            for i, subtask in enumerate(self.current_subtask):\n                previous_plan += f\"The {i}-th plan: {subtask.working_plan}\\n\"\n            previous_plan_inst = self.prompt_dict[\n                \"previous_plan_inst\"\n            ].format_map(\n                {\n                    \"previous_plan\": previous_plan,\n                    \"objective\": self.current_subtask[-1].objective,\n                },\n            )\n\n            try:\n                gaps_and_plan = await self.get_model_output(\n                    msgs=[\n                        Msg(\"system\", decompose_sys_prompt, \"system\"),\n                        Msg(\"user\", previous_plan_inst, \"user\"),\n                    ],\n                    format_template=SubtasksDecomposition,\n                    stream=self.model.stream,\n                )\n                response = json.dumps(\n                    gaps_and_plan,\n                    indent=2,\n                    ensure_ascii=False,\n                )\n            except Exception:  # noqa: F841\n                gaps_and_plan = {}\n                response = self.prompt_dict[\"retry_hint\"].format_map(\n                    {\"state\": \"decomposing the subtask\"},\n                )\n            self.current_subtask[-1].knowledge_gaps = gaps_and_plan.get(\n                \"knowledge_gaps\",\n                None,\n            )\n            self.current_subtask[-1].working_plan = gaps_and_plan.get(\n                \"working_plan\",\n                None,\n            )\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=response,\n                    ),\n                ],\n            )\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=self.prompt_dict[\"max_depth_hint\"],\n                ),\n            ],\n        )\n\n    async def _follow_up(\n        self,\n        search_results: list | str,\n        tool_call: ToolUseBlock,\n    ) -> ToolResponse:\n        \"\"\"Read the website more intensively to mine more information for\n        the task. And generate a follow-up subtask if necessary to perform\n        deep search.\n        \"\"\"\n\n        if len(self.current_subtask) < self.max_depth:\n            # Step#1: query expansion\n            expansion_sys_prompt = self.prompt_dict[\"expansion_sys_prompt\"]\n            expansion_inst = self.prompt_dict[\"expansion_inst\"].format_map(\n                {\n                    \"objective\": tool_call[\"input\"].get(\"query\", \"\"),\n                    \"checklist\": self.current_subtask[0].knowledge_gaps,\n                    \"knowledge_gaps\": self.current_subtask[-1].working_plan,\n                    \"search_results\": search_results,\n                },\n            )\n\n            try:\n                follow_up_subtask = await self.get_model_output(\n                    msgs=[\n                        Msg(\"system\", expansion_sys_prompt, \"system\"),\n                        Msg(\"user\", expansion_inst, \"user\"),\n                    ],\n                    format_template=WebExtraction,\n                    stream=self.model.stream,\n                )\n            except Exception:  # noqa: F841\n                follow_up_subtask = {}\n\n            #  Step #2: extract the url\n            if follow_up_subtask.get(\"need_more_information\", False):\n                expansion_response_msg = Msg(\n                    \"assistant\",\n                    follow_up_subtask.get(\n                        \"reasoning\",\n                        \"I need more information.\",\n                    ),\n                    role=\"assistant\",\n                )\n                urls = follow_up_subtask.get(\"url\", None)\n                logger.info(\"Reading %s\", urls)\n\n                # call the extract_function\n                params = {\n                    \"urls\": urls,\n                    \"extract_depth\": \"basic\",\n                }\n                (\n                    extract_tool_use_msg,\n                    extract_tool_res_msg,\n                ) = await self.call_specific_tool(\n                    func_name=self.extract_function,\n                    params=params,\n                )\n                self.intermediate_memory.append(extract_tool_use_msg)\n\n                extract_tool_res_msg.content[0][\n                    \"output\"\n                ] = truncate_search_result(\n                    extract_tool_res_msg.content[0][\"output\"],\n                    self.max_tool_results_words,\n                )\n                # await self.memory.add(tool_res_msg)\n                await self.print(extract_tool_res_msg, True)\n                self.intermediate_memory.append(extract_tool_res_msg)\n\n                # Step #4: follow up judge\n                try:\n                    follow_up_response = await self.get_model_output(\n                        msgs=[\n                            Msg(\"user\", expansion_inst, \"user\"),\n                            expansion_response_msg,\n                            extract_tool_use_msg,\n                            extract_tool_res_msg,\n                            Msg(\n                                \"user\",\n                                self.prompt_dict[\"follow_up_judge_sys_prompt\"],\n                                role=\"user\",\n                            ),\n                        ],\n                        format_template=FollowupJudge,\n                        stream=self.model.stream,\n                    )\n                except Exception:  # noqa: F841\n                    follow_up_response = {}\n                if not follow_up_response.get(\"is_sufficient\", True):\n                    subtasks = follow_up_subtask.get(\"subtask\", None)\n                    logger.info(\"Figuring out %s\", subtasks)\n                    intermediate_report = (\n                        await self.summarize_intermediate_results()\n                    )\n                    self.current_subtask.append(\n                        SubTaskItem(objective=subtasks),\n                    )\n                    return ToolResponse(\n                        content=[\n                            TextBlock(\n                                type=\"text\",\n                                text=follow_up_response.get(\n                                    \"reasoning\",\n                                    self.prompt_dict[\"need_deeper_hint\"],\n                                ),\n                            ),\n                        ],\n                        metadata={\n                            \"update_memory\": True,\n                            \"intermediate_report\": intermediate_report,\n                        },\n                    )\n                else:\n                    return ToolResponse(\n                        content=[\n                            TextBlock(\n                                type=\"text\",\n                                text=follow_up_response.get(\n                                    \"reasoning\",\n                                    self.prompt_dict[\"sufficient_hint\"],\n                                ),\n                            ),\n                        ],\n                    )\n            else:\n                return ToolResponse(\n                    content=[\n                        TextBlock(\n                            type=\"text\",\n                            text=follow_up_subtask.get(\n                                \"reasoning\",\n                                self.prompt_dict[\"sufficient_hint\"],\n                            ),\n                        ),\n                    ],\n                )\n        else:\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=self.prompt_dict[\"max_depth_hint\"],\n                    ),\n                ],\n            )\n\n    async def summarize_intermediate_results(self) -> ToolResponse:\n        \"\"\"Summarize the intermediate results into a report when a step\n        in working plan is completed.\n\n        Returns:\n            ToolResponse:\n                The summarized draft report.\n        \"\"\"\n        if len(self.intermediate_memory) == 0:\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=self.prompt_dict[\"no_result_hint\"],\n                    ),\n                ],\n            )\n        # agent actively call this tool\n        if self.intermediate_memory[-1].name == self.summarize_function:\n            blocks = await self.get_model_output(\n                msgs=self.intermediate_memory\n                + [\n                    Msg(\n                        \"user\",\n                        self.prompt_dict[\"summarize_hint\"].format_map(\n                            {\n                                \"plan\": self.current_subtask[-1].working_plan,\n                            },\n                        ),\n                        role=\"user\",\n                    ),\n                ],\n                stream=self.model.stream,\n            )\n            self.current_subtask[-1].working_plan = blocks[0][\n                \"text\"\n            ]  # type: ignore[index]\n        report_prefix = \"#\" * len(self.current_subtask)\n        summarize_sys_prompt = self.prompt_dict[\n            \"summarize_sys_prompt\"\n        ].format_map(\n            {\"report_prefix\": report_prefix},\n        )\n        # get all tool result\n        tool_result = \"\"\n        for item in self.intermediate_memory:\n            if isinstance(item.content, str):\n                tool_result += item.content + \"\\n\"\n            elif isinstance(item.content, list):\n                for each in item.content:\n                    if each[\"type\"] == \"tool_result\":\n                        tool_result += str(each) + \"\\n\"\n            else:\n                logger.warning(\n                    \"Unknown content type: %s!\",\n                    type(item.content),\n                )\n                continue\n        summarize_instruction = self.prompt_dict[\"summarize_inst\"].format_map(\n            {\n                \"objective\": self.current_subtask[0].objective,\n                \"knowledge_gaps\": self.current_subtask[0].knowledge_gaps,\n                \"working_plan\": self.current_subtask[-1].working_plan,\n                \"tool_result\": tool_result,\n            },\n        )\n\n        blocks = await self.get_model_output(\n            msgs=[\n                Msg(\"system\", summarize_sys_prompt, \"system\"),\n                Msg(\"user\", summarize_instruction, \"user\"),\n            ],\n            stream=self.model.stream,\n        )\n        intermediate_report = blocks[0][\"text\"]  # type: ignore[index]\n\n        # Write the intermediate report\n        intermediate_report_path = os.path.join(\n            self.tmp_file_storage_dir,\n            f\"{self.report_path_based}_\"\n            f\"{self.user_query}_inprocess_report_{self.report_index}.md\",\n        )\n        self.report_index += 1\n        params = {\n            \"file_path\": intermediate_report_path,\n            \"content\": intermediate_report,\n        }\n        await self.call_specific_tool(\n            func_name=self.write_file_function,\n            params=params,\n        )\n        logger.info(\n            \"Storing the intermediate findings: %s\",\n            intermediate_report,\n        )\n        if (\n            self.intermediate_memory[-1].has_content_blocks(\"tool_use\")\n            and self.intermediate_memory[-1].get_content_blocks(\"tool_use\")[0][\n                \"name\"\n            ]\n            == self.summarize_function\n        ):\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=self.prompt_dict[\"update_report_hint\"].format_map(\n                            {\n                                \"intermediate_report\": intermediate_report,\n                                \"report_path\": intermediate_report_path,\n                            },\n                        ),\n                    ),\n                ],\n            )\n        else:\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=self.prompt_dict[\"save_report_hint\"].format_map(\n                            {\n                                \"intermediate_report\": intermediate_report,\n                            },\n                        ),\n                    ),\n                ],\n            )\n\n    async def _generate_deepresearch_report(\n        self,\n        checklist: str,\n    ) -> Tuple[Msg, str]:\n        \"\"\"Collect and polish all draft reports into a final report.\n\n        Args:\n            checklist (`str`):\n                The expected output items of the original task.\n        \"\"\"\n        reporting_sys_prompt = self.prompt_dict[\"reporting_sys_prompt\"]\n        reporting_sys_prompt.format_map(\n            {\n                \"original_task\": self.user_query,\n                \"checklist\": checklist,\n            },\n        )\n\n        # Collect all intermediate reports\n        if self.report_index > 1:\n            inprocess_report = \"\"\n            for index in range(self.report_index):\n                params = {\n                    \"file_path\": os.path.join(\n                        self.tmp_file_storage_dir,\n                        f\"{self.report_path_based}_\"\n                        f\"{self.user_query}_inprocess_report_{index + 1}.md\",\n                    ),\n                }\n                _, read_draft_tool_res_msg = await self.call_specific_tool(\n                    func_name=self.read_file_function,\n                    params=params,\n                )\n                inprocess_report += (\n                    read_draft_tool_res_msg.content[0][\"output\"][0][\"text\"]\n                    + \"\\n\"\n                )\n\n            msgs = [\n                Msg(\n                    \"system\",\n                    content=reporting_sys_prompt,\n                    role=\"system\",\n                ),\n                Msg(\n                    \"user\",\n                    content=f\"Draft report:\\n{inprocess_report}\",\n                    role=\"user\",\n                ),\n            ]\n        else:  # Use only intermediate memory to generate report\n            msgs = [\n                Msg(\n                    \"system\",\n                    content=reporting_sys_prompt,\n                    role=\"system\",\n                ),\n            ] + self.intermediate_memory\n\n        blocks = await self.get_model_output(\n            msgs=msgs,\n            stream=self.model.stream,\n        )\n        final_report_content = blocks[0][\"text\"]  # type: ignore[index]\n        logger.info(\n            \"The final Report is generated: %s\",\n            final_report_content,\n        )\n\n        # Write the final report into a file\n        detailed_report_path = os.path.join(\n            self.tmp_file_storage_dir,\n            f\"{self.report_path_based}_detailed_report.md\",\n        )\n\n        params = {\n            \"file_path\": detailed_report_path,\n            \"content\": final_report_content,\n        }\n        _, write_report_tool_res_msg = await self.call_specific_tool(\n            func_name=self.write_file_function,\n            params=params,\n        )\n\n        return write_report_tool_res_msg, detailed_report_path\n\n    async def _summarizing(self) -> Msg:\n        \"\"\"Generate a report based on the exsisting findings when the\n        agent fails to solve the problem in the maximum iterations.\"\"\"\n\n        (\n            summarized_content,\n            _,\n        ) = await self._generate_deepresearch_report(\n            checklist=self.current_subtask[0].knowledge_gaps,\n        )\n        summarize_result = Msg(\n            name=self.name,\n            role=\"assistant\",\n            content=json.dumps(\n                summarized_content.content[0][\"output\"][0],\n                indent=2,\n                ensure_ascii=False,\n            ),\n        )\n        self.memory.add(summarize_result)\n        return summarize_result\n\n    async def reflect_failure(self) -> ToolResponse:\n        \"\"\"Reflect on the failure of the action and determine to rephrase\n        the plan or deeper decompose the current step.\n\n        Returns:\n            ToolResponse:\n                The reflection about plan rephrasing and subtask decomposition.\n        \"\"\"\n        reflect_sys_prompt = self.prompt_dict[\"reflect_sys_prompt\"]\n        conversation_history = \"\"\n        for msg in self.intermediate_memory:\n            conversation_history += (\n                json.dumps(\n                    {\"role\": \"user\", \"content\": msg.content},\n                    ensure_ascii=False,\n                    indent=2,\n                )\n                + \"\\n\"\n            )\n        reflect_inst = self.prompt_dict[\"reflect_instruction\"].format_map(\n            {\n                \"conversation_history\": conversation_history,\n                \"plan\": self.current_subtask[-1].working_plan,\n            },\n        )\n        try:\n            reflection = await self.get_model_output(\n                msgs=[\n                    Msg(\"system\", reflect_sys_prompt, \"system\"),\n                    Msg(\"user\", reflect_inst, \"user\"),\n                ],\n                format_template=ReflectFailure,\n                stream=self.model.stream,\n            )\n            response = json.dumps(\n                reflection,\n                indent=2,\n                ensure_ascii=False,\n            )\n        except Exception:  # noqa: F841\n            reflection = {}\n            response = self.prompt_dict[\"retry_hint\"].format_map(\n                {\"state\": \"making the reflection\"},\n            )\n\n        if reflection.get(\"rephrase_subtask\", False) and reflection[\n            \"rephrase_subtask\"\n        ].get(\n            \"need_rephrase\",\n            False,\n        ):  # type: ignore[index]\n            self.current_subtask[-1].working_plan = reflection[\n                \"rephrase_subtask\"\n            ][\n                \"rephrased_plan\"\n            ]  # type: ignore[index]\n        elif reflection.get(\"decompose_subtask\", False) and reflection[\n            \"decompose_subtask\"\n        ].get(\n            \"need_decompose\",\n            False,\n        ):  # type: ignore[index]\n            if len(self.current_subtask) <= self.max_depth:\n                intermediate_report = (\n                    await self.summarize_intermediate_results()\n                )\n                self.current_subtask.append(\n                    SubTaskItem(\n                        objective=reflection[\n                            \"decompose_subtask\"\n                        ].get(  # type: ignore[index]\n                            \"failed_subtask\",\n                            None,\n                        ),\n                    ),\n                )\n                return ToolResponse(\n                    content=[\n                        TextBlock(\n                            type=\"text\",\n                            text=response,\n                        ),\n                    ],\n                    metadata={\n                        \"update_memory\": True,\n                        \"intermediate_report\": intermediate_report,\n                    },\n                )\n            else:\n                return ToolResponse(\n                    content=[\n                        TextBlock(\n                            type=\"text\",\n                            text=self.prompt_dict[\"max_depth_hint\"],\n                        ),\n                    ],\n                )\n        else:\n            pass\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=response,\n                ),\n            ],\n        )\n\n    # pylint: disable=invalid-overridden-method, unused-argument\n    async def generate_response(  #\n        self,\n        response: str,\n        **_kwargs: Any,\n    ) -> ToolResponse:\n        \"\"\"Generate a detailed report as a response.\n\n        Besides, when calling this function, the reasoning-acting memory will\n        be cleared, so your response should contain a brief summary of what\n        you have done so far.\n\n        Args:\n            response (`str`):\n                Your response to the user.\n        \"\"\"\n        checklist = self.current_subtask[0].knowledge_gaps\n        completed_subtask = self.current_subtask.pop()\n\n        if len(self.current_subtask) == 0:\n            (\n                summarized_content,\n                _,\n            ) = await self._generate_deepresearch_report(\n                checklist=checklist,\n            )\n            response_msg = Msg(\n                name=self.name,\n                role=\"assistant\",\n                content=json.dumps(\n                    summarized_content.content[0][\"output\"][0],\n                    indent=2,\n                    ensure_ascii=False,\n                ),\n            )\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=\"Successfully generated detailed report.\",\n                    ),\n                ],\n                metadata={\n                    \"success\": True,\n                    \"response_msg\": response_msg,\n                },\n                is_last=True,\n            )\n        else:\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=self.prompt_dict[\n                            \"subtask_complete_hint\"\n                        ].format_map(\n                            {\n                                \"cur_obj\": completed_subtask.objective,\n                                \"next_obj\": self.current_subtask[-1].objective,\n                            },\n                        ),\n                    ),\n                ],\n                metadata={\n                    \"success\": True,\n                },\n                is_last=True,\n            )\n"
  },
  {
    "path": "examples/agent/deep_research_agent/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The main entry point of the Deep Research agent example.\"\"\"\nimport asyncio\nimport os\n\nfrom deep_research_agent import DeepResearchAgent\n\nfrom agentscope import logger\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.message import Msg\nfrom agentscope.mcp import StdIOStatefulClient\n\n\nasync def main(user_query: str) -> None:\n    \"\"\"The main entry point for the Deep Research agent example.\"\"\"\n    logger.setLevel(\"DEBUG\")\n\n    tavily_search_client = StdIOStatefulClient(\n        name=\"tavily_mcp\",\n        command=\"npx\",\n        args=[\"-y\", \"tavily-mcp@latest\"],\n        env={\"TAVILY_API_KEY\": os.getenv(\"TAVILY_API_KEY\", \"\")},\n    )\n\n    default_working_dir = os.path.join(\n        os.path.dirname(__file__),\n        \"deepresearch_agent_demo_env\",\n    )\n    agent_working_dir = os.getenv(\n        \"AGENT_OPERATION_DIR\",\n        default_working_dir,\n    )\n    os.makedirs(agent_working_dir, exist_ok=True)\n\n    try:\n        await tavily_search_client.connect()\n        agent = DeepResearchAgent(\n            name=\"Friday\",\n            sys_prompt=\"You are a helpful assistant named Friday.\",\n            model=DashScopeChatModel(\n                api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n                model_name=\"qwen3-max\",\n                enable_thinking=False,\n                stream=True,\n            ),\n            formatter=DashScopeChatFormatter(),\n            memory=InMemoryMemory(),\n            search_mcp_client=tavily_search_client,\n            tmp_file_storage_dir=agent_working_dir,\n            max_tool_results_words=10000,\n        )\n        user_name = \"Bob\"\n        msg = Msg(\n            user_name,\n            content=user_query,\n            role=\"user\",\n        )\n        result = await agent(msg)\n        logger.info(result)\n\n    except Exception as err:\n        logger.exception(err)\n    finally:\n        await tavily_search_client.close()\n\n\nif __name__ == \"__main__\":\n    query = (\n        \"If Eliud Kipchoge could maintain his record-making \"\n        \"marathon pace indefinitely, how many thousand hours \"\n        \"would it take him to run the distance between the \"\n        \"Earth and the Moon its closest approach? Please use \"\n        \"the minimum perigee value on the Wikipedia page for \"\n        \"the Moon when carrying out your calculation. Round \"\n        \"your result to the nearest 1000 hours and do not use \"\n        \"any comma separators if necessary.\"\n    )\n    try:\n        asyncio.run(main(query))\n    except Exception as e:\n        logger.exception(e)\n"
  },
  {
    "path": "examples/agent/deep_research_agent/utils.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The utilities for deep research agent\"\"\"\nimport os\nimport json\nimport re\nfrom typing import Union, Sequence, Any, Type\nfrom pydantic import BaseModel\n\nfrom agentscope.tool import Toolkit, ToolResponse\n\n\ndef get_prompt_from_file(\n    file_path: str,\n    return_json: bool,\n) -> Union[str, dict]:\n    \"\"\"Get prompt from file\"\"\"\n    with open(os.path.join(file_path), \"r\", encoding=\"utf-8\") as f:\n        if return_json:\n            prompt = json.load(f)\n        else:\n            prompt = f.read()\n    return prompt\n\n\ndef truncate_by_words(\n    sentence: str,\n    max_tool_results_words: int = 10000,\n) -> str:\n    \"\"\"Truncate too long sentences by words number\"\"\"\n    words = re.findall(\n        r\"\\w+|[^\\w\\s]\",\n        sentence,\n        re.UNICODE,\n    )\n\n    word_count = 0\n    result = []\n    for word in words:\n        if re.match(r\"\\w+\", word):\n            word_count += 1\n        if word_count > max_tool_results_words:\n            break\n        result.append(word)\n\n    truncated_sentence = \"\"\n    for i, word in enumerate(result):\n        if i == 0:\n            truncated_sentence += word\n        elif re.match(r\"\\w+\", word):\n            truncated_sentence += \" \" + word\n        else:\n            truncated_sentence += word\n    return truncated_sentence\n\n\ndef truncate_search_result(\n    res: list,\n    max_tool_results_words: int = 10000,\n    search_func: str = \"tavily-search\",\n    extract_function: str = \"tavily-extract\",\n) -> list:\n    \"\"\"Truncate search result in deep research agent\"\"\"\n    if search_func != \"tavily-search\" or extract_function != \"tavily-extract\":\n        raise NotImplementedError(\n            \"Specific implementation of truncation should be provided.\",\n        )\n\n    for i, val in enumerate(res):\n        res[i][\"text\"] = truncate_by_words(\n            val[\"text\"],\n            max_tool_results_words,\n        )\n\n    return res\n\n\ndef generate_structure_output(**kwargs: Any) -> ToolResponse:\n    \"\"\"Generate a structured output tool response.\n\n    This function is designed to be used as a tool function for generating\n    structured outputs. It takes arbitrary keyword arguments and wraps them\n    in a ToolResponse with metadata.\n\n    Args:\n        **kwargs: Arbitrary keyword arguments that should match the format\n            of the expected structured output specification.\n\n    Returns:\n        ToolResponse: A tool response object with empty content and the\n            provided kwargs as metadata.\n\n    Note:\n        The input parameters should be in the same format as the specification\n        and include as much detail as requested by the calling context.\n    \"\"\"\n    return ToolResponse(content=[], metadata=kwargs)\n\n\ndef get_dynamic_tool_call_json(data_model_type: Type[BaseModel]) -> list[dict]:\n    \"\"\"Generate JSON schema for dynamic tool calling with a given data model.\n\n    Creates a temporary toolkit, registers the structure output function,\n    and configures it with the specified data model to generate appropriate\n    JSON schemas for tool calling.\n\n    Args:\n        data_model_type: A Pydantic BaseModel class that defines the expected\n            structure of the tool output.\n\n    Returns:\n        A list of dictionary that contains the JSON schemas for\n        the configured tool, suitable for use in API calls that\n        support structured outputs.\n\n    Example:\n        class MyModel(BaseModel):\n            name: str\n            value: int\n\n        schema = get_dynamic_tool_call_json(MyModel)\n    \"\"\"\n    tmp_toolkit = Toolkit()\n    tmp_toolkit.register_tool_function(generate_structure_output)\n    tmp_toolkit.set_extended_model(\n        \"generate_structure_output\",\n        data_model_type,\n    )\n    return tmp_toolkit.get_json_schemas()\n\n\ndef get_structure_output(blocks: list | Sequence) -> dict:\n    \"\"\"Extract structured output from a sequence of blocks.\n\n    Processes a list or sequence of blocks to extract tool use outputs\n    and combine them into a single dictionary. This is typically used\n    to parse responses from language models that include tool calls.\n\n    Args:\n        blocks: A list or sequence of blocks that may contain tool use\n            information. Each block should be a dictionary with 'type'\n            and 'input' keys for tool use blocks.\n\n    Returns:\n        A dictionary containing the combined input data from all tool\n        use blocks found in the input sequence.\n\n    Example:\n        blocks = [\n            {\"type\": \"tool_use\", \"input\": {\"name\": \"test\"}},\n            {\"type\": \"text\", \"content\": \"Some text\"},\n            {\"type\": \"tool_use\", \"input\": {\"value\": 42}}\n        ]\n        result = PromptBase.get_structure_output(blocks)\n        # result: {\"name\": \"test\", \"value\": 42}\n    \"\"\"\n\n    dict_output = {}\n    for block in blocks:\n        if isinstance(block, dict) and block.get(\"type\") == \"tool_use\":\n            dict_output.update(block.get(\"input\", {}))\n    return dict_output\n\n\ndef load_prompt_dict() -> dict:\n    \"\"\"Load prompt into dict\"\"\"\n    prompt_dict = {}\n    cur_dir = os.path.dirname(os.path.abspath(__file__))\n\n    prompt_dict[\"add_note\"] = get_prompt_from_file(\n        file_path=os.path.join(\n            cur_dir,\n            \"built_in_prompt/prompt_worker_additional_sys_prompt.md\",\n        ),\n        return_json=False,\n    )\n\n    prompt_dict[\"tool_use_rule\"] = get_prompt_from_file(\n        file_path=os.path.join(\n            cur_dir,\n            \"built_in_prompt/prompt_tool_usage_rules.md\",\n        ),\n        return_json=False,\n    )\n\n    prompt_dict[\"decompose_sys_prompt\"] = get_prompt_from_file(\n        file_path=os.path.join(\n            cur_dir,\n            \"built_in_prompt/prompt_decompose_subtask.md\",\n        ),\n        return_json=False,\n    )\n\n    prompt_dict[\"expansion_sys_prompt\"] = get_prompt_from_file(\n        file_path=os.path.join(\n            cur_dir,\n            \"built_in_prompt/prompt_deeper_expansion.md\",\n        ),\n        return_json=False,\n    )\n\n    prompt_dict[\"summarize_sys_prompt\"] = get_prompt_from_file(\n        file_path=os.path.join(\n            cur_dir,\n            \"built_in_prompt/prompt_inprocess_report.md\",\n        ),\n        return_json=False,\n    )\n\n    prompt_dict[\"reporting_sys_prompt\"] = get_prompt_from_file(\n        file_path=os.path.join(\n            cur_dir,\n            \"built_in_prompt/prompt_deepresearch_summary_report.md\",\n        ),\n        return_json=False,\n    )\n\n    prompt_dict[\"reflect_sys_prompt\"] = get_prompt_from_file(\n        file_path=os.path.join(\n            cur_dir,\n            \"built_in_prompt/prompt_reflect_failure.md\",\n        ),\n        return_json=False,\n    )\n\n    prompt_dict[\"reasoning_prompt\"] = (\n        \"## Current Subtask:\\n{objective}\\n\"\n        \"## Working Plan:\\n{plan}\\n\"\n        \"{knowledge_gap}\\n\"\n        \"## Research Depth:\\n{depth}\"\n    )\n\n    prompt_dict[\"previous_plan_inst\"] = (\n        \"## Previous Plan:\\n{previous_plan}\\n\"\n        \"## Current Subtask:\\n{objective}\\n\"\n    )\n\n    prompt_dict[\"max_depth_hint\"] = (\n        \"The search depth has reached the maximum limit. So the \"\n        \"current subtask can not be further decomposed and \"\n        \"expanded anymore. I need to find another way to get it \"\n        \"done no matter what.\"\n    )\n\n    prompt_dict[\"expansion_inst\"] = (\n        \"Review the web search results and identify whether \"\n        \"there is any information that can potentially help address \"\n        \"checklist items or fulfill knowledge gaps of the task, \"\n        \"but whose content is limited or only briefly mentioned.\\n\"\n        \"**Task Description:**\\n{objective}\\n\"\n        \"**Checklist:**\\n{checklist}\\n\"\n        \"**Knowledge Gaps:**\\n{knowledge_gaps}\\n\"\n        \"**Search Results:**\\n{search_results}\\n\"\n        \"**Output:**\\n\"\n    )\n\n    prompt_dict[\"follow_up_judge_sys_prompt\"] = (\n        \"To provide sufficient external information for the user's \"\n        \"query, you have conducted a web search to obtain additional \"\n        \"data. However, you found that some of the information, while \"\n        \"important, was insufficient. Consequently, you extracted the \"\n        \"entire content from one of the URLs to gather more \"\n        \"comprehensive information. Now, you must rigorously and \"\n        \"carefully assess whether, after both the web search and \"\n        \"extraction process, the information content is adequate to \"\n        \"address the given task. Be aware that any arbitrary decisions \"\n        \"may result in unnecessary and unacceptable time costs.\\n\"\n    )\n\n    prompt_dict[\n        \"retry_hint\"\n    ] = \"Something went wrong when {state}. I need to retry.\"\n\n    prompt_dict[\"need_deeper_hint\"] = (\n        \"The information is insufficient and I need to make deeper \"\n        \"research to fill the knowledge gap.\"\n    )\n\n    prompt_dict[\n        \"sufficient_hint\"\n    ] = \"The information after web search and extraction is sufficient enough!\"\n\n    prompt_dict[\"no_result_hint\"] = (\n        \"I mistakenly called the `summarize_intermediate_results` tool as \"\n        \"there exists no milestone result to summarize now.\"\n    )\n\n    prompt_dict[\"summarize_hint\"] = (\n        \"Based on your work history above, examine which step in the \"\n        \"following working plan has been completed. Mark the completed \"\n        \"step with [DONE] at the end of its line (e.g., k. step k [DONE]) \"\n        \"and leave the uncompleted steps unchanged. You MUST return only \"\n        \"the updated plan, preserving exactly the same format as the \"\n        \"original plan. Do not include any explanations, reasoning, \"\n        \"or section headers such as '## Working Plan:', just output the\"\n        \"updated plan itself.\"\n        \"\\n\\n## Working Plan:\\n{plan}\"\n    )\n\n    prompt_dict[\"summarize_inst\"] = (\n        \"**Task Description:**\\n{objective}\\n\"\n        \"**Checklist:**\\n{knowledge_gaps}\\n\"\n        \"**Knowledge Gaps:**\\n{working_plan}\\n\"\n        \"**Search Results:**\\n{tool_result}\"\n    )\n\n    prompt_dict[\"update_report_hint\"] = (\n        \"Due to the overwhelming quantity of information, I have replaced the \"\n        \"original bulk search results from the research phase with the \"\n        \"following report that consolidates and summarizes the essential \"\n        \"findings:\\n {intermediate_report}\\n\\n\"\n        \"Such report has been saved to the {report_path}. \"\n        \"I will now **proceed to the next item** in the working plan.\"\n    )\n\n    prompt_dict[\"save_report_hint\"] = (\n        \"The milestone results of the current item in working plan \"\n        \"are summarized into the following report:\\n{intermediate_report}\"\n    )\n\n    prompt_dict[\"reflect_instruction\"] = (\n        \"## Work History:\\n{conversation_history}\\n\"\n        \"## Working Plan:\\n{plan}\\n\"\n    )\n\n    prompt_dict[\"subtask_complete_hint\"] = (\n        \"Subtask ‘{cur_obj}’ is completed. Now the current subtask \"\n        \"fallbacks to '{next_obj}'\"\n    )\n\n    return prompt_dict\n"
  },
  {
    "path": "examples/agent/meta_planner_agent/README.md",
    "content": "# Meta Planner Agent Example\n\nIn this example, we demonstrate\n\n- how to build a planner agent that can decompose complex task into manageable subtasks and orchestrates sub-agents to\n complete them\n- how to handle the printing messages of the sub-agents properly in a multi-agent system\n- how to propagate interrupt events from sub agents to the main planner agent\n\nSpecifically, in [main.py](./main.py), a planner agent is created with the `PlanNotebook` instance to create and manage\nplans. It's equipped with a tool function named `create_worker` in [tool.py](./tool.py) to create sub-agents\ndynamically and finish the assigned subtask. The sub-agents are equipped with some basic tools, and some preset\nMCP servers to enhance their capabilities.\n\n> We suggest to use AgentScope-Studio to visualize the agent-interactions in this example.\n\n## Quick Start\n\nInstall agentscope if you haven't already:\n\n```bash\npip install agentscope\n```\n\nMake sure you have set your DashScope API key as an environment variable.\n\nIn this example, the sub-agents are equipped with the following MCP servers, set the corresponding environment variables to activate them.\nIf not set, the corresponding MCP will be disabled.\nFor more details about the tools, refer to [tool.py](./tool.py). You can also add or modify the tools as needed.\n\n| MCP                      | Description                    | Environment Variable |\n|--------------------------|--------------------------------|----------------------|\n| AMAP MCP                 | Provide map related services   | GAODE_API_KEY        |\n| GitHub MCP               | Search and access GitHub repos | GITHUB_TOKEN         |\n| Microsoft Playwright MCP | Web Browser-use MCP server     | -                    |\n\nRun the example:\n\n```bash\npython main.py\n```\n\nThen you can ask the planner agent to help you complete a complex task, such as \"Conduct research on AgentScope repo\".\n\nNote for simple questions, the planner agent may directly answer without creating sub-agents.\n\n## Advanced Usage\n\n### Handling Sub-agent Output\n\nIn this example, the sub-agents won't print messages to the console directly (by `agent.set_console_output_enable(True)` in tool.py).\nInstead, its printing messages are streamlined back to the planner agent as the streaming responses of the tool function `create_worker`.\nBy this way, we only expose the planner agent to the user, rather than multiple agents, which provides a better user experience.\nHowever, the response of the tool function `create_worker` maybe take too much context length if the sub-agent finishes the given task with a long reasoning-acting process.\n\nThis figure shows how the sub-agent output is displayed as tool streaming response in AgentScope-Studio:\n\n<details>\n <summary>Chinese</summary>\n <p align=\"center\">\n  <img src=\"./assets/screenshot_zh.jpg\"/>\n </p>\n</details>\n\n<details>\n <summary>English</summary>\n <p align=\"center\">\n  <img src=\"./assets/screenshot_en.jpg\"/>\n </p>\n</details>\n\n\n\nAlso, you can choose to expose the sub-agent to the user, and only take the structured results back to the planner agent as the tool result of `create_worker`.\n\n### Propagating Interrupt Events\n\nIn `ReActAgent`, when the final answer is generated from the `handle_interrupt` function, the metadata field of the return message\nwill contain a `_is_interrupted` key with value `True` to indicate that the agent is interrupted.\n\nBy this field, we can propagate the interrupt event from the sub-agent to the main planner agent in the tool function `create_worker`.\nFor user defined agent classes, you can define your own propagation logic in the `handle_interrupt` function of your agent class.\n\n### Changing the LLM\n\nThe example is built with DashScope chat model. If you want to change the model in this example, don't forget\nto change the formatter at the same time! The corresponding relationship between built-in models and formatters are\nlist in [our tutorial](https://doc.agentscope.io/tutorial/task_prompt.html#id1)\n\n## Further Reading\n\n- [Plan](https://doc.agentscope.io/tutorial/task_plan.html)\n"
  },
  {
    "path": "examples/agent/meta_planner_agent/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The planner agent example.\"\"\"\nimport asyncio\nimport os\n\nfrom tool import create_worker\n\nfrom agentscope.agent import ReActAgent, UserAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.plan import PlanNotebook\nfrom agentscope.tool import Toolkit\n\n\nasync def main() -> None:\n    \"\"\"The main function.\"\"\"\n    # Connect to the studio for better visualization (optional)\n    # import agentscope\n    # agentscope.init(\n    #     project=\"meta_planner_agent\",\n    #     studio_url=\"http://localhost:3000\",\n    # )\n\n    toolkit = Toolkit()\n    toolkit.register_tool_function(create_worker)\n\n    planner = ReActAgent(\n        name=\"Friday\",\n        # pylint: disable=C0301\n        sys_prompt=\"\"\"You are Friday, a multifunctional agent that can help people solving different complex tasks. You act like a meta planner to solve complicated tasks by decomposing the task and building/orchestrating different worker agents to finish the sub-tasks.\n\n## Core Mission\nYour primary purpose is to break down complicated tasks into manageable subtasks (a plan), create worker agents to finish the subtask, and coordinate their execution to achieve the user's goal efficiently.\n\n### Important Constraints\n1. DO NOT TRY TO SOLVE THE SUBTASKS DIRECTLY yourself.\n2. Always follow the plan sequence.\n3. DO NOT finish the plan until all subtasks are finished.\n\"\"\",  # noqa: E501\n        model=DashScopeChatModel(\n            model_name=\"qwen3-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        ),\n        formatter=DashScopeChatFormatter(),\n        plan_notebook=PlanNotebook(),\n        toolkit=toolkit,\n        max_iters=20,\n    )\n    user = UserAgent(name=\"user\")\n\n    msg = None\n    while True:\n        msg = await planner(msg)\n        msg = await user(msg)\n        if msg.get_text_content() == \"exit\":\n            break\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/agent/meta_planner_agent/tool.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The tool functions used in the planner example.\"\"\"\nimport asyncio\nimport json\nimport os\nfrom collections import OrderedDict\nfrom typing import AsyncGenerator\n\nfrom pydantic import BaseModel, Field\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.mcp import HttpStatelessClient, StdIOStatefulClient\nfrom agentscope.message import Msg, TextBlock\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.pipeline import stream_printing_messages\nfrom agentscope.tool import (\n    ToolResponse,\n    Toolkit,\n    write_text_file,\n    insert_text_file,\n    view_text_file,\n)\n\n\nclass ResultModel(BaseModel):\n    \"\"\"\n    The result model used for the sub worker to summarize the task result.\n    \"\"\"\n\n    success: bool = Field(\n        description=\"Whether the task was successful or not.\",\n    )\n    message: str = Field(\n        description=(\n            \"The specific task result, should include necessary details, \"\n            \"e.g. the file path if any file is generated, the deviation, \"\n            \"and the error message if any.\"\n        ),\n    )\n\n\ndef _convert_to_text_block(msgs: list[Msg]) -> list[TextBlock]:\n    # Collect all the content blocks\n    blocks: list = []\n    # Convert tool_use block into text block for streaming tool response\n    for _ in msgs:\n        for block in _.get_content_blocks():\n            if block[\"type\"] == \"text\":\n                blocks.append(block)\n\n            elif block[\"type\"] == \"tool_use\":\n                blocks.append(\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Calling tool {block['name']} ...\",\n                    ),\n                )\n\n    return blocks\n\n\nasync def create_worker(\n    task_description: str,\n) -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"Create a sub-worker to finish the given task.\n\n    Args:\n        task_description (`str`):\n            The description of the task to be done by the sub-worker, should\n            contain all the necessary information.\n\n    Returns:\n        `AsyncGenerator[ToolResponse, None]`:\n            An async generator yielding ToolResponse objects.\n    \"\"\"\n    toolkit = Toolkit()\n\n    # Gaode MCP client\n    if os.getenv(\"GAODE_API_KEY\"):\n        toolkit.create_tool_group(\n            group_name=\"amap_tools\",\n            description=\"Map-related tools, including geocoding, routing, and \"\n            \"place search.\",\n        )\n        client = HttpStatelessClient(\n            name=\"amap_mcp\",\n            transport=\"streamable_http\",\n            url=f\"https://mcp.amap.com/mcp?key={os.environ['GAODE_API_KEY']}\",\n        )\n        await toolkit.register_mcp_client(client, group_name=\"amap_tools\")\n    else:\n        print(\n            \"Warning: GAODE_API_KEY not set in environment, skipping Gaode \"\n            \"MCP client registration.\",\n        )\n\n    # Browser MCP client\n    toolkit.create_tool_group(\n        group_name=\"browser_tools\",\n        description=\"Web browsing related tools.\",\n    )\n    browser_client = StdIOStatefulClient(\n        name=\"playwright-mcp\",\n        command=\"npx\",\n        args=[\"@playwright/mcp@latest\"],\n    )\n    await browser_client.connect()\n    await toolkit.register_mcp_client(\n        browser_client,\n        group_name=\"browser_tools\",\n    )\n\n    # GitHub MCP client\n    if os.getenv(\"GITHUB_TOKEN\"):\n        toolkit.create_tool_group(\n            group_name=\"github_tools\",\n            description=\"GitHub related tools, including repository \"\n            \"search and code file retrieval.\",\n        )\n        github_client = HttpStatelessClient(\n            name=\"github\",\n            transport=\"streamable_http\",\n            url=\"https://api.githubcopilot.com/mcp/\",\n            headers={\"Authorization\": f\"Bearer {os.getenv('GITHUB_TOKEN')}\"},\n        )\n        await toolkit.register_mcp_client(\n            github_client,\n            group_name=\"github_tools\",\n        )\n\n    else:\n        print(\n            \"Warning: GITHUB_TOKEN not set in environment, skipping GitHub \"\n            \"MCP client registration.\",\n        )\n\n    # Basic read/write tools\n    toolkit.register_tool_function(write_text_file)\n    toolkit.register_tool_function(insert_text_file)\n    toolkit.register_tool_function(view_text_file)\n\n    # Create a new sub-agent to finish the given task\n    sub_agent = ReActAgent(\n        name=\"Worker\",\n        sys_prompt=f\"\"\"You're an agent named Worker.\n\n## Your Target\nYour target is to finish the given task with your tools.\n\n## IMPORTANT\nYou MUST use the `{ReActAgent.finish_function_name}` to generate the final answer after finishing the task.\n\"\"\",  # noqa: E501  # pylint: disable=C0301\n        model=DashScopeChatModel(\n            model_name=\"qwen3-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        ),\n        enable_meta_tool=True,\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n        max_iters=20,\n    )\n\n    # disable the console output of the sub-agent\n    sub_agent.set_console_output_enabled(False)\n\n    # Collect the execution process content\n    msgs = OrderedDict()\n\n    # Wrap the sub-agent in a coroutine task to obtain the final\n    # structured output\n    result = []\n\n    async def call_sub_agent() -> None:\n        msg_res = await sub_agent(\n            Msg(\n                \"user\",\n                content=task_description,\n                role=\"user\",\n            ),\n            structured_model=ResultModel,\n        )\n        result.append(msg_res)\n\n    # Use stream_printing_message to get the streaming response as the\n    # sub-agent works\n    async for msg, _ in stream_printing_messages(\n        agents=[sub_agent],\n        coroutine_task=call_sub_agent(),\n    ):\n        msgs[msg.id] = msg\n\n        # Collect all the content blocks\n        yield ToolResponse(\n            content=_convert_to_text_block(\n                list(msgs.values()),\n            ),\n            stream=True,\n            is_last=False,\n        )\n\n        # Expose the interruption signal to the caller\n        if msg.metadata and msg.metadata.get(\"_is_interrupted\", False):\n            raise asyncio.CancelledError()\n\n    # Obtain the last message from the coroutine task\n    if result:\n        yield ToolResponse(\n            content=[\n                *_convert_to_text_block(\n                    list(msgs.values()),\n                ),\n                TextBlock(\n                    type=\"text\",\n                    text=json.dumps(\n                        result[0].metadata,\n                        indent=2,\n                        ensure_ascii=False,\n                    ),\n                ),\n            ],\n            stream=True,\n            is_last=True,\n        )\n\n    await browser_client.close()\n"
  },
  {
    "path": "examples/agent/react_agent/README.md",
    "content": "# ReAct Agent Example\n\nThis example showcases a **ReAct** agent in AgentScope. Specifically, the ReAct agent will discuss with the user in\nan alternative manner, i.e., chatbot style. It is equipped with a suite of tools to assist in answering user queries.\n\n> 💡 Tip: Try ``Ctrl+C`` to interrupt the agent's reply to experience the realtime steering/interruption feature!\n\n## Quick Start\n\nEnsure you have installed agentscope and set ``DASHSCOPE_API_KEY`` in your environment variables.\n\nRun the following commands to set up and run the example:\n\n```bash\npython main.py\n```\n\n> Note:\n> - The example is built with DashScope chat model. If you want to change the model used in this example, don't\n> forget to change the formatter at the same time! The corresponding relationship between built-in models and\n> formatters are list in [our tutorial](https://doc.agentscope.io/tutorial/task_prompt.html#id1)\n> - For local models, ensure the model service (like Ollama) is running before starting the agent.\n"
  },
  {
    "path": "examples/agent/react_agent/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The main entry point of the ReAct agent example.\"\"\"\nimport asyncio\nimport os\n\nfrom agentscope.agent import ReActAgent, UserAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import (\n    Toolkit,\n    execute_shell_command,\n    execute_python_code,\n    view_text_file,\n)\n\n\nasync def main() -> None:\n    \"\"\"The main entry point for the ReAct agent example.\"\"\"\n    toolkit = Toolkit()\n\n    toolkit.register_tool_function(execute_shell_command)\n    toolkit.register_tool_function(execute_python_code)\n    toolkit.register_tool_function(view_text_file)\n\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"You are a helpful assistant named Friday.\",\n        model=DashScopeChatModel(\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            model_name=\"qwen-max\",\n            enable_thinking=False,\n            stream=True,\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n        memory=InMemoryMemory(),\n    )\n\n    user = UserAgent(\"User\")\n\n    msg = None\n    while True:\n        msg = await user(msg)\n        if msg.get_text_content() == \"exit\":\n            break\n        msg = await agent(msg)\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/agent/realtime_voice_agent/README.md",
    "content": "# Realtime Voice Agent Example\n\nThis example demonstrates how to build a **real-time voice conversation agent** using AgentScope's RealtimeAgent. The agent supports bidirectional voice streaming, enabling natural voice conversations with low latency and real-time audio transcription.\n\n## Prerequisites\n\n- Python 3.10 or higher\n- Your DashScope API key in an environment variable `DASHSCOPE_API_KEY`\n\nInstall the required packages:\n\n```bash\nuv pip install agentscope fastapi uvicorn websockets\n# or\n# pip install agentscope\n```\n\n## Usage\n\n### 1. Start the Server\n\nRun the FastAPI server:\n\n```bash\ncd examples/agent/realtime_voice_agent\npython run_server.py\n```\n\nThe server will start on `http://localhost:8000` by default.\n\n### 2. Open the Web Interface\n\nOpen your web browser and navigate to:\n\n```\nhttp://localhost:8000\n```\n\nYou will see a web interface with:\n- Configuration panel (instructions and user name)\n- Voice control buttons (Start Recording, Stop Recording, Disconnect)\n- Video recording button (Start Video Recording)\n- Text input field\n- Message display area\n- Video preview area (when video recording is active)\n\n### 3. Start Conversation\n\n1. **Configure the Agent** (optional):\n   - Modify the \"Instructions\" to customize the agent's behavior\n   - Enter your name in the \"User Name\" field\n\n2. **Start Voice Recording**:\n   - Click the \"🎤 Start Recording\" button\n   - Allow microphone access when prompted by your browser\n   - Speak naturally to the agent\n   - The agent will respond with voice and text\n\n3. **Stop Recording**:\n   - Click \"⏹️ Stop Recording\" to pause voice input\n\n4. **Video Recording** (Optional):\n   - Click the \"📹 Start Video Recording\" button to start video recording\n   - Allow camera access when prompted by your browser\n   - The system will automatically capture and send video frames to the server at 1 frame per second (1 fps)\n   - A video preview will be displayed while recording\n   - Click \"🔴 Stop Video Recording\" to stop recording\n   - **Note**: Video recording requires an active voice chat session. Please start voice chat first before starting video recording.\n\n## Switching Models\n\nAgentScope supports multiple realtime voice models. By default, this example uses DashScope's `qwen3-omni-flash-realtime` model, but you can easily switch to other providers.\n\n### Supported Models\n\n- **GeminiRealtimeModel**\n- **OpenAIRealtimeModel**\n\n### How to Switch Models\n\nEdit `run_server.py` and replace the model initialization code:\n\n**For OpenAI:**\n\n```python\nfrom agentscope.realtime import OpenAIRealtimeModel\n\nagent = RealtimeAgent(\n    name=\"Friday\",\n    sys_prompt=sys_prompt,\n    model=OpenAIRealtimeModel(\n        model_name=\"gpt-4o-realtime-preview\",\n        api_key=os.getenv(\"OPENAI_API_KEY\"),\n        voice=\"alloy\",  # Options: \"alloy\", \"echo\", \"marin\", \"cedar\"\n    ),\n)\n```\n\n**For Gemini:**\n\n```python\nfrom agentscope.realtime import GeminiRealtimeModel\n\nagent = RealtimeAgent(\n    name=\"Friday\",\n    sys_prompt=sys_prompt,\n    model=GeminiRealtimeModel(\n        model_name=\"gemini-2.5-flash-native-audio-preview-09-2025\",\n        api_key=os.getenv(\"GEMINI_API_KEY\"),\n        voice=\"Puck\",  # Options: \"Puck\", \"Charon\", \"Kore\", \"Fenrir\"\n    ),\n)\n```\n\nDon't forget to set the corresponding API key environment variable before starting the server!\n\n"
  },
  {
    "path": "examples/agent/realtime_voice_agent/chatbot.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <title>Realtime Chatbot with AgentScope</title>\n    <meta charset=\"UTF-8\">\n    <style>\n        * {\n            margin: 0;\n            padding: 0;\n            box-sizing: border-box;\n        }\n\n        body {\n            font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n            max-width: 900px;\n            margin: 0 auto;\n            padding: 2rem;\n            background: hsl(0, 0%, 98%);\n            color: hsl(222.2, 84%, 4.9%);\n            line-height: 1.5;\n            min-height: 100vh;\n            display: flex;\n            flex-direction: column;\n            gap: 1.5rem;\n        }\n\n        h1 {\n            font-size: 2rem;\n            font-weight: 600;\n            color: hsl(222.2, 84%, 4.9%);\n            letter-spacing: -0.025em;\n            flex-shrink: 0;\n        }\n\n        #messages {\n            border: 1px solid hsl(214.3, 31.8%, 91.4%);\n            min-height: 300px;\n            flex: 1;\n            overflow-y: auto;\n            padding: 1rem;\n            background: hsl(0, 0%, 100%);\n            border-radius: 0.5rem;\n            box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);\n        }\n\n        #messages::-webkit-scrollbar {\n            width: 8px;\n        }\n\n        #messages::-webkit-scrollbar-track {\n            background: hsl(210, 40%, 96.1%);\n            border-radius: 4px;\n        }\n\n        #messages::-webkit-scrollbar-thumb {\n            background: hsl(215.4, 16.3%, 56.9%);\n            border-radius: 4px;\n        }\n\n        #messages::-webkit-scrollbar-thumb:hover {\n            background: hsl(215.4, 16.3%, 46.9%);\n        }\n\n        input[type=\"text\"] {\n            width: 100%;\n            padding: 0.625rem 0.875rem;\n            font-size: 0.875rem;\n            border: 1px solid hsl(214.3, 31.8%, 91.4%);\n            border-radius: 0.375rem;\n            background: hsl(0, 0%, 100%);\n            color: hsl(222.2, 84%, 4.9%);\n            transition: all 0.15s ease;\n            outline: none;\n        }\n\n        input[type=\"text\"]:focus {\n            border-color: hsl(221.2, 83.2%, 53.3%);\n            box-shadow: 0 0 0 3px hsl(221.2, 83.2%, 53.3%, 0.1);\n        }\n\n        textarea {\n            width: 100%;\n            min-height: 100px;\n            padding: 0.625rem 0.875rem;\n            font-size: 0.875rem;\n            border: 1px solid hsl(214.3, 31.8%, 91.4%);\n            border-radius: 0.375rem;\n            background: hsl(0, 0%, 100%);\n            color: hsl(222.2, 84%, 4.9%);\n            transition: all 0.15s ease;\n            outline: none;\n            resize: vertical;\n            font-family: inherit;\n            line-height: 1.5;\n        }\n\n        textarea:focus {\n            border-color: hsl(221.2, 83.2%, 53.3%);\n            box-shadow: 0 0 0 3px hsl(221.2, 83.2%, 53.3%, 0.1);\n        }\n\n        button {\n            display: inline-flex;\n            align-items: center;\n            justify-content: center;\n            padding: 0.625rem 1rem;\n            font-size: 0.875rem;\n            font-weight: 500;\n            border: 1px solid transparent;\n            border-radius: 0.375rem;\n            cursor: pointer;\n            transition: all 0.15s ease;\n            box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);\n        }\n\n        button:active {\n            transform: scale(0.98);\n        }\n\n        button:focus-visible {\n            outline: 2px solid hsl(221.2, 83.2%, 53.3%);\n            outline-offset: 2px;\n        }\n\n        button:disabled {\n            opacity: 0.5;\n            cursor: not-allowed;\n        }\n\n        /* Primary button style */\n        button.btn-primary {\n            background: hsl(221.2, 83.2%, 53.3%);\n            color: hsl(0, 0%, 100%);\n            border-color: hsl(221.2, 83.2%, 53.3%);\n        }\n\n        button.btn-primary:hover:not(:disabled) {\n            background: hsl(221.2, 83.2%, 45%);\n            border-color: hsl(221.2, 83.2%, 45%);\n        }\n\n        button.btn-primary.recording,\n        button.btn-primary.recording-video {\n            background: hsl(0, 84.2%, 60.2%);\n            border-color: hsl(0, 84.2%, 60.2%);\n            animation: pulse 1.5s ease-in-out infinite;\n        }\n\n        button.btn-primary.recording:hover:not(:disabled),\n        button.btn-primary.recording-video:hover:not(:disabled) {\n            background: hsl(0, 84.2%, 50%);\n            border-color: hsl(0, 84.2%, 50%);\n        }\n\n        /* Secondary button style */\n        button.btn-secondary {\n            background: hsl(0, 0%, 100%);\n            color: hsl(222.2, 47.4%, 11.2%);\n            border-color: hsl(214.3, 31.8%, 91.4%);\n        }\n\n        button.btn-secondary:hover:not(:disabled) {\n            background: hsl(210, 40%, 98%);\n            border-color: hsl(214.3, 31.8%, 81.4%);\n        }\n\n        .message {\n            margin: 0.75rem 0;\n            padding: 0.75rem 1rem;\n            background: hsl(210, 40%, 98%);\n            border-radius: 0.5rem;\n            border: 1px solid hsl(214.3, 31.8%, 91.4%);\n            font-size: 0.875rem;\n        }\n\n        .message strong {\n            color: hsl(222.2, 47.4%, 11.2%);\n            font-weight: 600;\n        }\n\n        @keyframes pulse {\n            0%, 100% {\n                opacity: 1;\n                box-shadow: 0 0 0 0 hsl(0, 84.2%, 60.2%, 0.7);\n            }\n            50% {\n                opacity: 0.9;\n                box-shadow: 0 0 0 8px hsl(0, 84.2%, 60.2%, 0);\n            }\n        }\n\n        .controls {\n            display: flex;\n            gap: 0.75rem;\n            align-items: center;\n            flex-shrink: 0;\n        }\n\n        .controls #voiceBtn,\n        .controls #videoBtn {\n            flex: 1;\n        }\n\n        .controls button:not(#voiceBtn):not(#videoBtn) {\n            width: 140px;\n            flex-shrink: 0;\n        }\n\n        .text-input-container {\n            display: flex;\n            gap: 0.75rem;\n            align-items: center;\n            flex-shrink: 0;\n        }\n\n        .text-input-container input[type=\"text\"] {\n            flex: 1;\n        }\n\n        .text-input-container button {\n            width: 100px;\n            flex-shrink: 0;\n        }\n\n        .configuration-container {\n            background: hsl(0, 0%, 100%);\n            padding: 1.5rem;\n            border-radius: 0.5rem;\n            border: 1px solid hsl(214.3, 31.8%, 91.4%);\n            box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);\n            flex-shrink: 0;\n        }\n\n        .configuration-container h3 {\n            font-size: 1.125rem;\n            font-weight: 600;\n            margin-bottom: 1rem;\n            color: hsl(222.2, 84%, 4.9%);\n            display: flex;\n            align-items: center;\n            gap: 0.5rem;\n        }\n\n        .config-field {\n            margin-bottom: 1.25rem;\n        }\n\n        .config-field:last-child {\n            margin-bottom: 0;\n        }\n\n        .config-field label {\n            display: block;\n            font-weight: 500;\n            margin-bottom: 0.5rem;\n            color: hsl(222.2, 47.4%, 11.2%);\n            font-size: 0.875rem;\n        }\n\n        .error-message {\n            padding: 0.875rem 1rem;\n            background: hsl(0, 84.2%, 95%);\n            border: 1px solid hsl(0, 84.2%, 85%);\n            border-radius: 0.5rem;\n            display: none;\n            color: hsl(0, 84.2%, 30%);\n            font-size: 0.875rem;\n            font-weight: 500;\n            box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);\n            flex-shrink: 0;\n        }\n\n\n        .model-options {\n            display: flex;\n            flex-direction: row;\n            gap: 0.75rem;\n        }\n\n        .model-option {\n            flex: 1;\n            padding: 0.75rem;\n            border: 2px solid hsl(214.3, 31.8%, 91.4%);\n            border-radius: 0.5rem;\n            cursor: pointer;\n            transition: all 0.15s ease;\n            background: hsl(0, 0%, 100%);\n            display: flex;\n            flex-direction: column;\n            justify-content: center;\n            gap: 0.5rem;\n        }\n\n        .model-option:hover:not(.disabled) {\n            border-color: hsl(221.2, 83.2%, 53.3%);\n            background: hsl(221.2, 83.2%, 98%);\n        }\n\n        .model-option.selected {\n            border-color: hsl(221.2, 83.2%, 53.3%);\n            background: hsl(221.2, 83.2%, 95%);\n        }\n\n        .model-option.disabled {\n            opacity: 0.5;\n            cursor: not-allowed;\n            background: hsl(0, 0%, 98%);\n        }\n\n        .model-option-header {\n            display: flex;\n            align-items: center;\n            gap: 0.5rem;\n        }\n\n        .model-option input[type=\"radio\"] {\n            margin: 0;\n            cursor: pointer;\n            flex-shrink: 0;\n        }\n\n        .model-option.disabled input[type=\"radio\"] {\n            cursor: not-allowed;\n        }\n\n        .model-info {\n            display: flex;\n            flex-direction: column;\n            gap: 0.5rem;\n            flex: 1;\n        }\n\n        .model-name-line {\n            display: flex;\n            align-items: center;\n            gap: 0.5rem;\n            min-height: 1.25rem;\n        }\n\n        .model-name {\n            font-weight: 600;\n            color: hsl(222.2, 84%, 4.9%);\n        }\n\n        .model-unavailable-reason {\n            font-size: 0.625rem;\n            color: hsl(215.4, 16.3%, 56.9%);\n            font-style: italic;\n            white-space: nowrap;\n        }\n\n        .model-tags {\n            display: flex;\n            gap: 0.375rem;\n            flex-wrap: wrap;\n        }\n\n        .model-tag {\n            display: inline-flex;\n            align-items: center;\n            padding: 0.125rem 0.5rem;\n            font-size: 0.75rem;\n            font-weight: 500;\n            border-radius: 0.25rem;\n            background: hsl(214.3, 31.8%, 91.4%);\n            color: hsl(222.2, 47.4%, 11.2%);\n        }\n\n        .model-tag.text {\n            background: hsl(200, 95%, 90%);\n            color: hsl(200, 95%, 30%);\n        }\n\n        .model-tag.audio {\n            background: hsl(280, 85%, 90%);\n            color: hsl(280, 85%, 30%);\n        }\n\n        .model-tag.image {\n            background: hsl(25, 95%, 90%);\n            color: hsl(25, 95%, 30%);\n        }\n\n        .model-tag.tool {\n            background: hsl(142, 71%, 90%);\n            color: hsl(142, 71%, 30%);\n        }\n\n        .tools-label {\n            display: block;\n            font-weight: 500;\n            margin-bottom: 0.5rem;\n            color: hsl(222.2, 47.4%, 11.2%);\n            font-size: 0.875rem;\n        }\n\n        .tools-disabled-hint {\n            font-size: 0.75rem;\n            color: hsl(215.4, 16.3%, 56.9%);\n            font-weight: 400;\n            font-style: italic;\n            margin-left: 0.5rem;\n        }\n\n        .tools-list {\n            display: flex;\n            gap: 0.5rem;\n        }\n\n        .tool-item {\n            flex: 1;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            padding: 0.5rem 0.75rem;\n            background: hsl(0, 0%, 100%);\n            border: 2px solid hsl(214.3, 31.8%, 91.4%);\n            border-radius: 0.375rem;\n            font-size: 0.875rem;\n            color: hsl(222.2, 47.4%, 11.2%);\n            font-weight: 500;\n            transition: all 0.15s ease;\n        }\n\n        .tool-item.disabled {\n            background: hsl(0, 0%, 96%);\n            color: hsl(215.4, 16.3%, 56.9%);\n            border-color: hsl(214.3, 31.8%, 91.4%);\n            opacity: 0.6;\n        }\n\n        #videoPreview {\n            width: 100%;\n            max-width: 640px;\n            border-radius: 0.5rem;\n            border: 1px solid hsl(214.3, 31.8%, 91.4%);\n            background: hsl(0, 0%, 0%);\n            display: none;\n            margin: 1rem 0;\n            flex-shrink: 0;\n        }\n\n        #videoPreview.active {\n            display: block;\n        }\n\n        .video-container {\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            flex-shrink: 0;\n        }\n    </style>\n</head>\n<body>\n    <h1>Realtime Chatbot</h1>\n\n    <div class=\"configuration-container\">\n        <h3>⚙️ Configuration</h3>\n\n        <div class=\"config-field\">\n            <label for=\"instructions\">Instructions</label>\n            <textarea id=\"instructions\" placeholder=\"Enter agent instructions...\">You're a helpful assistant named Friday.</textarea>\n        </div>\n\n        <div class=\"config-field\">\n            <label for=\"agentName\">Agent Name</label>\n            <input type=\"text\" id=\"agentName\" placeholder=\"Enter agent name\" value=\"Friday\" />\n        </div>\n\n\n        <div class=\"config-field\">\n            <label>Model Provider</label>\n            <div class=\"model-options\" id=\"modelOptions\">\n                <label class=\"model-option\" data-provider=\"dashscope\">\n                    <div class=\"model-option-header\">\n                        <input type=\"radio\" name=\"modelProvider\" value=\"dashscope\" checked />\n                        <div class=\"model-info\">\n                            <div class=\"model-name-line\">\n                                <span class=\"model-name\">DashScope</span>\n                                <span class=\"model-unavailable-reason\" style=\"display: none;\"></span>\n                            </div>\n                            <div class=\"model-tags\">\n                                <span class=\"model-tag audio\">Audio</span>\n                                <span class=\"model-tag image\">Image</span>\n                            </div>\n                        </div>\n                    </div>\n                </label>\n                <label class=\"model-option\" data-provider=\"gemini\">\n                    <div class=\"model-option-header\">\n                        <input type=\"radio\" name=\"modelProvider\" value=\"gemini\" />\n                        <div class=\"model-info\">\n                            <div class=\"model-name-line\">\n                                <span class=\"model-name\">Gemini</span>\n                                <span class=\"model-unavailable-reason\" style=\"display: none;\"></span>\n                            </div>\n                            <div class=\"model-tags\">\n                                <span class=\"model-tag text\">Text</span>\n                                <span class=\"model-tag audio\">Audio</span>\n                                <span class=\"model-tag image\">Image</span>\n                                <span class=\"model-tag tool\">Tool</span>\n                            </div>\n                        </div>\n                    </div>\n                </label>\n                <label class=\"model-option\" data-provider=\"openai\">\n                    <div class=\"model-option-header\">\n                        <input type=\"radio\" name=\"modelProvider\" value=\"openai\" />\n                        <div class=\"model-info\">\n                            <div class=\"model-name-line\">\n                                <span class=\"model-name\">OpenAI</span>\n                                <span class=\"model-unavailable-reason\" style=\"display: none;\"></span>\n                            </div>\n                            <div class=\"model-tags\">\n                                <span class=\"model-tag text\">Text</span>\n                                <span class=\"model-tag audio\">Audio</span>\n                                <span class=\"model-tag tool\">Tool</span>\n                            </div>\n                        </div>\n                    </div>\n                </label>\n            </div>\n        </div>\n\n        <div class=\"config-field\">\n            <label class=\"tools-label\">\n                Equipped Tools\n                <span class=\"tools-disabled-hint\" id=\"toolsDisabledHint\" style=\"display: none;\">(Not supported by this model)</span>\n            </label>\n            <div class=\"tools-list\" id=\"toolsList\">\n                <div class=\"tool-item\" data-tool=\"execute_python_code\">🐍 execute_python_code</div>\n                <div class=\"tool-item\" data-tool=\"execute_shell_command\">💻 execute_shell_command</div>\n                <div class=\"tool-item\" data-tool=\"view_text_file\">📄 view_text_file</div>\n            </div>\n        </div>\n    </div>\n\n    <div id=\"errorMessage\" class=\"error-message\"></div>\n\n    <div class=\"controls\">\n        <button id=\"voiceBtn\" class=\"btn-primary\" onclick=\"toggleVoice()\">🎤 Start Voice Chat</button>\n        <button id=\"videoBtn\" class=\"btn-primary\" onclick=\"toggleVideo()\">📹 Start Video Recording</button>\n        <button class=\"btn-secondary\" onclick=\"disconnect()\">❌ Disconnect</button>\n    </div>\n\n    <div class=\"video-container\">\n        <video id=\"videoPreview\" autoplay muted playsinline></video>\n    </div>\n\n    <div class=\"text-input-container\">\n        <input type=\"text\" id=\"textInput\" placeholder=\"Type your message here...\" />\n        <button id=\"sendBtn\" class=\"btn-primary\" onclick=\"sendTextMessage()\" disabled>📤 Send</button>\n    </div>\n\n\n    <div id=\"messages\"></div>\n\n    <script>\n        let ws = null;\n        let audioContext = null;  // For recording, 16kHz\n        let playbackAudioContext = null;  // For playback, 24kHz\n        let mediaStream = null;\n        let videoStream = null;  // For video recording\n        let videoFrameInterval = null;  // Interval for sending video frames\n        let videoCanvas = null;  // Canvas for capturing video frames\n        let videoCanvasCtx = null;  // Canvas context\n        let isRecording = false;\n        let isRecordingVideo = false;\n        let isPlaying = false;\n        let audioPlaybackNode = null;\n        let audioPlaybackQueue = [];  // Store decoded Float32Array\n        let audioPlaybackIndex = 0;\n        let sessionId = \"session1\";  // Session ID\n        let sessionCreated = false;  // Track if session has been created\n\n        // Update send button state based on input and recording status\n        function updateSendButtonState() {\n            const textInput = document.getElementById(\"textInput\");\n            const sendBtn = document.getElementById(\"sendBtn\");\n            const hasText = textInput.value.trim().length > 0;\n            sendBtn.disabled = !(hasText && isRecording);\n        }\n\n        // Send text message to backend\n        function sendTextMessage() {\n            const textInput = document.getElementById(\"textInput\");\n            const text = textInput.value.trim();\n\n            if (!text || !isRecording) {\n                return;\n            }\n\n            if (!ws || ws.readyState !== WebSocket.OPEN) {\n                showError(\"⚠️ WebSocket is not connected!\");\n                return;\n            }\n\n            // Send ClientTextAppendEvent\n            ws.send(JSON.stringify({\n                type: \"client_text_append\",\n                session_id: sessionId,\n                text: text\n            }));\n\n            addMessage(\"You\", text);\n            textInput.value = \"\";\n            updateSendButtonState();\n        }\n\n        // Add event listener for text input\n        document.addEventListener(\"DOMContentLoaded\", function() {\n            const textInput = document.getElementById(\"textInput\");\n\n            // Update button state on input\n            textInput.addEventListener(\"input\", updateSendButtonState);\n\n            // Send on Enter key\n            textInput.addEventListener(\"keypress\", function(e) {\n                if (e.key === \"Enter\" && !document.getElementById(\"sendBtn\").disabled) {\n                    sendTextMessage();\n                }\n            });\n        });\n\n        // Used to accumulate transcript text\n        let currentTranscript = \"\";\n        let currentTranscriptElement = null;\n        let currentResponseTranscript = \"\";\n        let currentResponseTranscriptElement = null;\n\n        // Check available models on page load\n        async function checkAvailableModels() {\n            try {\n                const apiUrl = `${window.location.protocol}//${window.location.host}/api/check-models`;\n\n                const response = await fetch(apiUrl);\n                const availability = await response.json();\n\n                console.log(\"Model availability:\", availability);\n\n                let hasAvailableModel = false;\n\n                // Update UI based on availability\n                const modelOptions = document.querySelectorAll('.model-option');\n                modelOptions.forEach(option => {\n                    const provider = option.getAttribute('data-provider');\n                    const radio = option.querySelector('input[type=\"radio\"]');\n                    const unavailableReason = option.querySelector('.model-unavailable-reason');\n\n                    if (!availability[provider]) {\n                        option.classList.add('disabled');\n                        radio.disabled = true;\n\n                        // Show unavailable reason\n                        const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);\n                        unavailableReason.textContent = `(${providerName.toUpperCase()}_API_KEY not set)`;\n                        unavailableReason.style.display = 'inline';\n\n                        // If this was the selected option, uncheck it\n                        if (radio.checked) {\n                            radio.checked = false;\n                        }\n                    } else {\n                        hasAvailableModel = true;\n                        unavailableReason.style.display = 'none';\n                    }\n                });\n\n                // If no model is selected after checking availability, select first available\n                const selectedRadio = document.querySelector('input[name=\"modelProvider\"]:checked');\n                if (!selectedRadio && hasAvailableModel) {\n                    for (const option of modelOptions) {\n                        const provider = option.getAttribute('data-provider');\n                        if (availability[provider]) {\n                            option.querySelector('input[type=\"radio\"]').checked = true;\n                            option.classList.add('selected');\n                            break;\n                        }\n                    }\n                }\n\n                // Disable recording button if no model is available\n                const voiceBtn = document.getElementById('voiceBtn');\n                if (!hasAvailableModel) {\n                    voiceBtn.disabled = true;\n                    showError('⚠️ No model API keys configured. Please set at least one API key to start voice chat.');\n                } else {\n                    voiceBtn.disabled = false;\n                }\n\n                // Add click handlers for model options\n                modelOptions.forEach(option => {\n                    option.addEventListener('click', function() {\n                        if (!this.classList.contains('disabled')) {\n                            modelOptions.forEach(opt => opt.classList.remove('selected'));\n                            this.classList.add('selected');\n                            updateToolsDisplay();\n                        }\n                    });\n                });\n\n                // Mark initially selected option\n                const currentSelected = document.querySelector('input[name=\"modelProvider\"]:checked');\n                if (currentSelected) {\n                    currentSelected.closest('.model-option').classList.add('selected');\n                }\n\n                // Update tools display based on initial selection\n                updateToolsDisplay();\n\n            } catch (error) {\n                console.error(\"Failed to check model availability:\", error);\n                showError(\"⚠️ Failed to check model availability. Please refresh the page.\");\n            }\n        }\n\n        function updateToolsDisplay() {\n            const selectedRadio = document.querySelector('input[name=\"modelProvider\"]:checked');\n            const toolItems = document.querySelectorAll('.tool-item');\n            const toolsDisabledHint = document.getElementById('toolsDisabledHint');\n\n            if (selectedRadio) {\n                const provider = selectedRadio.value;\n                // Enable tools for Gemini and OpenAI, disable for others\n                if (provider === 'gemini' || provider === 'openai') {\n                    toolItems.forEach(item => item.classList.remove('disabled'));\n                    toolsDisabledHint.style.display = 'none';\n                } else {\n                    toolItems.forEach(item => item.classList.add('disabled'));\n                    toolsDisabledHint.style.display = 'inline';\n                }\n            } else {\n                toolItems.forEach(item => item.classList.add('disabled'));\n                toolsDisabledHint.style.display = 'inline';\n            }\n        }\n\n        function showError(message) {\n            const errorDiv = document.getElementById(\"errorMessage\");\n            errorDiv.innerText = message;\n            errorDiv.style.display = \"block\";\n            setTimeout(() => {\n                errorDiv.style.display = \"none\";\n            }, 5000);\n        }\n\n        async function connect() {\n            const userId = \"You\";\n            const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n            const wsUrl = `${protocol}//${window.location.host}/ws/${userId}/${sessionId}`;\n\n            console.log(`Connecting to WebSocket: ${wsUrl}`);\n            ws = new WebSocket(wsUrl);\n\n            ws.onopen = function() {\n                addMessage(\"System\", \"✅ WebSocket connected successfully, ready for voice conversation\");\n            };\n\n            ws.onmessage = async function(event) {\n                try {\n                    const data = JSON.parse(event.data);\n                    console.log(\"Received message:\", data);\n\n                    // Handle ServerEvents\n                    switch (data.type) {\n                        case \"server_session_created\":\n                            sessionCreated = true;\n                            addMessage(\"System\", `✅ Session created: ${data.session_id}`);\n                            break;\n\n                        case \"agent_ready\":\n                            addMessage(\"System\", `🤖 Agent ${data.agent_name} is ready`);\n                            break;\n\n                        case \"agent_response_created\":\n                            addMessage(\"System\", `💬 Agent ${data.agent_name} started generating response...`);\n                            break;\n\n                        case \"agent_response_audio_delta\":\n                            // Receive audio data and add to playback queue\n                            queueAudioChunk(data.delta);\n                            break;\n\n                        case \"agent_response_audio_done\":\n                            addMessage(\"System\", \"🔊 Audio response completed\");\n                            break;\n\n                        case \"agent_response_audio_transcript_delta\":\n                            // Agent response transcript text\n                            appendResponseTranscript(data.agent_name, data.delta || \"\");\n                            break;\n\n                        case \"agent_response_audio_transcript_done\":\n                            // Complete Agent response transcript message\n                            finishResponseTranscript();\n                            break;\n\n                        case \"agent_input_transcription_delta\":\n                            // User input transcript text\n                            appendTranscript(\"You\", data.delta || \"\");\n                            break;\n\n                        case \"agent_input_transcription_done\":\n                            appendTranscript(\"You\", data.transcript || \"\");\n                            // Complete user input transcript message\n                            finishTranscript();\n                            addMessage(\"System\", `📝 User input recognition completed`);\n                            break;\n\n                        case \"agent_input_started\":\n                            addMessage(\"System\", \"🎤 Voice input started\");\n                            break;\n\n                        case \"agent_input_done\":\n                            addMessage(\"System\", \"⏹️ Voice input ended\");\n                            break;\n\n                        case \"agent_response_done\":\n                            addMessage(\"System\", `✅ Response completed (input tokens: ${data.input_tokens}, output tokens: ${data.output_tokens})`);\n                            break;\n\n                        case \"agent_response_tool_use_delta\":\n                            addMessage(\"System\", `🔧 Tool call: ${data.name}`);\n                            break;\n\n                        case \"agent_response_tool_use_done\":\n                            // Display tool use with complete information\n                            const toolUseInfo = JSON.stringify(data.tool_use, null, 2);\n                            addMessage(data.agent_name, `🔧 Tool Use:\\n${toolUseInfo}`);\n                            break;\n\n                        case \"agent_response_tool_result\":\n                            // Display tool result with complete information\n                            const toolResultInfo = JSON.stringify(data.tool_result, null, 2);\n                            addMessage(data.agent_name, `✅ Tool Result:\\n${toolResultInfo}`);\n                            break;\n\n                        case \"agent_error\":\n                            addMessage(\"Error\", `❌ ${data.error_type}: ${data.message}`);\n                            break;\n\n                        case \"agent_ended\":\n                            addMessage(\"System\", `👋 Agent ${data.agent_name} has ended`);\n                            break;\n\n                        case \"server_session_ended\":\n                            addMessage(\"System\", `🔚 Session ${data.session_id} has ended`);\n                            break;\n\n                        default:\n                            console.log(\"Unhandled event type:\", data.type);\n                            break;\n                    }\n                } catch (e) {\n                    console.error(\"Error processing message:\", e);\n                }\n            };\n\n            ws.onclose = function() {\n                addMessage(\"System\", \"❌ Disconnected\");\n                stopVoice();\n                stopVideoRecording();  // Also stop video recording on disconnect\n                sessionCreated = false;  // Reset session state\n                updateSendButtonState();\n            };\n\n            ws.onerror = function() {\n                addMessage(\"System\", \"⚠️ Connection error\");\n            };\n        }\n\n        async function toggleVoice() {\n            if (!isRecording) {\n                await startVoice();\n            } else {\n                stopVoice();\n            }\n        }\n\n        async function startVoice() {\n            try {\n                // Check if recording button is disabled\n                const voiceBtn = document.getElementById(\"voiceBtn\");\n                if (voiceBtn.disabled) {\n                    showError(\"⚠️ No model API keys configured. Please set at least one API key to start voice chat.\");\n                    return;\n                }\n\n                // Validate instructions\n                const instructions = document.getElementById(\"instructions\").value.trim();\n                if (!instructions) {\n                    showError(\"⚠️ Instructions cannot be empty! Please enter instructions before starting voice chat.\");\n                    return;\n                }\n\n                // Check if WebSocket is connected\n                if (!ws || ws.readyState !== WebSocket.OPEN) {\n                    showError(\"⚠️ WebSocket is not connected! Please wait for connection.\");\n                    return;\n                }\n\n                // Send session create event if not already created\n                if (!sessionCreated) {\n                    const agentName = document.getElementById(\"agentName\").value.trim() || \"Friday\";\n                    const selectedModel = document.querySelector('input[name=\"modelProvider\"]:checked');\n                    const modelProvider = selectedModel ? selectedModel.value : \"dashscope\";\n\n                    addMessage(\"System\", \"📝 Creating session with instructions...\");\n                    ws.send(JSON.stringify({\n                        type: \"client_session_create\",\n                        config: {\n                            instructions: instructions,\n                            agent_name: agentName,\n                            model_provider: modelProvider\n                        }\n                    }));\n\n                    // Wait for session_created event before proceeding\n                    // We'll set a timeout to wait for session creation\n                    await new Promise((resolve, reject) => {\n                        const timeout = setTimeout(() => {\n                            reject(new Error(\"Session creation timeout\"));\n                        }, 5000);\n\n                        const checkSession = setInterval(() => {\n                            if (sessionCreated) {\n                                clearTimeout(timeout);\n                                clearInterval(checkSession);\n                                resolve();\n                            }\n                        }, 100);\n                    });\n                }\n\n                if (!audioContext) {\n                    audioContext = new (window.AudioContext || window.webkitAudioContext)({\n                        sampleRate: 16000\n                    });\n                }\n\n                mediaStream = await navigator.mediaDevices.getUserMedia({\n                    audio: {\n                        echoCancellation: true,\n                        noiseSuppression: true,\n                        sampleRate: 16000\n                    }\n                });\n\n                const source = audioContext.createMediaStreamSource(mediaStream);\n\n                // Use ScriptProcessorNode to process audio\n                const processor = audioContext.createScriptProcessor(4096, 1, 1);\n\n                let audioChunkCount = 0;\n                processor.onaudioprocess = function(e) {\n                    if (!isRecording) return;\n\n                    const inputData = e.inputBuffer.getChannelData(0);\n                    const pcmData = convertToPCM16(inputData);\n                    const base64Audio = arrayBufferToBase64(pcmData);\n\n                    if (ws && ws.readyState === WebSocket.OPEN) {\n                        audioChunkCount++;\n                        if (audioChunkCount % 10 === 0) {\n                            console.log(`Sending audio chunk ${audioChunkCount}`);\n                        }\n                        // Send ClientAudioAppendEvent\n                        ws.send(JSON.stringify({\n                            type: \"client_audio_append\",\n                            session_id: sessionId,\n                            audio: base64Audio,\n                            format: {\n                                rate: 16000,\n                                type: \"audio/pcm\",\n                            }\n                        }));\n                    }\n                };\n\n                source.connect(processor);\n                const dummyGain = audioContext.createGain();\n                dummyGain.gain.value = 0;  // Mute to avoid feedback\n                processor.connect(dummyGain);\n                dummyGain.connect(audioContext.destination);\n\n                isRecording = true;\n                document.getElementById(\"voiceBtn\").classList.add(\"recording\");\n                document.getElementById(\"voiceBtn\").innerText = \"🔴 Voice Chat Active\";\n                addMessage(\"System\", \"🎤 Voice chat started...\");\n                updateSendButtonState();\n\n            } catch (err) {\n                console.error(\"Failed to start recording:\", err);\n                if (err.message === \"Session creation timeout\") {\n                    showError(\"⚠️ Session creation timeout. Please try again.\");\n                    addMessage(\"System\", \"⚠️ Session creation timeout\");\n                } else {\n                    showError(\"⚠️ Unable to access microphone: \" + err.message);\n                    addMessage(\"System\", \"⚠️ Unable to access microphone: \" + err.message);\n                }\n            }\n        }\n\n        function stopVoice() {\n            isRecording = false;\n\n            if (mediaStream) {\n                mediaStream.getTracks().forEach(track => track.stop());\n                mediaStream = null;\n            }\n\n            // Notify server that recording has stopped - send ClientAudioCommitEvent\n            if (ws && ws.readyState === WebSocket.OPEN) {\n                ws.send(JSON.stringify({\n                    type: \"client_audio_commit\",\n                    session_id: sessionId\n                }));\n            }\n\n            document.getElementById(\"voiceBtn\").classList.remove(\"recording\");\n            document.getElementById(\"voiceBtn\").innerText = \"🎤 Start Voice Chat\";\n            addMessage(\"System\", \"⏹️ Voice chat stopped\");\n            updateSendButtonState();\n        }\n\n        function convertToPCM16(float32Array) {\n            const int16Array = new Int16Array(float32Array.length);\n            for (let i = 0; i < float32Array.length; i++) {\n                const s = Math.max(-1, Math.min(1, float32Array[i]));\n                int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;\n            }\n            return int16Array.buffer;\n        }\n\n        function arrayBufferToBase64(buffer) {\n            const bytes = new Uint8Array(buffer);\n            let binary = '';\n            for (let i = 0; i < bytes.byteLength; i++) {\n                binary += String.fromCharCode(bytes[i]);\n            }\n            return btoa(binary);\n        }\n\n        function queueAudioChunk(base64Audio) {\n            try {\n                // Decode base64 audio data and convert to Float32Array\n                const binaryString = atob(base64Audio);\n                const bytes = new Uint8Array(binaryString.length);\n                for (let i = 0; i < binaryString.length; i++) {\n                    bytes[i] = binaryString.charCodeAt(i);\n                }\n\n                // Convert to Int16Array (PCM16), then to Float32Array\n                const int16Array = new Int16Array(bytes.buffer);\n                const float32Array = new Float32Array(int16Array.length);\n\n                for (let i = 0; i < int16Array.length; i++) {\n                    float32Array[i] = int16Array[i] / 32768.0;\n                }\n\n                // Add decoded audio data to queue\n                audioPlaybackQueue.push(float32Array);\n\n                // If not playing yet, start player\n                if (!isPlaying) {\n                    startAudioPlayback();\n                }\n            } catch (err) {\n                console.error(\"Failed to decode audio chunk:\", err);\n            }\n        }\n\n        function startAudioPlayback() {\n            if (isPlaying) return;\n\n            try {\n                // Create separate AudioContext for playback\n                if (!playbackAudioContext) {\n                    playbackAudioContext = new (window.AudioContext || window.webkitAudioContext)({\n                        sampleRate: 24000\n                    });\n                }\n\n                // If AudioContext is suspended (browser policy), resume it\n                if (playbackAudioContext.state === 'suspended') {\n                    playbackAudioContext.resume();\n                }\n\n                isPlaying = true;\n                audioPlaybackIndex = 0;\n\n                // Use ScriptProcessorNode for streaming playback\n                const bufferSize = 4096;\n                const processor = playbackAudioContext.createScriptProcessor(bufferSize, 0, 1);\n\n                processor.onaudioprocess = function(e) {\n                    const output = e.outputBuffer.getChannelData(0);\n                    const samplesNeeded = output.length;\n                    let samplesWritten = 0;\n\n                    // Get audio data from queue and fill output buffer\n                    while (samplesWritten < samplesNeeded && audioPlaybackQueue.length > 0) {\n                        const chunk = audioPlaybackQueue[0];\n\n                        // Calculate number of samples to read from current chunk\n                        const samplesToRead = Math.min(\n                            samplesNeeded - samplesWritten,\n                            chunk.length - audioPlaybackIndex\n                        );\n\n                        // Directly copy Float32 data to output\n                        for (let i = 0; i < samplesToRead; i++) {\n                            output[samplesWritten + i] = chunk[audioPlaybackIndex + i];\n                        }\n\n                        samplesWritten += samplesToRead;\n                        audioPlaybackIndex += samplesToRead;\n\n                        // If current chunk is finished, remove it and reset index\n                        if (audioPlaybackIndex >= chunk.length) {\n                            audioPlaybackQueue.shift();\n                            audioPlaybackIndex = 0;\n                        }\n                    }\n\n                    // If queue is empty and no more data, fill with silence\n                    if (samplesWritten < samplesNeeded) {\n                        for (let i = samplesWritten; i < samplesNeeded; i++) {\n                            output[i] = 0;\n                        }\n\n                        // If queue continues to be empty for a while, stop playback\n                        if (audioPlaybackQueue.length === 0) {\n                            setTimeout(() => {\n                                if (audioPlaybackQueue.length === 0) {\n                                    stopAudioPlayback();\n                                }\n                            }, 100);\n                        }\n                    }\n                };\n\n                processor.connect(playbackAudioContext.destination);\n                audioPlaybackNode = processor;\n\n            } catch (err) {\n                console.error(\"Failed to start audio playback:\", err);\n                isPlaying = false;\n            }\n        }\n\n        function stopAudioPlayback() {\n            if (audioPlaybackNode) {\n                audioPlaybackNode.disconnect();\n                audioPlaybackNode = null;\n            }\n            isPlaying = false;\n            audioPlaybackQueue = [];\n            audioPlaybackIndex = 0;\n        }\n\n\n        /**\n         * Toggles video recording on and off by calling the\n         * start or stop functions.\n         */\n        async function toggleVideo() {\n            if (!isRecordingVideo) {\n                await startVideoRecording();\n            } else {\n                stopVideoRecording();\n            }\n        }\n\n        /**\n         * Starts video recording by requesting camera access, displaying a\n         * live preview, and initiating frame capture to the server at 1 fps.\n         * Requires an active voice chat session (WebSocket connected and\n         * session created).\n         */\n        async function startVideoRecording() {\n            // Prevent duplicate starts\n            if (isRecordingVideo) {\n                return;\n            }\n\n            try {\n                const videoBtn = document.getElementById(\"videoBtn\");\n                const videoPreview = document.getElementById(\"videoPreview\");\n\n                // Check if WebSocket is connected\n                if (!ws || ws.readyState !== WebSocket.OPEN) {\n                    showError(\"⚠️ WebSocket is not connected! Please wait for connection.\");\n                    return;\n                }\n\n                // Check if session is created\n                if (!sessionCreated) {\n                    showError(\"⚠️ Session not created yet! Please start voice chat first.\");\n                    return;\n                }\n\n                // Request video stream only (audio is already captured by voice chat)\n                videoStream = await navigator.mediaDevices.getUserMedia({\n                    video: {\n                        width: { ideal: 1280 },\n                        height: { ideal: 720 },\n                        facingMode: \"user\"\n                    },\n                    audio: false\n                });\n\n                // Show video preview\n                videoPreview.srcObject = videoStream;\n                videoPreview.classList.add(\"active\");\n\n                // Start sending video frames to server at 1 fps\n                startSendingVideoFrames();\n\n                isRecordingVideo = true;\n                videoBtn.classList.add(\"recording-video\");\n                videoBtn.innerText = \"🔴 Stop Video Recording\";\n                addMessage(\"System\", \"📹 Video recording started, sending frames to server at 1 fps...\");\n\n            } catch (err) {\n                console.error(\"Failed to start video recording:\", err);\n                showError(\"⚠️ Unable to access camera: \" + err.message);\n                addMessage(\"System\", \"⚠️ Unable to access camera: \" + err.message);\n            }\n        }\n\n        /**\n         * Captures video frames from the preview element at 1 fps and sends\n         * them to the server as base64-encoded JPEG images via WebSocket\n         * using the client_image_append event.\n         */\n        function startSendingVideoFrames() {\n            if (videoFrameInterval) {\n                clearInterval(videoFrameInterval);\n            }\n\n            const videoPreview = document.getElementById(\"videoPreview\");\n\n            // Create canvas if needed\n            if (!videoCanvas) {\n                videoCanvas = document.createElement('canvas');\n                videoCanvasCtx = videoCanvas.getContext('2d');\n            }\n\n            // Send frame every second (1 fps)\n            videoFrameInterval = setInterval(function() {\n                if (!isRecordingVideo || !videoStream || !videoPreview) {\n                    return;\n                }\n\n                try {\n                    // Check if video is ready\n                    if (videoPreview.readyState < 2 || !videoPreview.videoWidth || !videoPreview.videoHeight) {\n                        return;\n                    }\n\n                    // Set canvas size to match video\n                    const videoWidth = videoPreview.videoWidth;\n                    const videoHeight = videoPreview.videoHeight;\n                    if (videoCanvas.width !== videoWidth || videoCanvas.height !== videoHeight) {\n                        videoCanvas.width = videoWidth;\n                        videoCanvas.height = videoHeight;\n                    }\n\n                    // Capture frame and convert to base64 JPEG\n                    videoCanvasCtx.drawImage(videoPreview, 0, 0, videoWidth, videoHeight);\n                    const base64Data = videoCanvas.toDataURL('image/jpeg', 0.8).split(',')[1];\n\n                    // Send to server\n                    if (ws && ws.readyState === WebSocket.OPEN && sessionCreated) {\n                        ws.send(JSON.stringify({\n                            type: \"client_image_append\",\n                            session_id: sessionId,\n                            data: base64Data,\n                            format: {\n                                type: \"image/jpeg\",\n                                mime_type: \"image/jpeg\"\n                            }\n                        }));\n                    }\n                } catch (err) {\n                    console.error(\"Failed to capture video frame:\", err);\n                }\n            }, 1000);\n        }\n\n        /**\n         * Stops video recording by clearing the frame capture interval,\n         * releasing the camera stream, and resetting the UI state.\n         */\n        function stopVideoRecording() {\n            if (videoFrameInterval) {\n                clearInterval(videoFrameInterval);\n                videoFrameInterval = null;\n            }\n\n            if (videoStream) {\n                videoStream.getTracks().forEach(track => track.stop());\n                videoStream = null;\n            }\n\n            const videoPreview = document.getElementById(\"videoPreview\");\n            if (videoPreview) {\n                videoPreview.srcObject = null;\n                videoPreview.classList.remove(\"active\");\n            }\n\n            const videoBtn = document.getElementById(\"videoBtn\");\n            isRecordingVideo = false;\n            if (videoBtn) {\n                videoBtn.classList.remove(\"recording-video\");\n                videoBtn.innerText = \"📹 Start Video Recording\";\n            }\n            addMessage(\"System\", \"⏹️ Video recording stopped\");\n        }\n\n        function disconnect() {\n            stopVoice();\n            stopVideoRecording();\n            stopAudioPlayback();\n            if (ws) {\n                ws.close();\n            }\n            sessionCreated = false;  // Reset session state\n            updateSendButtonState();\n        }\n\n\n        function addMessage(sender, message) {\n            const messagesDiv = document.getElementById(\"messages\");\n            const messageDiv = document.createElement(\"div\");\n            messageDiv.className = \"message\";\n            const time = new Date().toLocaleTimeString();\n\n            // Check if message contains newlines (like JSON), use <pre> for formatting\n            if (message.includes('\\n')) {\n                messageDiv.innerHTML = `<strong>[${time}] ${sender}:</strong><pre style=\"margin: 0.5rem 0; padding: 0.5rem; background: hsl(210, 40%, 96.1%); border-radius: 0.25rem; overflow-x: auto; font-size: 0.75rem;\">${message}</pre>`;\n            } else {\n                messageDiv.innerHTML = `<strong>[${time}] ${sender}:</strong> ${message}`;\n            }\n\n            messagesDiv.insertBefore(messageDiv, messagesDiv.firstChild);\n            messagesDiv.scrollTop = 0;\n        }\n\n        function appendTranscript(sender, text) {\n            const messagesDiv = document.getElementById(\"messages\");\n\n            // If there's no current message element yet, create a new one\n            if (!currentTranscriptElement) {\n                currentTranscript = \"\";\n                currentTranscriptElement = document.createElement(\"div\");\n                currentTranscriptElement.className = \"message\";\n                const time = new Date().toLocaleTimeString();\n                currentTranscriptElement.innerHTML = `<strong>[${time}] ${sender}:</strong> <span class=\"transcript-content\"></span>`;\n                messagesDiv.insertBefore(currentTranscriptElement, messagesDiv.firstChild);\n            }\n\n            // Accumulate text\n            currentTranscript += text;\n\n            // Update displayed content\n            const contentSpan = currentTranscriptElement.querySelector('.transcript-content');\n            if (contentSpan) {\n                contentSpan.textContent = currentTranscript;\n            }\n\n            // Scroll to top\n            messagesDiv.scrollTop = 0;\n        }\n\n        function finishTranscript() {\n            // Complete current transcript message, prepare for next one\n            currentTranscript = \"\";\n            currentTranscriptElement = null;\n        }\n\n        function appendResponseTranscript(sender, text) {\n            const messagesDiv = document.getElementById(\"messages\");\n\n            // If there's no current response message element yet, create a new one\n            if (!currentResponseTranscriptElement) {\n                currentResponseTranscript = \"\";\n                currentResponseTranscriptElement = document.createElement(\"div\");\n                currentResponseTranscriptElement.className = \"message\";\n                const time = new Date().toLocaleTimeString();\n                currentResponseTranscriptElement.innerHTML = `<strong>[${time}] ${sender}:</strong> <span class=\"response-transcript-content\"></span>`;\n                messagesDiv.insertBefore(currentResponseTranscriptElement, messagesDiv.firstChild);\n            }\n\n            // Accumulate text\n            currentResponseTranscript += text;\n\n            // Update displayed content\n            const contentSpan = currentResponseTranscriptElement.querySelector('.response-transcript-content');\n            if (contentSpan) {\n                contentSpan.textContent = currentResponseTranscript;\n            }\n\n            // Scroll to top\n            messagesDiv.scrollTop = 0;\n        }\n\n        function finishResponseTranscript() {\n            // Complete current response transcript message, prepare for next one\n            currentResponseTranscript = \"\";\n            currentResponseTranscriptElement = null;\n        }\n\n\n        // Check available models and auto-connect when page loads\n        window.onload = async function() {\n            await checkAvailableModels();\n            connect();\n        };\n    </script>\n</body>\n</html>"
  },
  {
    "path": "examples/agent/realtime_voice_agent/run_server.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"A test server\"\"\"\nimport asyncio\nimport os\nimport traceback\nfrom pathlib import Path\n\nimport uvicorn\nfrom fastapi import FastAPI, WebSocket\nfrom fastapi.responses import FileResponse\n\nfrom agentscope import logger\nfrom agentscope.agent import RealtimeAgent\nfrom agentscope.realtime import (\n    DashScopeRealtimeModel,\n    GeminiRealtimeModel,\n    OpenAIRealtimeModel,\n    ClientEvents,\n    ServerEvents,\n    ClientEventType,\n)\nfrom agentscope.tool import (\n    Toolkit,\n    execute_python_code,\n    execute_shell_command,\n    view_text_file,\n)\n\napp = FastAPI()\n\n\n@app.get(\"/\")\nasync def get() -> FileResponse:\n    \"\"\"Serve the HTML test page.\"\"\"\n    html_path = Path(__file__).parent / \"chatbot.html\"\n    return FileResponse(html_path)\n\n\n@app.get(\"/api/check-models\")\nasync def check_models() -> dict:\n    \"\"\"Check which model API keys are available in environment variables.\"\"\"\n    return {\n        \"dashscope\": bool(os.getenv(\"DASHSCOPE_API_KEY\")),\n        \"gemini\": bool(os.getenv(\"GEMINI_API_KEY\")),\n        \"openai\": bool(os.getenv(\"OPENAI_API_KEY\")),\n    }\n\n\nasync def frontend_receive(\n    websocket: WebSocket,\n    frontend_queue: asyncio.Queue,\n) -> None:\n    \"\"\"Forward the message received from the agent to the frontend.\"\"\"\n    try:\n        while True:\n            msg: ServerEvents.EventBase = await frontend_queue.get()\n\n            # Send the message as JSON\n            await websocket.send_json(msg.model_dump())\n\n    except Exception as e:\n        print(f\"[ERROR] frontend_receive error: {e}\")\n        traceback.print_exc()\n\n\n@app.websocket(\"/ws/{user_id}/{session_id}\")\nasync def single_agent_endpoint(\n    websocket: WebSocket,\n    user_id: str,\n    session_id: str,\n) -> None:\n    \"\"\"WebSocket endpoint for a single realtime agent.\"\"\"\n    try:\n        await websocket.accept()\n\n        logger.info(\n            \"Connected to WebSocket: user_id=%s, session_id=%s\",\n            user_id,\n            session_id,\n        )\n\n        # Create the queue to forward messages to the frontend\n        frontend_queue = asyncio.Queue()\n        asyncio.create_task(\n            frontend_receive(websocket, frontend_queue),\n        )\n\n        # Create the realtime agent\n        agent = None\n\n        while True:\n            # Handle the incoming messages from the frontend\n            # i.e. ClientEvents\n            data = await websocket.receive_json()\n\n            client_event = ClientEvents.from_json(data)\n\n            if isinstance(\n                client_event,\n                ClientEvents.ClientSessionCreateEvent,\n            ):\n                # Create the agent by the given session arguments\n                instructions = client_event.config.get(\n                    \"instructions\",\n                    \"You're a helpful assistant.\",\n                )\n                agent_name = client_event.config.get(\"agent_name\", \"Friday\")\n                model_provider = client_event.config.get(\n                    \"model_provider\",\n                    \"dashscope\",\n                )\n\n                sys_prompt = instructions\n\n                # Create toolkit with tools for models that support them\n                toolkit = None\n                if model_provider in [\"gemini\", \"openai\"]:\n                    toolkit = Toolkit()\n                    toolkit.register_tool_function(execute_python_code)\n                    toolkit.register_tool_function(execute_shell_command)\n                    toolkit.register_tool_function(view_text_file)\n\n                # Create the appropriate model based on provider\n                if model_provider == \"dashscope\":\n                    model = DashScopeRealtimeModel(\n                        model_name=\"qwen3-omni-flash-realtime\",\n                        api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n                    )\n                elif model_provider == \"gemini\":\n                    model = GeminiRealtimeModel(\n                        model_name=(\n                            \"gemini-2.5-flash-native-audio-preview-09-2025\"\n                        ),\n                        api_key=os.getenv(\"GEMINI_API_KEY\"),\n                    )\n                elif model_provider == \"openai\":\n                    model = OpenAIRealtimeModel(\n                        model_name=\"gpt-4o-realtime-preview\",\n                        api_key=os.getenv(\"OPENAI_API_KEY\"),\n                    )\n                else:\n                    raise ValueError(\n                        f\"Unsupported model provider: {model_provider}\",\n                    )\n\n                # Create the agent\n                agent = RealtimeAgent(\n                    name=agent_name,\n                    sys_prompt=sys_prompt,\n                    model=model,\n                    toolkit=toolkit,\n                )\n\n                await agent.start(frontend_queue)\n\n                # Send session_created event to frontend\n                await websocket.send_json(\n                    ServerEvents.ServerSessionCreatedEvent(\n                        session_id=session_id,\n                    ).model_dump(),\n                )\n                print(\n                    f\"Session created successfully: {session_id}\",\n                )\n\n            elif client_event.type == ClientEventType.CLIENT_SESSION_END:\n                # End the session with the agent\n                if agent:\n                    await agent.stop()\n                    agent = None\n\n            else:\n                await agent.handle_input(client_event)\n\n    except Exception as e:\n        print(f\"[ERROR] WebSocket endpoint error: {e}\")\n        traceback.print_exc()\n        raise\n\n\nif __name__ == \"__main__\":\n    uvicorn.run(\n        \"run_server:app\",\n        host=\"localhost\",\n        port=8000,\n        reload=True,\n        log_level=\"info\",\n    )\n"
  },
  {
    "path": "examples/agent/voice_agent/README.md",
    "content": "# Voice Agent\n\n> This is experimental functionality in AgentScope.\n\nThis example demonstrates how to create a voice agent using AgentScope with Qwen-Omni model, featuring both text and audio output capabilities.\n\n> **Note**:\n>  - Qwen-Omni may not generate tool calls when the audio output is enabled.\n>  - This example supports DashScope `Qwen-Omni` and OpenAI `GPT-4o Audio` models. You can change model by modifying the `model` parameter in `main.py`.\n>  - We haven't tested vLLM yet. Contributions are welcome!\n\n## Quick Start\n\nEnsure you have installed agentscope and set ``DASHSCOPE_API_KEY`` in your environment variables.\n\nRun the following commands to set up and run the example:\n\n```bash\npython main.py\n```"
  },
  {
    "path": "examples/agent/voice_agent/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nA ReAct agent example that demonstrates audio output capability.\nNote: When audio output is enabled, tool calling functionality may be disabled.\n\"\"\"\nimport asyncio\nimport os\nfrom agentscope.agent import ReActAgent, UserAgent\nfrom agentscope.formatter import OpenAIChatFormatter\n\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.model import OpenAIChatModel\n\n\nasync def main() -> None:\n    \"\"\"The main entry point for the ReAct audio agent example.\"\"\"\n\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"You are a helpful assistant\",\n        model=OpenAIChatModel(\n            model_name=\"qwen3-omni-flash\",\n            client_kwargs={\n                \"base_url\": \"https://dashscope.aliyuncs.com/\"\n                \"compatible-mode/v1\",\n            },\n            api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n            stream=True,\n            # More options can be found in the DashScope API docs:\n            # https://help.aliyun.com/zh/model-studio/qwen-omni\n            generate_kwargs={\n                \"modalities\": [\"text\", \"audio\"],\n                \"audio\": {\"voice\": \"Cherry\", \"format\": \"wav\"},\n            },\n        ),\n        formatter=OpenAIChatFormatter(),\n        memory=InMemoryMemory(),\n    )\n\n    user = UserAgent(\"Bob\")\n\n    msg = None\n    while True:\n        msg = await user(msg)\n        if msg.get_text_content() == \"exit\":\n            break\n        msg = await agent(msg)\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/deployment/README.md",
    "content": "This dir contains examples of deploying multi-agent systems with different strategies using AgentScope.\n\n| Example | Multi-agent System | Frontend Displaying |\n| --- | --- | --- |\n| [planning_agent](./planning_agent) | A planning agent that can create worker agents to handle sub-tasks. | Only exposing the main planner agent |\n"
  },
  {
    "path": "examples/deployment/planning_agent/README.md",
    "content": "# High-code Deployment of a Routing Agent\n\nThis example demonstrates how to deploy a multi-agent system using AgentScope. The system is composed of a main\nrouting agent equipped with a tool function named `create_worker` to dispatch tasks to specialized worker agents.\n\nSpecifically, the routing agent is deployed as a chat endpoint in server hold by the `Quart` library.\nOnce receiving an input request, we\n- set up a routing agent\n- load the session state if any\n- invoke the routing agent to handle the request, and return the streaming response\n- save the session state\n\n\n# Example Structure\n\n```\nplanning_agent/\n    ├── main.py              # Entry point to start the Quart server with routing agent\n    ├── tool.py              # Tool function to create worker agents\n    └── test_post.py         # Preset test script to send requests to the server\n```\n\n\n## Note\n\n1. The printing messages from sub-agent/worker agents is converted to the streaming response of the tool\nfunction `create_worker`, meaning the sub-agent won't be exposing to the user directly.\n\n2. The sub-agent in `tool.py` is equipped with the following tools. For GitHub and AMap tools, they will be activated only\nif the corresponding environment variables are set.\nYou can customize the toolset by modifying the `tool.py` file.\n\n| Tool                  | Description                                         | Required Environment Variable |\n|-----------------------|-----------------------------------------------------|-------------------------------|\n| write/view text files | Read and write text files                           | -                             |\n| Playwright MCP server | Automate browser actions using Microsoft Playwright | -                             |\n| GitHub MCP server     | Access GitHub repositories and data                 | GITHUB_TOKEN                  |\n| AMap MCP server       | Access AMap services for location-based tasks       | GAODE_API_KEY                 |\n\n\n3. Optionally, you can also expose the sub-agent's response to the user by modifying the `tool.py` file.\n\n## Quick Start\n\nInstall the latest agentscope and Quart packages:\n\n```bash\npip install agentscope quart\n```\n\nEnsure you have set `DASHSCOPE_API_KEY` in your environment for DashScope LLM API, or change the used model in\nboth `main.py` and `tool.py` (Remember to change the formatter correspondingly).\n\nSet the environment variables for GitHub and AMap tools if needed.\n\nRun the Quart server:\n\n```bash\npython main.py\n```\n\nIn another terminal, run the test script to send a request to the server:\n\n```bash\npython test_post.py\n```\n"
  },
  {
    "path": "examples/deployment/planning_agent/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The server that holds agent service.\"\"\"\nimport json\nimport os\nfrom typing import AsyncGenerator\n\nfrom quart import Quart, Response, request\n\nfrom tool import create_worker\n\nfrom agentscope.pipeline import stream_printing_messages\nfrom agentscope.session import JSONSession\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import Toolkit\n\napp = Quart(__name__)\n\n\nasync def handle_input(\n    msg: Msg,\n    user_id: str,\n    session_id: str,\n) -> AsyncGenerator[str, None]:\n    \"\"\"Handle the input message and yield response chunks.\n\n    Args:\n        msg (`Msg`):\n            The input message from the user.\n        user_id (`str`):\n            The user ID.\n        session_id (`str`):\n            The session ID.\n\n    Yields:\n        `str`:\n            A response message in dict format by `Msg().to_dict()`.\n    \"\"\"\n    toolkit = Toolkit()\n    toolkit.register_tool_function(\n        create_worker,\n    )\n\n    # Init JSONSession to save and load the state\n    session = JSONSession(save_dir=\"./sessions\")\n\n    agent = ReActAgent(\n        name=\"Friday\",\n        # pylint: disable=line-too-long\n        sys_prompt=\"\"\"You are Friday, a multifunctional agent that can help people solving different complex tasks. You act like a meta planner to solve complicated tasks by decomposing the task and building/orchestrating different worker agents to finish the sub-tasks.\n\n## Core Mission\nYour primary purpose is to break down complicated tasks into manageable subtasks (a plan), create worker agents to finish the subtask, and coordinate their execution to achieve the user's goal efficiently.\n\n### Important Constraints\n1. DO NOT TRY TO SOLVE THE SUBTASKS DIRECTLY yourself.\n2. Always follow the plan sequence.\n3. DO NOT finish the plan until all subtasks are finished.\n\"\"\",  # noqa: E501\n        model=DashScopeChatModel(\n            model_name=\"qwen3-max\",\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n    )\n\n    # Load the session state if exists\n    await session.load_session_state(\n        session_id=f\"{user_id}-{session_id}\",\n        agent=agent,\n    )\n\n    async for msg, _ in stream_printing_messages(\n        agents=[agent],\n        coroutine_task=agent(msg),\n    ):\n        # Transform the message into a dict string and yield it\n        data = json.dumps(msg.to_dict(), ensure_ascii=False)\n        yield f\"data: {data}\\n\\n\"\n\n    # Save the session state\n    await session.save_session_state(\n        session_id=f\"{user_id}-{session_id}\",\n        agent=agent,\n    )\n\n\n@app.route(\"/chat_endpoint\", methods=[\"POST\"])\nasync def chat_endpoint() -> Response:\n    \"\"\"A simple chat endpoint that streams responses.\"\"\"\n    # Parse the user_id, session_id and user message from the request body\n    data = await request.get_json()\n\n    user_id = data.get(\"user_id\")\n    session_id = data.get(\"session_id\")\n\n    # We use textual input here, you can extend it to support other types\n    user_input = data.get(\"user_input\")\n\n    # If the user_id or session_id is missing, return 400\n    if not user_id or not session_id:\n        return Response(\n            f\"user_id and session_id are required, got user_id: {user_id}, \"\n            f\"session_id: {session_id}\",\n            status=400,\n        )\n\n    return Response(\n        handle_input(\n            Msg(\n                \"user\",\n                user_input,\n                \"user\",\n            ),\n            user_id,\n            session_id,\n        ),\n        mimetype=\"text/event-stream\",\n    )\n\n\nif __name__ == \"__main__\":\n    app.run(\n        port=5000,\n        debug=True,\n    )\n"
  },
  {
    "path": "examples/deployment/planning_agent/test_post.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Send the post request to get the response from the agent\"\"\"\n\nimport requests\n\n\ndef send_post(user_query: str) -> None:\n    \"\"\"Send the post request to the agent endpoint and print the response.\"\"\"\n    res = requests.post(\n        url=\"http://127.0.0.1:5000/chat_endpoint\",\n        json={\n            \"user_id\": \"test_user\",\n            \"session_id\": \"test_session\",\n            \"user_input\": user_query,\n        },\n        stream=True,\n    )\n\n    res.raise_for_status()\n\n    for chunk in res.iter_content(chunk_size=None):\n        if chunk:\n            print(repr(chunk.decode(\"utf-8\")))\n\n\nprint(\"The first request response:\")\n# We first tell who we are in the first request\nsend_post(\"Hi, Alice!\")\n\nprint(\"\\n\\nThe second request response:\")\n# Test if the session is loaded correctly\nsend_post(\"Do you know my name?\")\n\nprint(\"\\n\\nThe third request response:\")\nsend_post(\"Help me to write a hello world in Python\")\n"
  },
  {
    "path": "examples/deployment/planning_agent/tool.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The tool functions used in the planner example.\"\"\"\nimport asyncio\nimport json\nimport os\nfrom collections import OrderedDict\nfrom typing import AsyncGenerator\n\nfrom pydantic import BaseModel, Field\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.mcp import HttpStatelessClient, StdIOStatefulClient\nfrom agentscope.message import Msg, TextBlock\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.pipeline import stream_printing_messages\nfrom agentscope.tool import (\n    ToolResponse,\n    Toolkit,\n    write_text_file,\n    insert_text_file,\n    view_text_file,\n)\n\n\nclass ResultModel(BaseModel):\n    \"\"\"\n    The result model used for the sub worker to summarize the task result.\n    \"\"\"\n\n    success: bool = Field(\n        description=\"Whether the task was successful or not.\",\n    )\n    message: str = Field(\n        description=(\n            \"The specific task result, should include necessary details, \"\n            \"e.g. the file path if any file is generated, the deviation, \"\n            \"and the error message if any.\"\n        ),\n    )\n\n\ndef _convert_to_text_block(msgs: list[Msg]) -> list[TextBlock]:\n    # Collect all the content blocks\n    blocks: list = []\n    # Convert tool_use block into text block for streaming tool response\n    for _ in msgs:\n        for block in _.get_content_blocks():\n            if block[\"type\"] == \"text\":\n                blocks.append(block)\n\n            elif block[\"type\"] == \"tool_use\":\n                blocks.append(\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Calling tool {block['name']} ...\",\n                    ),\n                )\n\n    return blocks\n\n\nasync def create_worker(\n    task_description: str,\n) -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"Create a sub-worker to finish the given task.\n\n    Args:\n        task_description (`str`):\n            The description of the task to be done by the sub-worker, should\n            contain all the necessary information.\n\n    Returns:\n        `AsyncGenerator[ToolResponse, None]`:\n            An async generator yielding ToolResponse objects.\n    \"\"\"\n    toolkit = Toolkit()\n\n    # Gaode MCP client\n    if os.getenv(\"GAODE_API_KEY\"):\n        toolkit.create_tool_group(\n            group_name=\"amap_tools\",\n            description=\"Map-related tools, including geocoding, routing, and \"\n            \"place search.\",\n        )\n        client = HttpStatelessClient(\n            name=\"amap_mcp\",\n            transport=\"streamable_http\",\n            url=f\"https://mcp.amap.com/mcp?key={os.environ['GAODE_API_KEY']}\",\n        )\n        await toolkit.register_mcp_client(client, group_name=\"amap_tools\")\n    else:\n        print(\n            \"Warning: GAODE_API_KEY not set in environment, skipping Gaode \"\n            \"MCP client registration.\",\n        )\n\n    # Browser MCP client\n    toolkit.create_tool_group(\n        group_name=\"browser_tools\",\n        description=\"Web browsing related tools.\",\n    )\n    browser_client = StdIOStatefulClient(\n        name=\"playwright-mcp\",\n        command=\"npx\",\n        args=[\"@playwright/mcp@latest\"],\n    )\n    await browser_client.connect()\n    await toolkit.register_mcp_client(\n        browser_client,\n        group_name=\"browser_tools\",\n    )\n\n    # GitHub MCP client\n    if os.getenv(\"GITHUB_TOKEN\"):\n        toolkit.create_tool_group(\n            group_name=\"github_tools\",\n            description=\"GitHub related tools, including repository \"\n            \"search and code file retrieval.\",\n        )\n        github_client = HttpStatelessClient(\n            name=\"github\",\n            transport=\"streamable_http\",\n            url=\"https://api.githubcopilot.com/mcp/\",\n            headers={\"Authorization\": f\"Bearer {os.getenv('GITHUB_TOKEN')}\"},\n        )\n        await toolkit.register_mcp_client(\n            github_client,\n            group_name=\"github_tools\",\n        )\n\n    else:\n        print(\n            \"Warning: GITHUB_TOKEN not set in environment, skipping GitHub \"\n            \"MCP client registration.\",\n        )\n\n    # Basic read/write tools\n    toolkit.register_tool_function(write_text_file)\n    toolkit.register_tool_function(insert_text_file)\n    toolkit.register_tool_function(view_text_file)\n\n    # Create a new sub-agent to finish the given task\n    sub_agent = ReActAgent(\n        name=\"Worker\",\n        sys_prompt=f\"\"\"You're an agent named Worker.\n\n## Your Target\nYour target is to finish the given task with your tools.\n\n## IMPORTANT\nYou MUST use the `{ReActAgent.finish_function_name}` to generate the final answer after finishing the task.\n\"\"\",  # noqa: E501  # pylint: disable=C0301\n        model=DashScopeChatModel(\n            model_name=\"qwen3-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        ),\n        enable_meta_tool=True,\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n        max_iters=20,\n    )\n\n    # disable the console output of the sub-agent\n    sub_agent.set_console_output_enabled(False)\n\n    # Collect the execution process content\n    msgs = OrderedDict()\n\n    # Wrap the sub-agent in a coroutine task to obtain the final\n    # structured output\n    result = []\n\n    async def call_sub_agent() -> None:\n        msg_res = await sub_agent(\n            Msg(\n                \"user\",\n                content=task_description,\n                role=\"user\",\n            ),\n            structured_model=ResultModel,\n        )\n        result.append(msg_res)\n\n    # Use stream_printing_message to get the streaming response as the\n    # sub-agent works\n    async for msg, _ in stream_printing_messages(\n        agents=[sub_agent],\n        coroutine_task=call_sub_agent(),\n    ):\n        msgs[msg.id] = msg\n\n        # Collect all the content blocks\n        yield ToolResponse(\n            content=_convert_to_text_block(\n                list(msgs.values()),\n            ),\n            stream=True,\n            is_last=False,\n        )\n\n        # Expose the interruption signal to the caller\n        if msg.metadata and msg.metadata.get(\"_is_interrupted\", False):\n            raise asyncio.CancelledError()\n\n    # Obtain the last message from the coroutine task\n    if result:\n        yield ToolResponse(\n            content=[\n                *_convert_to_text_block(\n                    list(msgs.values()),\n                ),\n                TextBlock(\n                    type=\"text\",\n                    text=json.dumps(\n                        result[0].metadata,\n                        indent=2,\n                        ensure_ascii=False,\n                    ),\n                ),\n            ],\n            stream=True,\n            is_last=True,\n        )\n\n    await browser_client.close()\n"
  },
  {
    "path": "examples/evaluation/ace_bench/README.md",
    "content": "# ACEBench Example\n\nThis is an example of agent-oriented evaluation in AgentScope.\n\nWe take [ACEBench](https://github.com/ACEBench/ACEBench) as an example benchmark, and run\na ReAct agent with [Ray](https://github.com/ray-project/ray)-based evaluator, which supports\n**distributed** and **parallel** evaluation.\n\nTo run the example, you need to install AgentScope first, and then run the evaluation with the following command:\n\n```bash\npython main.py --data_dir {data_dir} --result_dir {result_dir}\n```\n\n## Further Reading\n\n- [ACEBench](https://github.com/ACEBench/ACEBench)\n- [Ray](https://github.com/ray-project/ray)\n"
  },
  {
    "path": "examples/evaluation/ace_bench/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Example of running ACEBench evaluation with AgentScope.\"\"\"\nimport os\nimport asyncio\nfrom argparse import ArgumentParser\nfrom typing import Callable\n\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.agent import ReActAgent\nfrom agentscope.evaluate import (\n    ACEBenchmark,\n    Task,\n    SolutionOutput,\n    RayEvaluator,\n    FileEvaluatorStorage,\n    ACEPhone,\n)\nfrom agentscope.tool import Toolkit\n\n\nasync def react_agent_solution(\n    ace_task: Task,\n    pre_hook: Callable,\n) -> SolutionOutput:\n    \"\"\"Run ReAct agent with the given task in ACEBench.\n\n    Args:\n        ace_task (`Task`):\n            Task to run in ACEBench.\n        pre_hook (Callable):\n            The pre-hook function to save the agent's pre-print messages.\n    \"\"\"\n    # Equip tool functions\n    toolkit = Toolkit()\n    for tool, json_schema in ace_task.metadata[\"tools\"]:\n        # register the tool function with the given json schema\n        toolkit.register_tool_function(tool, json_schema=json_schema)\n\n    # Create a ReAct agent\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"You are a helpful assistant named Friday. \"\n        \"Your target is to solve the given task with your tools.\"\n        \"Try to solve the task as best as you can.\",\n        model=DashScopeChatModel(\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            model_name=\"qwen3-max\",\n            stream=False,\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n    )\n\n    agent.register_instance_hook(\n        \"pre_print\",\n        \"save_logging\",\n        pre_hook,\n    )\n\n    # Execute the agent to solve the task\n    msg_input = Msg(\"user\", ace_task.input, role=\"user\")\n    # Print the input by the running agent to call the pre-print hook\n    await agent.print(msg_input)\n    await agent(msg_input)\n\n    # Obtain the trajectory of the agent's memory\n    traj = []\n    for msg in await agent.memory.get_memory():\n        traj.extend(msg.get_content_blocks([\"tool_use\", \"tool_result\"]))\n\n    # Obtain the final state of the phone and travel system\n    phone: ACEPhone = ace_task.metadata[\"phone\"]\n    final_state = phone.get_current_state()\n\n    # Wrap into a SolutionOutput\n    solution = SolutionOutput(\n        success=True,\n        output=final_state,\n        trajectory=traj,\n    )\n    return solution\n\n\nasync def main() -> None:\n    \"\"\"Main function for running ACEBench.\"\"\"\n    # Prepare data and results directories\n    parser = ArgumentParser()\n    parser.add_argument(\n        \"--data_dir\",\n        type=str,\n        required=True,\n        help=\"Where to save the dataset.\",\n    )\n    parser.add_argument(\n        \"--result_dir\",\n        type=str,\n        required=True,\n        help=\"Where to save the evaluation results.\",\n    )\n    parser.add_argument(\n        \"--n_workers\",\n        type=int,\n        default=1,\n        help=\"The number of ray workers to use for evaluation.\",\n    )\n    args = parser.parse_args()\n\n    # Create the evaluator\n    #  or GeneralEvaluator, which more suitable for local debug\n    evaluator = RayEvaluator(\n        name=\"ACEbench evaluation\",\n        benchmark=ACEBenchmark(\n            data_dir=args.data_dir,\n        ),\n        # Repeat how many times\n        n_repeat=1,\n        storage=FileEvaluatorStorage(\n            save_dir=args.result_dir,\n        ),\n        # How many workers to use\n        n_workers=args.n_workers,\n    )\n\n    # Run the evaluation\n    await evaluator.run(react_agent_solution)\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/agent_skill/README.md",
    "content": "# Agent Skills in AgentScope\n\n[Agent Skill](https://claude.com/blog/skills) is an approach proposed by\nAnthropic to improve agent capabilities on specific tasks.\n\nIn this example, we demonstrate how to integrate Agent Skills into an\nReAct agent in AgentScope via the `toolkit.register_agent_skill` API.\n\nSpecifically, we prepare a demonstration skill that helps the agent to\nlearn about the AgentScope framework itself in the `skill` directory.\nIn `main.py`, we register this skill to the agent's toolkit, and ask it\nto answer questions about AgentScope.\n\n## Quick Start\n\nInstall the latest version of AgentScope to run this example:\n\n```bash\npip install agentscope --upgrade\n```\n\nThen, run the example with:\n\n```bash\npython main.py\n```\n\n> Note:\n> - The example is built with DashScope chat model. If you want to change the model used in this example, don't\n> forget to change the formatter at the same time! The corresponding relationship between built-in models and\n> formatters are list in [our tutorial](https://doc.agentscope.io/tutorial/task_prompt.html#id1)\n> - For local models, ensure the model service (like Ollama) is running before starting the agent.\n\n"
  },
  {
    "path": "examples/functionality/agent_skill/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The main entry point of the agent skill example.\"\"\"\nimport asyncio\nimport os\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import (\n    Toolkit,\n    execute_shell_command,\n    execute_python_code,\n    view_text_file,\n)\n\n\nasync def main() -> None:\n    \"\"\"The main entry point for the ReAct agent example.\"\"\"\n    toolkit = Toolkit()\n\n    # To use agent skills, your agent must be equipped with text file viewing\n    # tools.\n    toolkit.register_tool_function(execute_shell_command)\n    toolkit.register_tool_function(execute_python_code)\n    toolkit.register_tool_function(view_text_file)\n\n    # Register the agent skill\n    toolkit.register_agent_skill(\n        \"./skill/analyzing-agentscope-library\",\n    )\n\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"\"\"You are a helpful assistant named Friday.\n\n# IMPORTANT\n- Don't make any assumptions. All your knowledge about AgentScope library must come from your equipped skills.\n\"\"\",  # noqa: E501\n        model=DashScopeChatModel(\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            model_name=\"qwen3-max\",\n            enable_thinking=False,\n            stream=True,\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n        memory=InMemoryMemory(),\n    )\n\n    # First, let's take a look at the agent's system prompt\n    print(\"\\033[1;32mAgent System Prompt:\\033[0m\")\n    print(agent.sys_prompt)\n    print(\"\\n\")\n\n    print(\n        \"\\033[1;32mResponse to Question 'What skills do you have?':\\033[0m\",\n    )\n    # We prepare two questions\n    await agent(\n        Msg(\"user\", \"What skills do you have?\", \"user\"),\n    )\n\n    print(\n        \"\\n\\033[1;32mResponse to Question 'How to create my own tool function \"\n        \"for the agent in agentscope?':\\033[0m\",\n    )\n    # The second question\n    await agent(\n        Msg(\n            \"user\",\n            \"How to custom tool function for the agent in agentscope?\",\n            \"user\",\n        ),\n    )\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/agent_skill/skill/analyzing-agentscope-library/SKILL.md",
    "content": "---\nname: Analyzing AgentScope Library\ndescription: This skill provides a way to retrieve information from the AgentScope library for analysis and decision-making.\n---\n\n# Analyzing AgentScope Library\n\n## Overview\n\nThis guide covers the essential operations for retrieving and answering questions about the AgentScope library.\nIf you need to answer questions regarding the AgentScope library, or look up specific information, functions/classes,\nexamples or guidance, this skill will help you achieve that.\n\n## Quick Start\n\nThe skill provides the following key scripts:\n\n- Search for guidance in the AgentScope tutorial.\n- Search for official examples and recommended implementations provided by AgentScope.\n- A quick interface to view AgentScope's Python library by given a module name (e.g. agentscope), and return the module's submodules, classes, and functions.\n\nWhen being asked an AgentScope-related question, you can follow the steps below to find the relevant information:\n\nFirst decide which of the three scripts to use based on the user's question.\n- If user asks for \"how to use\" types of questions, use the \"Search for Guidance\" script to find the relevant tutorial\n- If user asks for \"how to implement/build\" types of questions, first search for relevant examples. If not found, then\n  consider what functions are needed and search in the guide/tutorial\n- If user asks for \"how to initialize\" types of questions, first search for relevant tutorials. If not found, then\n  consider to search for the corresponding modules, classes, or functions in the library.\n\n\n### Search for Examples\n\nFirst ask for the user's permission to clone the agentscope GitHub repository if you haven't done so:\n\n```bash\ngit clone -b main https://github.com/agentscope-ai/agentscope\n```\n\nIn this repo, the `examples` folder contains various examples demonstrating how to use different features of the\nAgentScope library.\nThey are organized in a tree structure by different functionalities. You should use shell command like `ls` or `cat` to\nnavigate and view the examples. Avoid using `find` command to search for examples, as the name of the example\nfiles may not directly relate to the functionality being searched for.\n\n### Search for Guidance\n\nSimilarly, first ensure you have cloned the agentscope GitHub repository.\n\nThe source agentscope tutorial is located in the `docs/tutorials` folder of the agentscope GitHub repository. It's\norganized by the different sections. To search for guidance, go to the `docs/tutorials` folder and view the tutorial\nfiles by shell command like `ls` or `cat`.\n\n\n### Search for Targeted Modules\n\nFirst, ensure you have installed the agentscope library in your environment:\n\n```bash\npip list | grep agentscope\n```\n\nIf not installed, ask the user for permission to install it by command:\n\n```bash\npip install agentscope\n```\n\nThen, run the following script to search for specific modules, classes, or functions. It's suggested to start with\n`agentscope` as the root module name, and then specify the submodule name you want to search for.\n\n```bash\npython view_agentscope_module.py --module agentscope\n```\n\nAbout detailed usage, please refer to the `./view_agentscope_module.py` script (located in the same folder as this\nSKILL.md file).\n\n"
  },
  {
    "path": "examples/functionality/agent_skill/skill/analyzing-agentscope-library/view_agentscope_module.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: skip-file\n\"\"\"Get the signatures of functions and classes in the agentscope library.\"\"\"\nfrom typing import Literal, Callable\n\nimport agentscope\nimport inspect\nfrom pydantic import BaseModel\n\n\ndef get_class_signature(cls: type) -> str:\n    \"\"\"Get the signature of a class.\n\n    Args:\n        cls (`type`):\n            A class object.\n\n    Returns:\n        str: The signature of the class.\n    \"\"\"\n    # Obtain class name and docstring\n    class_name = cls.__name__\n    class_docstring = cls.__doc__ or \"\"\n\n    # Construct the class string\n    class_str = f\"class {class_name}:\\n\"\n    if class_docstring:\n        class_str += f'    \"\"\"{class_docstring}\"\"\"\\n'\n\n    # Obtain the module of the class\n    methods = []\n    for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):\n        # Skip methods that are not part of the class\n        if method.__qualname__.split(\".\")[0] != class_name:\n            continue\n\n        if name.startswith(\"_\") and name not in [\"__init__\", \"__call__\"]:\n            continue\n\n        # Obtain the method's signature\n        sig = inspect.signature(method)\n\n        # Construct the method string\n        method_str = f\"    def {name}{sig}:\\n\"\n\n        # Add the method's docstring if it exists\n        method_docstring = method.__doc__ or \"\"\n        if method_docstring:\n            method_str += f'        \"\"\"{method_docstring}\"\"\"\\n'\n\n        methods.append(method_str)\n\n    class_str += \"\\n\".join(methods)\n    return class_str\n\n\ndef get_function_signature(func: Callable) -> str:\n    \"\"\"Get the signature of a function.\"\"\"\n    sig = inspect.signature(func)\n    method_str = f\"def {func.__name__}{sig}:\\n\"\n\n    method_docstring = func.__doc__ or \"\"\n    if method_docstring:\n        method_str += f'   \"\"\"{method_docstring}\"\"\"\\n'\n\n    return method_str\n\n\nclass FuncOrCls(BaseModel):\n    \"\"\"The class records the module, signature, docstring, reference, and\n    type\"\"\"\n\n    module: str\n    \"\"\"The module of the function or class.\"\"\"\n    signature: str\n    \"\"\"The signature of the function or class.\"\"\"\n    docstring: str\n    \"\"\"The docstring of the function or class.\"\"\"\n    reference: str\n    \"\"\"The reference to the source code of the function or class\"\"\"\n    type: Literal[\"function\", \"class\"]\n    \"\"\"The type of the function or class, either 'function' or 'class'.\"\"\"\n\n    def __init__(\n        self,\n        module: str,\n        signature: str,\n        docstring: str,\n        reference: str,\n        # pylint: disable=redefined-builtin\n        type: Literal[\"function\", \"class\"],\n    ) -> None:\n        \"\"\"Initialize the FuncOrCls instance.\"\"\"\n        super().__init__(\n            module=module,\n            signature=signature.strip(),\n            docstring=docstring.strip(),\n            reference=reference,\n            type=type,\n        )\n\n\ndef _truncate_docstring(docstring: str, max_length: int = 200) -> str:\n    \"\"\"Truncate the docstring to a maximum length.\n\n    Args:\n        docstring (`str`):\n            The docstring to truncate.\n        max_length (`int`, *optional*, defaults to 200):\n            The maximum length of the docstring.\n\n    Returns:\n        `str`:\n            The truncated docstring.\n    \"\"\"\n    if len(docstring) > max_length:\n        return docstring[:max_length] + \"...\"\n    return docstring\n\n\ndef get_agentscope_module_signatures() -> list[FuncOrCls]:\n    \"\"\"Get the signatures of functions and classes in the agentscope library.\n\n    Returns:\n        `list[FuncOrCls]`:\n            A list of FuncOrCls instances representing the functions and\n            classes in the agentscope library.\n    \"\"\"\n    signatures = []\n    for module in agentscope.__all__:\n        as_module = getattr(agentscope, module)\n        path_module = \".\".join([\"agentscope\", module])\n\n        # Functions\n        if inspect.isfunction(as_module):\n            file = inspect.getfile(as_module)\n            source_lines, start_line = inspect.getsourcelines(as_module)\n            signatures.append(\n                FuncOrCls(\n                    module=path_module,\n                    signature=get_function_signature(as_module),\n                    docstring=_truncate_docstring(as_module.__doc__ or \"\"),\n                    reference=f\"{file}: {start_line}-\"\n                    f\"{start_line + len(source_lines)}\",\n                    type=\"function\",\n                ),\n            )\n\n        else:\n            if not hasattr(as_module, \"__all__\"):\n                continue\n\n            # Modules with __all__ attribute\n            for name in as_module.__all__:\n                func_or_cls = getattr(as_module, name)\n                path_func_or_cls = \".\".join([path_module, name])\n\n                if inspect.isclass(func_or_cls):\n                    file = inspect.getfile(func_or_cls)\n                    source_lines, start_line = inspect.getsourcelines(\n                        func_or_cls,\n                    )\n                    signatures.append(\n                        FuncOrCls(\n                            module=path_func_or_cls,\n                            signature=get_class_signature(func_or_cls),\n                            docstring=_truncate_docstring(\n                                func_or_cls.__doc__ or \"\",\n                            ),\n                            reference=(\n                                f\"{file}: {start_line}-\"\n                                f\"{start_line + len(source_lines)}\"\n                            ),\n                            type=\"class\",\n                        ),\n                    )\n\n                elif inspect.isfunction(func_or_cls):\n                    file = inspect.getfile(func_or_cls)\n                    source_lines, start_line = inspect.getsourcelines(\n                        func_or_cls,\n                    )\n                    signatures.append(\n                        FuncOrCls(\n                            module=path_func_or_cls,\n                            signature=get_function_signature(func_or_cls),\n                            docstring=_truncate_docstring(\n                                func_or_cls.__doc__ or \"\",\n                            ),\n                            reference=(\n                                f\"{file}: {start_line}-\"\n                                f\"{start_line + len(source_lines)}\"\n                            ),\n                            type=\"function\",\n                        ),\n                    )\n\n    return signatures\n\n\ndef view_agentscope_library(\n    module: str,\n) -> str:\n    \"\"\"View AgentScope's Python library by given a module name\n    (e.g. agentscope), and return the module's submodules, classes, and\n    functions. Given a class name, return the class's documentation, methods,\n    and their signatures. Given a function name, return the function's\n    documentation and signature. If you don't have any information about\n    AgentScope library, try to use \"agentscope\" to view the available top\n    modules.\n\n    Note this function only provide the module's brief information.\n    For more information, you should view the source code.\n\n    Args:\n        module (`str`):\n            The module name to view, which should be a module path separated\n            by dots (e.g. \"agentscope.models\"). It can refer to a module,\n            a class, or a function.\n    \"\"\"\n    if not module.startswith(\"agentscope\"):\n        return (\n            f\"Module '{module}' is invalid. The input module should be \"\n            f\"'agentscope' or submodule of 'agentscope.xxx.xxx' \"\n            f\"(separated by dots).\"\n        )\n\n    agentscope_top_modules = {}\n    for as_module in agentscope.__all__:\n        if as_module in [\"__version__\", \"logger\"]:\n            continue\n        agentscope_top_modules[as_module] = getattr(\n            agentscope,\n            as_module,\n        ).__doc__\n\n    # top modules\n    if module == \"agentscope\":\n        top_modules_description = (\n            [\n                \"The top-level modules in AgentScope library:\",\n            ]\n            + [\n                f\"- agentscope.{k}: {v}\"\n                for k, v in agentscope_top_modules.items()\n            ]\n            + [\n                \"You can further view the classes/function within above \"\n                \"modules by calling this function with the above module name.\",\n            ]\n        )\n        return \"\\n\".join(top_modules_description)\n\n    # class, functions\n    modules = get_agentscope_module_signatures()\n    for as_module in modules:\n        if as_module.module == module:\n            return f\"\"\"- The signature of '{module}':\n```python\n{as_module.signature}\n```\n\n- Source code reference: {as_module.reference}\"\"\"\n\n    # two-level modules\n    collected_modules = []\n    for as_module in modules:\n        if as_module.module.startswith(module):\n            collected_modules.append(as_module)\n\n    if len(collected_modules) > 0:\n        collected_modules_content = (\n            [\n                f\"The classes/functions and their truncated docstring in \"\n                f\"'{module}' module:\",\n            ]\n            + [f\"- {_.module}: {repr(_.docstring)}\" for _ in collected_modules]\n            + [\n                \"The docstring is truncated for limited context. For detailed \"\n                \"signature and methods, call this function with the above \"\n                \"module name\",\n            ]\n        )\n\n        return \"\\n\".join(collected_modules_content)\n\n    return (\n        f\"Module '{module}' not found. Use 'agentscope' to view the \"\n        f\"top-level modules to ensure the given module is valid.\"\n    )\n\n\nif __name__ == \"__main__\":\n    import argparse\n\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\n        \"--module\",\n        type=str,\n        default=\"agentscope\",\n        help=\"The module name to view, e.g. 'agentscope'\",\n    )\n    args = parser.parse_args()\n\n    res = view_agentscope_library(module=args.module)\n    print(res)\n"
  },
  {
    "path": "examples/functionality/long_term_memory/mem0/README.md",
    "content": " # Mem0 Long-Term Memory in AgentScope\n\nThis example demonstrates how to\n\n- use Mem0LongTermMemory to provide persistent semantic memory storage for AgentScope agents,\n- record and retrieve conversation history and user preferences across sessions,\n- integrate long-term memory with ReAct agents for context-aware conversations, and\n- configure DashScope embedding models and Qdrant vector store for memory management.\n\n## Prerequisites\n\n- Python 3.10 or higher\n- DashScope API key from Alibaba Cloud\n\n\n## QuickStart\n\nInstall agentscope and ensure you have a valid DashScope API key in your environment variables.\n\n> Note: The example is built with DashScope chat model and embedding model. If you want to use OpenAI models instead,\n> modify the model initialization in the example code accordingly.\n\n```bash\n# Install agentscope from source\ncd {PATH_TO_AGENTSCOPE}\npip install -e .\n# Install dependencies\npip install mem0ai\n```\n\nSet up your API key:\n\n```bash\nexport DASHSCOPE_API_KEY='YOUR_API_KEY'\n```\n\nRun the example:\n\n```bash\npython memory_example.py\n```\n\nThe example will:\n1. Initialize a Mem0LongTermMemory instance with DashScope models and Qdrant vector store\n2. Record a basic conversation to long-term memory\n3. Retrieve memories using semantic search\n4. Demonstrate ReAct agent integration with long-term memory for storing and retrieving user preferences\n\n## Key Features\n\n- **Vector-based Storage**: Uses Qdrant vector database for efficient semantic search and retrieval\n- **Flexible Configuration**: Support for multiple embedding models (OpenAI, DashScope) and vector stores\n- **Async Operations**: Full async support for non-blocking memory operations\n- **ReAct Agent Integration**: Seamless integration with AgentScope's ReActAgent and tool system\n\n## Basic Usage\n\n### Initialize Memory\n\n```python\nimport os\nfrom agentscope.memory import Mem0LongTermMemory\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.embedding import DashScopeTextEmbedding\nfrom mem0.vector_stores.configs import VectorStoreConfig\n\n# Initialize with DashScope models and Qdrant vector store\nlong_term_memory = Mem0LongTermMemory(\n    agent_name=\"Friday\",\n    user_name=\"user_123\",\n    model=DashScopeChatModel(\n        model_name=\"qwen-max-latest\",\n        api_key=os.environ.get(\"DASHSCOPE_API_KEY\")\n    ),\n    embedding_model=DashScopeTextEmbedding(\n        model_name=\"text-embedding-v3\",\n        api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n        dimensions=1024\n    ),\n    vector_store_config=VectorStoreConfig(\n        provider=\"qdrant\",\n        config={\n            \"on_disk\": True,\n            \"path\": \"./qdrant_data\",  # Your customized storage path\n            \"embedding_model_dims\": 1024\n        }\n    )\n)\n```\n\n> **Important**: If you change to a different embedding model or modify `embedding_model_dims`, you must either set a new storage path or delete the existing database files. Otherwise, a dimension mismatch error will occur.\n\n### Integrate with ReAct Agent\n\n```python\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.tool import Toolkit\n\n# Create a ReAct agent with long-term memory\ntoolkit = Toolkit()\nagent = ReActAgent(\n    name=\"Friday\",\n    sys_prompt=(\n        \"You are a helpful assistant named Friday. \"\n        \"If you think there is relevant information about \"\n        \"the user's preferences, you can record it to long-term \"\n        \"memory using the tool `record_to_memory`. \"\n        \"If you need to retrieve information from long-term \"\n        \"memory, use the tool `retrieve_from_memory`.\"\n    ),\n    model=DashScopeChatModel(\n        model_name=\"qwen-max-latest\",\n        api_key=os.environ.get(\"DASHSCOPE_API_KEY\")\n    ),\n    formatter=DashScopeChatFormatter(),\n    toolkit=toolkit,\n    memory=InMemoryMemory(),\n    long_term_memory=long_term_memory,\n    long_term_memory_mode=\"both\"\n)\n\n# Use the agent\nmsg = Msg(\n    role=\"user\",\n    content=\"When I travel to Hangzhou, I prefer to stay in a homestay\",\n    name=\"user\"\n)\nresponse = await agent(msg)\n```\n\n## Advanced Configuration\n\nYou can customize the mem0 config by directly set :\n\n```python\nlong_term_memory = Mem0LongTermMemory(\n    agent_name=\"Friday\",\n    user_name=\"user_123\",\n    mem0_config=your_mem0_config  # Pass your custom mem0 configuration\n)\n```\n\nFor more configuration options, refer to the [mem0 documentation](https://github.com/mem0ai/mem0).\n\n## What's in the Example\n\nThe `memory_example.py` file demonstrates:\n\n1. **Basic Memory Recording**: Recording user conversations to long-term memory\n2. **Memory Retrieval**: Searching for stored memories using semantic similarity\n3. **ReAct Agent Integration**: Using long-term memory with ReAct agents to store and retrieve user preferences automatically\n\n## Reference\n\n- [mem0 Documentation](https://github.com/mem0ai/mem0)\n- [Qdrant Vector Database](https://qdrant.tech/)"
  },
  {
    "path": "examples/functionality/long_term_memory/mem0/memory_example.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Memory example demonstrating long-term memory functionality with mem0.\n\nThis module provides examples of how to use the Mem0LongTermMemory class\nfor recording and retrieving persistent memories.\n\"\"\"\n\nimport asyncio\nimport os\n\nfrom dotenv import load_dotenv\nfrom mem0.vector_stores.configs import VectorStoreConfig\nfrom agentscope.memory import Mem0LongTermMemory\nfrom agentscope.agent import ReActAgent\nfrom agentscope.embedding import DashScopeTextEmbedding\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import Toolkit\n\n\nload_dotenv()\n\n\nasync def main() -> None:\n    \"\"\"Run the memory examples.\"\"\"\n    # Initialize long term memory\n    long_term_memory = Mem0LongTermMemory(\n        agent_name=\"Friday\",\n        user_name=\"user_123\",\n        model=DashScopeChatModel(\n            model_name=\"qwen-max-latest\",\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            stream=False,\n        ),\n        embedding_model=DashScopeTextEmbedding(\n            model_name=\"text-embedding-v3\",\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            dimensions=1024,\n        ),\n        vector_store_config=VectorStoreConfig(\n            provider=\"qdrant\",\n            config={\n                \"on_disk\": True,\n                \"path\": \"../memory/qdrant_data\",  # Specify custom path\n                \"embedding_model_dims\": 1024,\n            },\n        ),\n    )\n\n    # If you want to also use graph memory in mem0,\n    # the following is an example of using Neo4j graph store.\n    # from mem0.configs.base import MemoryConfig\n    # from mem0.graphs.configs import GraphStoreConfig\n    # long_term_memory = Mem0LongTermMemory(\n    #     agent_name=\"Friday\",\n    #     user_name=\"user_123\",\n    #     embedding_model=DashScopeTextEmbedding(\n    #         model_name=\"text-embedding-v3\",\n    #         api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n    #         dimensions=1024,\n    #     ),\n    #     model=DashScopeChatModel(\n    #         model_name=\"qwen-max-latest\",\n    #         api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n    #         stream=False,\n    #     ),\n    #     vector_store_config=VectorStoreConfig(\n    #         provider=\"qdrant\",\n    #         config={\n    #             \"on_disk\": True,\n    #             \"path\": \"../memory/qdrant_data\",  # Specify custom path\n    #             \"embedding_model_dims\": 1024,\n    #         }),\n    #     mem0_config=MemoryConfig(\n    #         graph_store=GraphStoreConfig(\n    #             provider=\"neo4j\",\n    #             config={\n    #                 \"url\": os.environ.get(\"NEO4J_URL\",\n    #                 \"neo4j://localhost:7687\"),\n    #                 \"username\": os.environ.get(\"NEO4J_USER\", \"neo4j\"),\n    #                 \"password\": os.environ.get(\"NEO4J_PASSWORD\",\n    #                 \"12345678\"),\n    #                 \"database\": \"neo4j\",\n    #             },\n    #         ),\n    #     ),\n    # )\n\n    print(\"=== Long Term Memory Examples with mem0 ===\\n\")\n\n    # Example 1: Basic conversation recording\n    print(\"1. Basic Conversation Recording\")\n    print(\"-\" * 40)\n    results = await long_term_memory.record(\n        msgs=[\n            Msg(\n                role=\"user\",\n                content=\"Please help me book a hotel, preferably homestay\",\n                name=\"user\",\n            ),\n        ],\n    )\n    print(f\"Recorded conversation: {results}\\n\")\n\n    # Example 2: Retrieving memories\n    print(\"2. Retrieving Memories\")\n    print(\"-\" * 40)\n    print(\"Searching for weather-related memories...\")\n    weather_memories = await long_term_memory.retrieve(\n        msg=[\n            Msg(\n                role=\"user\",\n                content=\"What's the weather like today?\",\n                name=\"user\",\n            ),\n        ],\n    )\n    print(f\"Retrieved weather memories: {weather_memories}\\n\")\n\n    print(\"Searching for user preference memories...\")\n    preference_memories = await long_term_memory.retrieve(\n        msg=[\n            Msg(\n                role=\"user\",\n                content=(\n                    \"I prefer temperatures in Celsius and wind speed in km/h\"\n                ),\n                name=\"user\",\n            ),\n        ],\n    )\n    print(f\"Retrieved preference memories: {preference_memories}\\n\")\n\n    # Example 3: ReActAgent with long term memory\n    print(\"3. ReActAgent with long term memory\")\n    print(\"-\" * 40)\n\n    toolkit = Toolkit()\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=(\n            \"You are a helpful assistant named Friday. \"\n            \"If you think there is relevant information about \"\n            \"user's preference, you can record it to the long term \"\n            \"memory by tool call `record_to_memory`. \"\n            \"If you need to retrieve information from the long term \"\n            \"memory, you can use the tool call `retrieve_from_memory`.\"\n        ),\n        model=DashScopeChatModel(\n            model_name=\"qwen-max-latest\",\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            stream=False,\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n        memory=InMemoryMemory(),\n        long_term_memory=long_term_memory,\n        long_term_memory_mode=\"both\",\n    )\n\n    await agent.memory.clear()\n    msg = Msg(\n        role=\"user\",\n        content=\"When I travel to Hangzhou, I prefer to stay in a homestay\",\n        name=\"user\",\n    )\n    msg = await agent(msg)\n    print(f\"ReActAgent response: {msg.get_text_content()}\\n\")\n\n    msg = Msg(role=\"user\", content=\"what preference do I have?\", name=\"user\")\n    msg = await agent(msg)\n    print(f\"ReActAgent response: {msg.get_text_content()}\\n\")\n    msg = Msg(\n        role=\"user\",\n        content=\"I prefer to visit the West Lake\",\n        name=\"user\",\n    )\n    msg = await agent(msg)\n    print(f\"ReActAgent response: {msg.get_text_content()}\\n\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/long_term_memory/reme/README.md",
    "content": "# ReMe Long-Term Memory in AgentScope\n\nThis example demonstrates how to:\n\n- Use ReMe (Reflection Memory) to provide three specialized types of persistent memory storage for AgentScope agents\n- Record and retrieve personal information, task execution trajectories, and tool usage patterns across sessions\n- Integrate long-term memory with ReActAgent for context-aware conversations and continuous learning\n- Configure DashScope embedding models and vector stores for efficient memory management\n\n## Overview\n\nReMe (Reflection Memory) provides three types of long-term memory for intelligent agents:\n\n1. **Personal Memory** (`ReMePersonalLongTermMemory`) - Records and retrieves persistent personal information, preferences, and facts about users\n2. **Task Memory** (`ReMeTaskLongTermMemory`) - Learns from task execution trajectories and retrieves relevant past experiences for similar tasks\n3. **Tool Memory** (`ReMeToolLongTermMemory`) - Records tool execution results and generates usage guidelines to improve tool calling\n\n## Prerequisites\n\n- Python 3.12 or higher\n- DashScope API key from Alibaba Cloud (for the examples)\n\n## QuickStart\n\n### Installation\n\n```bash\n# Install agentscope from source\ncd {PATH_TO_AGENTSCOPE}\npip install -e .\n\n# Install required dependencies\npip install reme-ai python-dotenv\n```\n\n### Setup\n\nSet up your API key:\n\n```bash\nexport DASHSCOPE_API_KEY='YOUR_API_KEY'\n```\n\nOr create a `.env` file:\n\n```bash\nDASHSCOPE_API_KEY=YOUR_API_KEY\n```\n\n### Run Examples\n\n```bash\n# Personal Memory Example - 5 core interfaces\npython personal_memory_example.py\n\n# Task Memory Example - 5 core interfaces\npython task_memory_example.py\n\n# Tool Memory Example - Complete workflow with ReActAgent\npython tool_memory_example.py\n```\n\n> **Note**: The examples use DashScope models by default. To use OpenAI or other models, modify the model initialization in the example code accordingly.\n\n## Key Features\n\n- **Three Specialized Memory Types**: Personal, Task, and Tool memory for different use cases\n- **Dual Interface Design**: Both tool functions (for agent calling) and direct methods (for programmatic use)\n- **Vector-based Retrieval**: Efficient semantic search using embedding models and vector stores\n- **Async-first Architecture**: Full async/await support for non-blocking operations\n- **ReActAgent Integration**: Seamless integration with AgentScope's ReActAgent and Toolkit\n- **Automatic Context Management**: Uses async context managers for proper resource handling\n\n## Core Concepts\n\n### Memory Types and Their Use Cases\n\n| Memory Type | Purpose | When to Use |\n|------------|---------|-------------|\n| **Personal Memory** | Store user preferences, habits, and personal facts | User profiles, personalized assistants, long-term user context |\n| **Task Memory** | Learn from task execution trajectories | Problem-solving, debugging, repeated workflows, learning from past successes |\n| **Tool Memory** | Record tool usage patterns and generate guidelines | Tool-using agents, improving tool call accuracy, avoiding past errors |\n\n### Interface Design\n\n**Personal Memory** and **Task Memory** provide **5 core interfaces**:\n\n1. **`record_to_memory()`** - Tool function for agents to record memories (returns `ToolResponse`)\n2. **`retrieve_from_memory()`** - Tool function for agents to retrieve memories (returns `ToolResponse`)\n3. **`record()`** - Direct method for programmatic recording (returns `None`)\n4. **`retrieve()`** - Direct method for programmatic retrieval (returns `str`)\n5. **ReActAgent Integration** - Use memory with `long_term_memory` and `long_term_memory_mode` parameters\n\n**Tool Memory** provides **2 core interfaces** (no tool functions):\n\n1. **`record()`** - Direct method for recording tool execution results (returns `None`)\n2. **`retrieve()`** - Direct method for retrieving tool usage guidelines (returns `str`)\n\n## Usage Examples\n\n### 1. Personal Memory\n\n**Use Case**: Record and retrieve user preferences, habits, and personal information.\n\n```python\nimport asyncio\nimport os\nfrom agentscope.memory import ReMePersonalLongTermMemory\nfrom agentscope.embedding import DashScopeTextEmbedding\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\n\n\nasync def main():\n    # Initialize personal memory\n    personal_memory = ReMePersonalLongTermMemory(\n        agent_name=\"Friday\",\n        user_name=\"user_123\",\n        model=DashScopeChatModel(\n            model_name=\"qwen3-max\",\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            stream=False,\n        ),\n        embedding_model=DashScopeTextEmbedding(\n            model_name=\"text-embedding-v4\",\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            dimensions=1024,\n        ),\n    )\n\n    # Use async context manager (required!)\n    async with personal_memory:\n        # Interface 1: record_to_memory (tool function)\n        result = await personal_memory.record_to_memory(\n            thinking=\"User sharing travel preferences\",\n            content=[\n                \"I prefer to stay in homestays when traveling to Hangzhou\",\n                \"I like to visit the West Lake in the morning\",\n                \"I enjoy drinking Longjing tea\",\n            ],\n        )\n\n        # Interface 2: retrieve_from_memory (tool function)\n        result = await personal_memory.retrieve_from_memory(\n            keywords=[\"Hangzhou travel\", \"tea preference\"],\n        )\n\n        # Interface 3: record (direct method)\n        await personal_memory.record(\n            msgs=[\n                Msg(role=\"user\", content=\"I work as a software engineer\", name=\"user\"),\n                Msg(role=\"assistant\", content=\"Got it!\", name=\"assistant\"),\n            ],\n        )\n\n        # Interface 4: retrieve (direct method)\n        memories = await personal_memory.retrieve(\n            msg=Msg(role=\"user\", content=\"What do you know about my work?\", name=\"user\"),\n        )\n        print(memories)\n\n\nasyncio.run(main())\n```\n\n**Integration with ReActAgent** (Interface 5):\n\n```python\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.tool import Toolkit\n\nasync def use_with_agent():\n    personal_memory = ReMePersonalLongTermMemory(...)\n\n    async with personal_memory:\n        agent = ReActAgent(\n            name=\"Friday\",\n            sys_prompt=\"You are Friday with long-term memory. Always record user information and retrieve memories when needed.\",\n            model=DashScopeChatModel(...),\n            formatter=DashScopeChatFormatter(),\n            toolkit=Toolkit(),\n            memory=InMemoryMemory(),\n            long_term_memory=personal_memory,  # Attach personal memory\n            long_term_memory_mode=\"both\",  # Enable both record and retrieve tools\n        )\n\n        # Agent can now use record_to_memory and retrieve_from_memory as tools\n        msg = Msg(role=\"user\", content=\"I prefer staying in homestays\", name=\"user\")\n        response = await agent(msg)\n```\n\n### 2. Task Memory\n\n**Use Case**: Learn from task execution trajectories and retrieve relevant experiences.\n\n```python\nfrom agentscope.memory import ReMeTaskLongTermMemory\n\n\nasync def main():\n    # Initialize task memory\n    task_memory = ReMeTaskLongTermMemory(\n        agent_name=\"TaskAssistant\",\n        user_name=\"task_workspace_123\",  # Acts as workspace_id\n        model=DashScopeChatModel(...),\n        embedding_model=DashScopeTextEmbedding(...),\n    )\n\n    async with task_memory:\n        # Interface 1: record_to_memory with score\n        result = await task_memory.record_to_memory(\n            thinking=\"Recording successful debugging approach\",\n            content=[\n                \"For API 404 errors: Check route definition, verify URL path, ensure correct port\",\n                \"Always use linter to catch typos in route paths\",\n            ],\n            score=0.95,  # High score for successful trajectory\n        )\n\n        # Interface 2: retrieve_from_memory\n        result = await task_memory.retrieve_from_memory(\n            keywords=[\"debugging\", \"API errors\"],\n        )\n\n        # Interface 3: record with score in direct method\n        await task_memory.record(\n            msgs=[\n                Msg(role=\"user\", content=\"I'm getting a 404 error\", name=\"user\"),\n                Msg(role=\"assistant\", content=\"Let's check the route path...\", name=\"assistant\"),\n                Msg(role=\"user\", content=\"Found the typo!\", name=\"user\"),\n            ],\n            score=0.95,  # Optional score for this trajectory\n        )\n\n        # Interface 4: retrieve (direct method)\n        experiences = await task_memory.retrieve(\n            msg=Msg(role=\"user\", content=\"How to debug API errors?\", name=\"user\"),\n        )\n        print(experiences)\n\n\nasyncio.run(main())\n```\n\n**Integration with ReActAgent** (Interface 5):\n\n```python\nasync def use_with_agent():\n    task_memory = ReMeTaskLongTermMemory(...)\n\n    async with task_memory:\n        agent = ReActAgent(\n            name=\"TaskAssistant\",\n            sys_prompt=\"You are a task assistant. Record solutions and retrieve past experiences before solving problems.\",\n            model=DashScopeChatModel(...),\n            formatter=DashScopeChatFormatter(),\n            toolkit=Toolkit(),\n            memory=InMemoryMemory(),\n            long_term_memory=task_memory,\n            long_term_memory_mode=\"both\",\n        )\n\n        # Agent learns from task executions over time\n        msg = Msg(role=\"user\", content=\"How should I optimize database queries?\", name=\"user\")\n        response = await agent(msg)\n```\n\n### 3. Tool Memory\n\n**Use Case**: Record tool execution results and generate usage guidelines for better tool calling.\n\n**Complete Workflow**:\n\n```python\nimport json\nfrom datetime import datetime\nfrom agentscope.memory import ReMeToolLongTermMemory\nfrom agentscope.tool import Toolkit, ToolResponse\nfrom agentscope.message import Msg, TextBlock\n\n\n# Step 1: Define tools\nasync def web_search(query: str, max_results: int = 5) -> ToolResponse:\n    \"\"\"Search the web for information.\"\"\"\n    result = f\"Found {max_results} results for query: '{query}'\"\n    return ToolResponse(content=[TextBlock(type=\"text\", text=result)])\n\n\nasync def main():\n    # Initialize tool memory\n    tool_memory = ReMeToolLongTermMemory(\n        agent_name=\"ToolBot\",\n        user_name=\"tool_workspace_demo\",\n        model=DashScopeChatModel(...),\n        embedding_model=DashScopeTextEmbedding(...),\n    )\n\n    async with tool_memory:\n        # Step 2: Record tool execution history (accepts JSON strings in msgs)\n        tool_result = {\n            \"create_time\": datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\"),\n            \"tool_name\": \"web_search\",\n            \"input\": {\"query\": \"Python asyncio tutorial\", \"max_results\": 10},\n            \"output\": \"Found 10 results for query: 'Python asyncio tutorial'\",\n            \"token_cost\": 150,\n            \"success\": True,\n            \"time_cost\": 2.3\n        }\n\n        # Interface 1: record (accepts JSON strings in message content)\n        await tool_memory.record(\n            msgs=[Msg(role=\"assistant\", content=json.dumps(tool_result), name=\"assistant\")],\n        )\n\n        # Step 3: Retrieve tool guidelines\n        # Interface 2: retrieve returns summarized guidelines\n        guidelines = await tool_memory.retrieve(\n            msg=Msg(role=\"user\", content=\"web_search\", name=\"user\"),\n        )\n\n        # Step 4: Inject guidelines into agent system prompt\n        toolkit = Toolkit()\n        toolkit.register_tool_function(web_search)\n\n        base_prompt = \"You are ToolBot, a helpful AI assistant.\"\n        enhanced_prompt = f\"{base_prompt}\\n\\n# Tool Guidelines:\\n{guidelines}\"\n\n        agent = ReActAgent(\n            name=\"ToolBot\",\n            sys_prompt=enhanced_prompt,  # Guidelines enhance tool usage\n            model=DashScopeChatModel(...),\n            formatter=DashScopeChatFormatter(),\n            toolkit=toolkit,\n            memory=InMemoryMemory(),\n        )\n\n        # Agent now uses tools with learned guidelines\n        msg = Msg(role=\"user\", content=\"Search for Python design patterns\", name=\"user\")\n        response = await agent(msg)\n\n\nasyncio.run(main())\n```\n\n> **Note**: Tool Memory does NOT provide `record_to_memory()` and `retrieve_from_memory()` tool functions. It only provides direct `record()` and `retrieve()` methods. Tool Memory is designed to be used programmatically to enhance agent system prompts, not as agent-callable tools.\n\n## API Reference\n\n### Common Parameters\n\nAll memory types share these initialization parameters:\n\n```python\nReMePersonalLongTermMemory(\n    agent_name: str,           # Name of the agent using this memory\n    user_name: str,            # User identifier (acts as workspace_id in ReMe)\n    model: ModelWrapper,       # LLM for summarization and processing\n    embedding_model: EmbeddingWrapper,  # Embedding model for vector retrieval\n    vector_store_dir: str = \"./memory_vector_store\",  # Storage location\n)\n```\n\n### Interface Specifications\n\n#### Personal Memory\n\n| Interface | Type | Signature | Returns | Description |\n|-----------|------|-----------|---------|-------------|\n| `record_to_memory` | Tool Function | `(thinking: str, content: list[str])` | `ToolResponse` | Record personal information with reasoning |\n| `retrieve_from_memory` | Tool Function | `(keywords: list[str], limit: int = 3)` | `ToolResponse` | Retrieve memories by keywords |\n| `record` | Direct Method | `(msgs: list[Msg])` | `None` | Record message conversations |\n| `retrieve` | Direct Method | `(msg: Msg, top_k: int = 3)` | `str` | Query-based retrieval |\n\n**Parameters**:\n- `thinking`: Reasoning about what to record\n- `content`: List of strings to remember\n- `keywords`: Search keywords\n- `limit`: Results per keyword (tool function, default: 3)\n- `top_k`: Total results to retrieve (direct method, default: 3)\n\n#### Task Memory\n\n| Interface | Type | Signature | Returns | Description |\n|-----------|------|-----------|---------|-------------|\n| `record_to_memory` | Tool Function | `(thinking: str, content: list[str], score: float = 1.0)` | `ToolResponse` | Record task trajectory with score |\n| `retrieve_from_memory` | Tool Function | `(keywords: list[str], top_k: int = 5)` | `ToolResponse` | Retrieve experiences by keywords |\n| `record` | Direct Method | `(msgs: list[Msg], score: float = 1.0)` | `None` | Record message conversations with score |\n| `retrieve` | Direct Method | `(msg: Msg, top_k: int = 5)` | `str` | Query-based experience retrieval |\n\n**Parameters**:\n- `thinking`: Reasoning about the task execution\n- `content`: Task execution information and insights\n- `score`: Success score for the trajectory (0.0-1.0, default: 1.0)\n- `keywords`: Search keywords (e.g., task type, domain)\n- `top_k`: Number of results to retrieve (default: 5)\n\n#### Tool Memory\n\n| Interface | Type | Signature | Returns | Description |\n|-----------|------|-----------|---------|-------------|\n| `record` | Direct Method | `(msgs: list[Msg])` | `None` | Record tool results as messages (JSON format) |\n| `retrieve` | Direct Method | `(msg: Msg)` | `str` | Retrieve guidelines for tools |\n\n**Parameters**:\n- `msgs`: List of messages where `content` contains JSON strings with tool execution metadata:\n  - `create_time`: Timestamp (`\"%Y-%m-%d %H:%M:%S\"`)\n  - `tool_name`: Tool identifier\n  - `input`: Parameters used (dict)\n  - `output`: Execution result (str)\n  - `token_cost`: Token usage (int)\n  - `success`: Execution status (bool)\n  - `time_cost`: Duration in seconds (float)\n- `msg`: Message containing tool name to retrieve guidelines for\n- **Note**: Tool Memory does NOT provide tool functions (`record_to_memory` and `retrieve_from_memory`). It only provides direct methods for programmatic use.\n\n### ReActAgent Integration Modes\n\nWhen attaching **Personal Memory** or **Task Memory** to ReActAgent, use the `long_term_memory_mode` parameter:\n\n```python\nagent = ReActAgent(\n    name=\"Assistant\",\n    long_term_memory=memory,  # ReMePersonalLongTermMemory or ReMeTaskLongTermMemory\n    long_term_memory_mode=\"both\",  # Options: \"record\", \"retrieve\", \"both\"\n    # ... other parameters\n)\n```\n\n**Modes**:\n- `\"record\"`: Only adds `record_to_memory` tool to agent\n- `\"retrieve\"`: Only adds `retrieve_from_memory` tool to agent\n- `\"both\"`: Adds both tools (recommended for most use cases)\n\n> **Note**: Tool Memory does NOT support ReActAgent integration with tool functions. Use Tool Memory programmatically to enhance system prompts as shown in the Tool Memory example.\n\n### Async Context Manager (Required!)\n\nAll ReMe memory types **must** be used with async context managers:\n\n```python\nasync with long_term_memory:\n    # All memory operations must be within this context\n    await long_term_memory.record(msgs=[...])\n    result = await long_term_memory.retrieve(msg=...)\n```\n\nThis ensures:\n- Proper initialization of the ReMe backend\n- Resource cleanup after operations\n- Vector store connection management\n\n### Custom Configuration\n\n```python\nfrom agentscope.memory import ReMePersonalLongTermMemory\n\n# Custom storage location and models\nmemory = ReMePersonalLongTermMemory(\n    agent_name=\"Friday\",\n    user_name=\"user_123\",\n    model=your_custom_model,  # Any AgentScope-compatible LLM\n    embedding_model=your_embedding,  # Any AgentScope-compatible embedding model\n    vector_store_dir=\"./custom_path\",  # Custom storage directory\n)\n```\n\n## Example Files Overview\n\n### `personal_memory_example.py`\n\nDemonstrates **5 core interfaces** for personal memory:\n\n1. **`record_to_memory()`** - Record user preferences using tool function\n2. **`retrieve_from_memory()`** - Search memories by keywords using tool function\n3. **`record()`** - Direct recording of message conversations\n4. **`retrieve()`** - Direct query-based retrieval\n5. **ReActAgent Integration** - Agent autonomously uses memory tools\n\n**Key Features**:\n- Recording travel preferences, work habits, and personal information\n- Keyword-based and query-based retrieval\n- System prompt guidelines for agent memory usage\n- Automatic memory tool calling by ReActAgent\n\n### `task_memory_example.py`\n\nDemonstrates **5 core interfaces** for task memory:\n\n1. **`record_to_memory()`** - Record task experiences with scores\n2. **`retrieve_from_memory()`** - Retrieve relevant experiences by keywords\n3. **`record()`** - Direct recording with trajectory scores\n4. **`retrieve()`** - Direct experience retrieval\n5. **ReActAgent Integration** - Agent learns from past task executions\n\n**Key Features**:\n- Recording project planning, debugging, and development experiences\n- Score-based trajectory evaluation (0.0-1.0)\n- Learning from successful and failed attempts\n- Continuous improvement through experience retrieval\n\n### `tool_memory_example.py`\n\nDemonstrates the **complete workflow** for tool memory:\n\n1. **Mock tools** - Define and register tools to Toolkit\n2. **Record tool history** - Store execution results with metadata using `record()`\n3. **Retrieve guidelines** - Get summarized usage guidelines using `retrieve()`\n4. **Enhance agent prompt** - Inject guidelines into system prompt\n5. **Use ReActAgent** - Agent uses tools with learned guidelines\n\n**Key Features**:\n- JSON-formatted tool execution recording via direct `record()` method\n- Automatic guideline generation through summarization\n- Multi-tool guideline retrieval via direct `retrieve()` method\n- System prompt enhancement for better tool usage\n- **Note**: Tool Memory does NOT provide agent-callable tool functions\n\n## Architecture\n\n### Inheritance Hierarchy\n\n```\nReMeLongTermMemoryBase (abstract base)\n├── ReMePersonalLongTermMemory\n├── ReMeTaskLongTermMemory\n└── ReMeToolLongTermMemory\n```\n\n**`ReMeLongTermMemoryBase`** provides:\n- Integration with ReMe library's `ReMeApp`\n- Async context manager implementation\n- Common interface definitions\n- Vector store and embedding management\n\n### Memory Storage\n\n- **Location**: `./memory_vector_store/` (configurable)\n- **Isolation**: Each `user_name` maintains separate storage\n- **Persistence**: Memories persist across sessions\n- **Format**: Vector embeddings with metadata\n\n## Best Practices\n\n### 1. System Prompt Design\n\nFor agents with long-term memory, clearly specify when to record and retrieve:\n\n```python\nsys_prompt = \"\"\"\nYou are an assistant with long-term memory.\n\nRecording Guidelines:\n- Record when users share personal information, preferences, or important facts\n- Record successful task execution approaches and solutions\n- Record tool execution results with detailed metadata\n\nRetrieval Guidelines:\n- ALWAYS retrieve before answering questions about past information\n- Retrieve when dealing with similar tasks to past executions\n- Check tool guidelines before using tools\n\"\"\"\n```\n\n### 2. Score Assignment (Task Memory)\n\nUse meaningful scores to prioritize experiences:\n\n```python\n# Successful trajectory\nawait task_memory.record_to_memory(..., score=0.95)\n\n# Partially successful\nawait task_memory.record_to_memory(..., score=0.6)\n\n# Failed trajectory (still useful to learn from)\nawait task_memory.record_to_memory(..., score=0.2)\n```\n\n### 3. Tool Memory Workflow\n\nFollow this pattern for tool memory:\n\n```\n1. Execute tool → 2. Record result → 3. Trigger summarization → 4. Retrieve guidelines → 5. Use in agent\n```\n\n## Troubleshooting\n\n### Common Issues\n\n**Issue**: `RuntimeError: Memory not initialized`\n- **Solution**: Always use `async with memory:` context manager\n\n**Issue**: No memories retrieved\n- **Solution**: Ensure you've recorded memories first and check `user_name` matches\n\n**Issue**: Tool memory not generating guidelines\n- **Solution**: Record multiple tool executions to trigger summarization\n\n**Issue**: Agent not using memory tools\n- **Solution**: Check `long_term_memory_mode=\"both\"` and verify system prompt encourages memory usage\n\n## References\n\n- [ReMe Library](https://github.com/modelscope/ReMe) - Core memory implementation\n- [AgentScope Documentation](https://github.com/modelscope/agentscope) - Framework documentation\n- [DashScope API](https://dashscope.aliyun.com/) - Model API for examples\n"
  },
  {
    "path": "examples/functionality/long_term_memory/reme/personal_memory_example.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Personal memory example demonstrating ReMe personal memory.\n\nThis module provides examples of how to use the ReMePersonalMemory\nclass.\n\nThe example demonstrates 5 core interfaces:\n1. record_to_memory - Tool function for explicit memory recording\n2. retrieve_from_memory - Tool function for keyword-based retrieval\n3. record - Direct method for recording message conversations\n4. retrieve - Direct method for query-based retrieval\n5. ReActAgent integration - Using personal memory with ReActAgent\n\"\"\"\n\nimport asyncio\nimport os\nfrom dotenv import load_dotenv\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.embedding import DashScopeTextEmbedding\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.memory import ReMePersonalLongTermMemory\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import ToolResponse, Toolkit\n\nload_dotenv()\n\n\nasync def test_record_to_memory(\n    memory: ReMePersonalLongTermMemory,\n) -> None:\n    \"\"\"Test the record_to_memory tool function interface.\"\"\"\n    print(\"Interface 1: record_to_memory (Tool Function)\")\n    print(\"-\" * 70)\n    print(\"Purpose: Explicit memory recording with structured content\")\n    print(\"Test case: Recording user's travel preferences...\")\n\n    result: ToolResponse = await memory.record_to_memory(\n        thinking=(\"The user is sharing their travel preferences and habits\"),\n        content=[\n            \"I prefer to stay in homestays when traveling to Hangzhou\",\n            \"I like to visit the West Lake in the morning\",\n            \"I enjoy drinking Longjing tea\",\n        ],\n    )\n    result_text = \" \".join(\n        block.get(\"text\", \"\")\n        for block in result.content\n        if block.get(\"type\") == \"text\"\n    )\n    print(f\"✓ Result: {result_text}\")\n    print(\n        f\"✓ Status: {'Success' if 'Success' in result_text else 'Failed'}\",\n    )\n    print()\n\n\nasync def test_retrieve_from_memory(\n    memory: ReMePersonalLongTermMemory,\n) -> None:\n    \"\"\"Test the retrieve_from_memory tool function interface.\"\"\"\n    print(\"Interface 2: retrieve_from_memory (Tool Function)\")\n    print(\"-\" * 70)\n    print(\"Purpose: Keyword-based memory retrieval\")\n    print()\n\n    result = await memory.retrieve_from_memory(\n        keywords=[\"Hangzhou travel\", \"tea preference\"],\n    )\n    retrieved_text = \" \".join(\n        block.get(\"text\", \"\")\n        for block in result.content\n        if block.get(\"type\") == \"text\"\n    )\n    print(\"✓ Retrieved memories:\")\n    print(f\"{retrieved_text}\")\n    print()\n\n\nasync def test_record_direct(memory: ReMePersonalLongTermMemory) -> None:\n    \"\"\"Test the direct record method interface.\"\"\"\n    print(\"Interface 3: record (Direct Recording)\")\n    print(\"-\" * 70)\n    print(\"Purpose: Direct recording of message conversations\")\n    print()\n    print(\"Test case: Recording work preferences and habits...\")\n\n    try:\n        await memory.record(\n            msgs=[\n                Msg(\n                    role=\"user\",\n                    content=(\n                        \"I work as a software engineer and prefer \"\n                        \"remote work\"\n                    ),\n                    name=\"user\",\n                ),\n                Msg(\n                    role=\"assistant\",\n                    content=(\n                        \"Understood! You're a software engineer who \"\n                        \"values remote work flexibility.\"\n                    ),\n                    name=\"assistant\",\n                ),\n                Msg(\n                    role=\"user\",\n                    content=(\n                        \"I usually start my day at 9 AM with a \"\n                        \"cup of coffee\"\n                    ),\n                    name=\"user\",\n                ),\n            ],\n        )\n        print(\"✓ Status: Successfully recorded conversation messages\")\n        print(\n            \"✓ Messages recorded: 3 messages (user-assistant dialogue)\",\n        )\n    except Exception as e:\n        print(f\"✗ Status: Failed - {str(e)}\")\n    print()\n\n\nasync def test_retrieve_direct(memory: ReMePersonalLongTermMemory) -> None:\n    \"\"\"Test the direct retrieve method interface.\"\"\"\n    print(\"Interface 4: retrieve (Direct Retrieval)\")\n    print(\"-\" * 70)\n    print(\"Purpose: Query-based memory retrieval using messages\")\n    print()\n    print(\n        \"Test case: Querying 'What do you know about my \"\n        \"work preferences?'...\",\n    )\n\n    memories = await memory.retrieve(\n        msg=Msg(\n            role=\"user\",\n            content=\"What do you know about my work preferences?\",\n            name=\"user\",\n        ),\n    )\n    print(\"✓ Retrieved memories:\")\n    print(f\"{memories if memories else 'No memories found'}\")\n    status = (\n        \"Success - Found memories\"\n        if memories\n        else \"No relevant memories found\"\n    )\n    print(f\"✓ Status: {status}\")\n    print()\n\n\nasync def test_react_agent_with_memory(\n    memory: ReMePersonalLongTermMemory,\n) -> None:\n    \"\"\"Test ReActAgent integration with personal memory.\"\"\"\n    print(\"Interface 5: ReActAgent with Personal Memory\")\n    print(\"-\" * 70)\n    print(\n        \"Purpose: Demonstrate how ReActAgent uses personal memory tools\",\n    )\n    print()\n    print(\"Test case: Agent-driven memory recording and retrieval...\")\n\n    toolkit = Toolkit()\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=(\n            \"You are a helpful assistant named Friday with long-term \"\n            \"memory capabilities. \"\n            \"\\n\\n## Memory Management Guidelines:\\n\"\n            \"1. **Recording Memories**: When users share personal \"\n            \"information, preferences, \"\n            \"habits, or facts about themselves, ALWAYS record them \"\n            \"using `record_to_memory` \"\n            \"for future reference.\\n\"\n            \"\\n2. **Retrieving Memories**: BEFORE answering questions \"\n            \"about the user's preferences, \"\n            \"past information, or personal details, you MUST FIRST \"\n            \"call `retrieve_from_memory` \"\n            \"to check if you have any relevant stored information. \"\n            \"Do NOT rely solely on the \"\n            \"current conversation context.\\n\"\n            \"\\n3. **When to Retrieve**: Call `retrieve_from_memory` \"\n            \"when:\\n\"\n            \"   - User asks questions like 'what do I like?', \"\n            \"'what are my preferences?', \"\n            \"'what do you know about me?'\\n\"\n            \"   - User asks about their past behaviors, habits, or \"\n            \"preferences\\n\"\n            \"   - User refers to information they mentioned before\\n\"\n            \"   - You need context about the user to provide \"\n            \"personalized responses\\n\"\n            \"\\nAlways check your memory first before claiming you \"\n            \"don't know something about the user.\"\n        ),\n        model=DashScopeChatModel(\n            model_name=\"qwen3-max\",\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            stream=False,\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n        memory=InMemoryMemory(),\n        long_term_memory=memory,\n        long_term_memory_mode=\"both\",\n    )\n\n    await agent.memory.clear()\n\n    print(\n        \"→ User: 'When I travel to Hangzhou, I prefer to stay in \"\n        \"a homestay'\",\n    )\n    msg = Msg(\n        role=\"user\",\n        content=(\n            \"When I travel to Hangzhou, I prefer to stay in \" \"a homestay\"\n        ),\n        name=\"user\",\n    )\n    msg = await agent(msg)\n    print(f\"✓ Agent response: {msg.get_text_content()}\\n\")\n\n    print(\"→ User: 'what preference do I have?'\")\n    msg = Msg(\n        role=\"user\",\n        content=\"what preference do I have?\",\n        name=\"user\",\n    )\n    msg = await agent(msg)\n    print(f\"✓ Agent response: {msg.get_text_content()}\\n\")\n\n    print(\n        \"✓ Status: Successfully demonstrated ReActAgent with \"\n        \"personal memory\",\n    )\n    print()\n\n\nasync def main() -> None:\n    \"\"\"Demonstrate the 5 core interfaces of ReMePersonalMemory.\n\n    This example shows how to use:\n    1. record_to_memory - Tool function for explicit memory recording\n    2. retrieve_from_memory - Tool function for keyword-based retrieval\n    3. record - Direct method for recording message conversations\n    4. retrieve - Direct method for query-based retrieval\n    5. ReActAgent integration - Using personal memory with ReActAgent\n    \"\"\"\n    long_term_memory = ReMePersonalLongTermMemory(\n        agent_name=\"Friday\",\n        user_name=\"user_123\",\n        model=DashScopeChatModel(\n            model_name=\"qwen3-max\",\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            stream=False,\n        ),\n        embedding_model=DashScopeTextEmbedding(\n            model_name=\"text-embedding-v4\",\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            dimensions=1024,\n        ),\n    )\n\n    print(\"=\" * 70)\n    print(\"ReMePersonalMemory - Testing 5 Core Interfaces\")\n    print(\"=\" * 70)\n    print()\n\n    # Use async context manager to ensure proper initialization\n    async with long_term_memory:\n        # await test_record_to_memory(long_term_memory)\n        # await test_retrieve_from_memory(long_term_memory)\n        # await test_record_direct(long_term_memory)\n        # await test_retrieve_direct(long_term_memory)\n        await test_react_agent_with_memory(long_term_memory)\n\n    # Alternative way: manually call __aenter__ and __aexit__\n    # This is equivalent to using \"async with long_term_memory\" above\n    # await long_term_memory.__aenter__()\n    # await test_react_agent_with_memory(long_term_memory)\n    # await long_term_memory.__aexit__()\n\n    print(\"=\" * 70)\n    print(\"Testing Complete: All 5 Core Interfaces Verified!\")\n    print(\"=\" * 70)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/long_term_memory/reme/task_memory_example.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Task memory example demonstrating ReMe task memory.\n\nThis module provides examples of how to use the ReMeTaskMemory class\nusing the ReMe library.\n\nThe example demonstrates 5 core interfaces:\n1. record_to_memory - Tool function for recording task information\n2. retrieve_from_memory - Tool function for keyword-based retrieval\n3. record - Direct method for recording message conversations\n   with scores\n4. retrieve - Direct method for retrieving task experiences\n5. ReActAgent integration - Using task memory with ReActAgent\n\"\"\"\n\nimport asyncio\nimport os\nfrom dotenv import load_dotenv\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.embedding import DashScopeTextEmbedding\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.memory import ReMeTaskLongTermMemory\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import ToolResponse, Toolkit\n\nload_dotenv()\n\n\nasync def test_record_to_memory(memory: ReMeTaskLongTermMemory) -> None:\n    \"\"\"Test the record_to_memory tool function interface.\"\"\"\n    print(\"Interface 1: record_to_memory (Tool Function)\")\n    print(\"-\" * 70)\n    print(\n        \"Purpose: Record task execution information with thinking and content\",\n    )\n    print()\n    print(\"Test case: Recording project planning task information...\")\n\n    result: ToolResponse = await memory.record_to_memory(\n        thinking=(\n            \"Recording project planning best practices and \"\n            \"development approach\"\n        ),\n        content=[\n            \"For web application projects, break down into phases: \"\n            \"Requirements gathering, Design, Development, Testing, \"\n            \"Deployment\",\n            \"Development phase recommendations: Frontend (React), \"\n            \"Backend (FastAPI), Database (PostgreSQL), Agile \"\n            \"methodology with 2-week sprints\",\n            \"Dependency management: Use npm for frontend and pip for \"\n            \"Python backend, maintain requirements.txt and \"\n            \"package.json files\",\n        ],\n        score=0.9,  # Optional: score for this trajectory (1.0)\n    )\n    result_text = \" \".join(\n        block.get(\"text\", \"\")\n        for block in result.content\n        if block.get(\"type\") == \"text\"\n    )\n    print(f\"✓ Result: {result_text}\")\n    print(\n        f\"✓ Status: {'Success' if 'Success' in result_text else 'Failed'}\",\n    )\n    print()\n\n\nasync def test_retrieve_from_memory(\n    memory: ReMeTaskLongTermMemory,\n) -> None:\n    \"\"\"Test the retrieve_from_memory tool function interface.\"\"\"\n    print(\"Interface 2: retrieve_from_memory (Tool Function)\")\n    print(\"-\" * 70)\n    print(\"Purpose: Keyword-based retrieval of task experiences\")\n    print()\n    print(\n        \"Test case: Searching with keywords 'project planning', \"\n        \"'development phase'...\",\n    )\n\n    result = await memory.retrieve_from_memory(\n        keywords=[\"project planning\", \"development phase\"],\n    )\n    retrieved_text = \" \".join(\n        block.get(\"text\", \"\")\n        for block in result.content\n        if block.get(\"type\") == \"text\"\n    )\n    print(\"✓ Retrieved experiences:\")\n    print(f\"{retrieved_text}\")\n    has_experiences = (\n        retrieved_text and \"No task experiences found\" not in retrieved_text\n    )\n    status = (\n        \"Success - Found experiences\"\n        if has_experiences\n        else \"No relevant experiences found\"\n    )\n    print(f\"✓ Status: {status}\")\n    print()\n\n\nasync def test_record_direct(memory: ReMeTaskLongTermMemory) -> None:\n    \"\"\"Test the direct record method interface.\"\"\"\n    print(\"Interface 3: record (Direct Recording)\")\n    print(\"-\" * 70)\n    print(\"Purpose: Direct recording of message conversations with scores\")\n    print()\n    print(\"Test case: Recording debugging task conversation...\")\n\n    try:\n        await memory.record(\n            msgs=[\n                Msg(\n                    role=\"user\",\n                    content=\"I'm getting a 404 error on my API endpoint\",\n                    name=\"user\",\n                ),\n                Msg(\n                    role=\"assistant\",\n                    content=(\n                        \"Let's troubleshoot: 1) Check if the route is \"\n                        \"properly defined, 2) Verify the URL path, \"\n                        \"3) Ensure the server is running on the correct \"\n                        \"port\"\n                    ),\n                    name=\"assistant\",\n                ),\n                Msg(\n                    role=\"user\",\n                    content=\"Found it! The route path had a typo.\",\n                    name=\"user\",\n                ),\n                Msg(\n                    role=\"assistant\",\n                    content=(\n                        \"Great! Always double-check route paths and use \"\n                        \"a linter to catch typos early.\"\n                    ),\n                    name=\"assistant\",\n                ),\n            ],\n            score=0.95,  # Optional: score (default: 1.0)\n        )\n        print(\"✓ Status: Successfully recorded debugging trajectory\")\n        print(\"✓ Messages recorded: 4 messages with score 0.95\")\n    except Exception as e:\n        print(f\"✗ Status: Failed - {str(e)}\")\n    print()\n\n\nasync def test_retrieve_direct(memory: ReMeTaskLongTermMemory) -> None:\n    \"\"\"Test the direct retrieve method interface.\"\"\"\n    print(\"Interface 4: retrieve (Direct Retrieval)\")\n    print(\"-\" * 70)\n    print(\"Purpose: Query-based retrieval using messages\")\n    print()\n    print(\"Test case: Querying 'How to debug API errors?'...\")\n\n    memories = await memory.retrieve(\n        msg=Msg(\n            role=\"user\",\n            content=(\n                \"How should I approach debugging API errors in my \"\n                \"application?\"\n            ),\n            name=\"user\",\n        ),\n    )\n    print(\"✓ Retrieved experiences:\")\n    print(f\"{memories if memories else 'No experiences found'}\")\n    status = (\n        \"Success - Found experiences\"\n        if memories\n        else \"No relevant experiences found\"\n    )\n    print(f\"✓ Status: {status}\")\n    print()\n\n\nasync def test_react_agent_with_memory(\n    memory: ReMeTaskLongTermMemory,\n) -> None:\n    \"\"\"Test ReActAgent integration with task memory.\"\"\"\n    print(\"Interface 5: ReActAgent with Task Memory\")\n    print(\"-\" * 70)\n    print(\n        \"Purpose: Demonstrate how ReActAgent uses task memory tools\",\n    )\n    print()\n    print(\n        \"Test case: Agent-driven task experience recording and \"\n        \"retrieval...\",\n    )\n\n    toolkit = Toolkit()\n    agent = ReActAgent(\n        name=\"TaskAssistant\",\n        sys_prompt=(\n            \"You are a helpful task assistant named TaskAssistant \"\n            \"with long-term task memory. \"\n            \"\\n\\n## Task Memory Management Guidelines:\\n\"\n            \"1. **Recording Task Experiences**: When you provide \"\n            \"technical solutions, solve problems, \"\n            \"or complete tasks, ALWAYS record the key insights using \"\n            \"`record_to_memory`. Include:\\n\"\n            \"   - Specific techniques and approaches used\\n\"\n            \"   - Best practices and implementation details\\n\"\n            \"   - Lessons learned and important considerations\\n\"\n            \"   - Step-by-step procedures that worked well\\n\"\n            \"\\n2. **Retrieving Past Experiences**: BEFORE solving a \"\n            \"problem or answering technical \"\n            \"questions, you MUST FIRST call `retrieve_from_memory` \"\n            \"to check if you have relevant \"\n            \"past experiences. This helps you:\\n\"\n            \"   - Avoid repeating past mistakes\\n\"\n            \"   - Leverage proven solutions\\n\"\n            \"   - Provide more accurate and tested approaches\\n\"\n            \"\\n3. **When to Retrieve**: Always retrieve when:\\n\"\n            \"   - Asked about technical topics or problem-solving \"\n            \"approaches\\n\"\n            \"   - Asked to provide recommendations or best practices\\n\"\n            \"   - Dealing with tasks similar to ones you may have \"\n            \"handled before\\n\"\n            \"   - User explicitly asks 'what do you know about...?' \"\n            \"or 'have you seen this before?'\\n\"\n            \"\\nAlways check your task memory first to provide the \"\n            \"most informed responses.\"\n        ),\n        model=DashScopeChatModel(\n            model_name=\"qwen3-max\",\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            stream=False,\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n        memory=InMemoryMemory(),\n        long_term_memory=memory,\n        long_term_memory_mode=\"both\",\n    )\n\n    await agent.memory.clear()\n\n    print(\n        \"→ User: 'Here are some database optimization techniques \"\n        \"I learned'\",\n    )\n    msg = Msg(\n        role=\"user\",\n        content=(\n            \"I just learned some valuable database optimization \"\n            \"techniques for slow queries: \"\n            \"1) Add indexes on foreign keys and WHERE clause columns \"\n            \"to speed up joins and filtering. \"\n            \"2) Use table partitioning to divide large tables by \"\n            \"date or category for faster queries. \"\n            \"3) Implement query result caching with Redis to avoid \"\n            \"repeated database hits. \"\n            \"4) Optimize JOIN order - put smallest tables first to \"\n            \"reduce intermediate result sets. \"\n            \"5) Use EXPLAIN ANALYZE to identify bottlenecks and \"\n            \"missing indexes. \"\n            \"Please record these optimization techniques for future \"\n            \"reference.\"\n        ),\n        name=\"user\",\n    )\n    msg = await agent(msg)\n    print(f\"✓ Agent response: {msg.get_text_content()}\\n\")\n\n    print(\n        \"→ User: 'What do you know about database optimization?'\",\n    )\n    msg = Msg(\n        role=\"user\",\n        content=(\n            \"What do you know about database optimization? \"\n            \"Can you retrieve any past experiences?\"\n        ),\n        name=\"user\",\n    )\n    msg = await agent(msg)\n    print(f\"✓ Agent response: {msg.get_text_content()}\\n\")\n\n    print()\n\n\nasync def main() -> None:\n    \"\"\"Demonstrate the 5 core interfaces of ReMeTaskMemory.\n\n    This example shows how to use:\n    1. record_to_memory - Tool function for recording task information\n    2. retrieve_from_memory - Tool function for keyword-based retrieval\n    3. record - Direct method for recording message conversations with scores\n    4. retrieve - Direct method for retrieving task experiences\n    5. ReActAgent integration - Using task memory with ReActAgent\n    \"\"\"\n    long_term_memory = ReMeTaskLongTermMemory(\n        agent_name=\"TaskAssistant\",\n        user_name=\"task_workspace_123\",\n        model=DashScopeChatModel(\n            model_name=\"qwen3-max\",\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            stream=False,\n        ),\n        embedding_model=DashScopeTextEmbedding(\n            model_name=\"text-embedding-v4\",\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            dimensions=1024,\n        ),\n    )\n\n    print(\"=\" * 70)\n    print(\"ReMeTaskMemory - Testing 5 Core Interfaces\")\n    print(\"=\" * 70)\n    print()\n\n    # Use async context manager to ensure proper initialization\n    async with long_term_memory:\n        # await test_record_to_memory(long_term_memory)\n        # await test_retrieve_from_memory(long_term_memory)\n        # await test_record_direct(long_term_memory)\n        # await test_retrieve_direct(long_term_memory)\n        await test_react_agent_with_memory(long_term_memory)\n\n    # Alternative way: manually call __aenter__ and __aexit__\n    # This is equivalent to using \"async with long_term_memory\" above\n    # await long_term_memory.__aenter__()\n    # await test_react_agent_with_memory(long_term_memory)\n    # await long_term_memory.__aexit__()\n\n    print(\"=\" * 70)\n    print(\"Testing Complete: All 5 Core Interfaces Verified!\")\n    print(\"=\" * 70)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/long_term_memory/reme/tool_memory_example.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tool memory example demonstrating ReMe tool memory with ReActAgent.\n\nThis module demonstrates the complete workflow:\n1. Mock a tool function and register it to Toolkit\n2. Record tool execution results to tool memory using record()\n3. Retrieve tool usage guidelines using retrieve()\n4. Inject guidelines into ReActAgent's system prompt\n5. Use ReActAgent with tool memory\n\nThis workflow helps LLMs learn from past tool usage patterns and\nimprove their tool calling decisions over time.\n\"\"\"\n\nimport asyncio\nimport json\nimport os\nfrom datetime import datetime\nfrom dotenv import load_dotenv\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.embedding import DashScopeTextEmbedding\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.memory import ReMeToolLongTermMemory\nfrom agentscope.message import Msg\nfrom agentscope.message import TextBlock\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import Toolkit, ToolResponse\n\nload_dotenv()\n\n\n# ============================================================================\n# Step 1: Mock tool functions\n# ============================================================================\n\n\nasync def web_search(query: str, max_results: int = 5) -> ToolResponse:\n    \"\"\"Search the web for information.\n\n    Args:\n        query: The search query string\n        max_results: Maximum number of results to return\n\n    Returns:\n        ToolResponse containing search results\n    \"\"\"\n    # Simulate web search\n    result = f\"Found {max_results} results for query: '{query}'\"\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=result,\n            ),\n        ],\n    )\n\n\nasync def calculate(expression: str) -> ToolResponse:\n    \"\"\"Calculate a mathematical expression.\n\n    Args:\n        expression: Mathematical expression to evaluate\n\n    Returns:\n        ToolResponse containing calculation result\n    \"\"\"\n    try:\n        # Simple calculation (in real scenario, use safer evaluation)\n        result = eval(expression)  # noqa: S307\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=f\"Result: {result}\",\n                ),\n            ],\n        )\n    except Exception as e:\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=f\"Error calculating '{expression}': {str(e)}\",\n                ),\n            ],\n        )\n\n\n# ============================================================================\n# Step 2: Record tool execution history to tool memory\n# ============================================================================\n\n\nasync def record_tool_history(\n    tool_memory: ReMeToolLongTermMemory,\n) -> None:\n    \"\"\"Record historical tool execution results to tool memory.\n\n    This simulates past tool usage that the agent can learn from.\n    \"\"\"\n    print(\"=\" * 70)\n    print(\"Step 1: Recording Tool Execution History to Memory\")\n    print(\"=\" * 70)\n    print()\n\n    # Record successful web_search examples\n    web_search_histories = [\n        {\n            \"create_time\": datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\"),\n            \"tool_name\": \"web_search\",\n            \"input\": {\n                \"query\": \"Python asyncio tutorial\",\n                \"max_results\": 10,\n            },\n            \"output\": (\n                \"Found 10 results for query: 'Python asyncio tutorial'\"\n            ),\n            \"token_cost\": 150,\n            \"success\": True,\n            \"time_cost\": 2.3,\n        },\n        {\n            \"create_time\": datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\"),\n            \"tool_name\": \"web_search\",\n            \"input\": {\n                \"query\": \"machine learning basics\",\n                \"max_results\": 5,\n            },\n            \"output\": (\"Found 5 results for query: 'machine learning basics'\"),\n            \"token_cost\": 120,\n            \"success\": True,\n            \"time_cost\": 1.8,\n        },\n    ]\n\n    # Record failed web_search example (empty query)\n    web_search_fail = {\n        \"create_time\": datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\"),\n        \"tool_name\": \"web_search\",\n        \"input\": {\n            \"query\": \"\",\n            \"max_results\": 5,\n        },\n        \"output\": \"Error: Query cannot be empty\",\n        \"token_cost\": 20,\n        \"success\": False,\n        \"time_cost\": 0.1,\n    }\n\n    # Record calculate examples\n    calculate_histories = [\n        {\n            \"create_time\": datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\"),\n            \"tool_name\": \"calculate\",\n            \"input\": {\n                \"expression\": \"2 + 2\",\n            },\n            \"output\": \"Result: 4\",\n            \"token_cost\": 30,\n            \"success\": True,\n            \"time_cost\": 0.05,\n        },\n        {\n            \"create_time\": datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\"),\n            \"tool_name\": \"calculate\",\n            \"input\": {\n                \"expression\": \"10 * 5 + 3\",\n            },\n            \"output\": \"Result: 53\",\n            \"token_cost\": 30,\n            \"success\": True,\n            \"time_cost\": 0.05,\n        },\n    ]\n\n    # Record all histories\n    all_histories = (\n        web_search_histories + [web_search_fail] + calculate_histories\n    )\n\n    print(f\"Recording {len(all_histories)} tool execution histories...\")\n    await tool_memory.record(\n        msgs=[\n            Msg(\n                role=\"assistant\",\n                content=json.dumps(history),\n                name=\"assistant\",\n            )\n            for history in all_histories\n        ],\n    )\n    print(f\"✓ Successfully recorded {len(all_histories)} tool executions\")\n    print()\n\n\n# ============================================================================\n# Step 3: Retrieve tool guidelines and create enhanced system prompt\n# ============================================================================\n\n\nasync def retrieve_tool_guidelines(\n    tool_memory: ReMeToolLongTermMemory,\n    tool_names: list[str],\n) -> str:\n    \"\"\"Retrieve tool usage guidelines from memory.\n\n    Args:\n        tool_memory: The ReMeToolMemory instance\n        tool_names: List of tool names to retrieve guidelines for\n\n    Returns:\n        Combined guidelines text to be added to system prompt\n    \"\"\"\n    print(\"=\" * 70)\n    print(\"Step 2: Retrieving Tool Usage Guidelines from Memory\")\n    print(\"=\" * 70)\n    print()\n\n    all_guidelines = []\n\n    for tool_name in tool_names:\n        print(f\"Retrieving guidelines for '{tool_name}'...\")\n        guidelines = await tool_memory.retrieve(\n            msg=Msg(\n                role=\"user\",\n                content=tool_name,\n                name=\"user\",\n            ),\n        )\n\n        if guidelines:\n            all_guidelines.append(\n                f\"## Guidelines for {tool_name}:\\n{guidelines}\",\n            )\n            print(f\"✓ Retrieved guidelines for '{tool_name}'\")\n            print(f\"  Preview: {guidelines}\")\n        else:\n            print(\n                f\"✓ No guidelines found for '{tool_name}' \" \"(first time use)\",\n            )\n        print()\n\n    if all_guidelines:\n        combined_guidelines = \"\\n\\n\".join(all_guidelines)\n        guidelines_prompt = f\"\"\"\n# Tool Usage Guidelines (from past experience)\n\n{combined_guidelines}\n\nPlease follow these guidelines when using the tools.\n\"\"\"\n        return guidelines_prompt\n    else:\n        return \"\"\n\n\n# ============================================================================\n# Step 4: Use ReActAgent with tool memory\n# ============================================================================\n\n\nasync def use_react_agent_with_tool_memory(\n    toolkit: Toolkit,\n    tool_guidelines: str,\n) -> None:\n    \"\"\"Create and use ReActAgent with tool memory guidelines.\n\n    Args:\n        toolkit: The Toolkit with registered tools\n        tool_guidelines: Retrieved tool usage guidelines\n    \"\"\"\n    print(\"=\" * 70)\n    print(\"Step 3: Using ReActAgent with Tool Memory\")\n    print(\"=\" * 70)\n    print()\n\n    # Create enhanced system prompt with tool guidelines\n    base_sys_prompt = (\n        \"You are a helpful AI assistant named ToolBot.\\n\"\n        \"You have access to various tools to help users complete \"\n        \"their tasks.\\n\"\n        \"Please use the tools appropriately based on the user's \"\n        \"requests.\"\n    )\n\n    if tool_guidelines:\n        sys_prompt = f\"{base_sys_prompt}\\n{tool_guidelines}\"\n        print(\n            \"✓ System prompt enhanced with tool memory guidelines\",\n        )\n    else:\n        sys_prompt = base_sys_prompt\n        print(\n            \"✓ Using base system prompt (no guidelines available)\",\n        )\n\n    print()\n\n    # Create ReActAgent\n    agent = ReActAgent(\n        name=\"ToolBot\",\n        sys_prompt=sys_prompt,\n        model=DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            stream=False,\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n        memory=InMemoryMemory(),\n        max_iters=5,\n    )\n\n    print(\"✓ ReActAgent created successfully\")\n    print()\n\n    # Test queries\n    test_queries = [\n        \"Search the web for 'Python design patterns'\",\n        \"Calculate 15 * 7 + 23\",\n    ]\n\n    print(\"-\" * 70)\n    print(\"Testing ReActAgent with tool memory...\")\n    print(\"-\" * 70)\n    print()\n\n    for i, query in enumerate(test_queries, 1):\n        print(f\"Query {i}: {query}\")\n        print(\"-\" * 70)\n\n        msg = Msg(\n            role=\"user\",\n            content=query,\n            name=\"user\",\n        )\n\n        response = await agent(msg)\n        print(f\"Response: {response.get_text_content()}\")\n        print()\n        print()\n\n\nasync def main() -> None:\n    \"\"\"Demonstrate the workflow of using ReMeToolMemory with ReActAgent.\n\n    This example shows:\n    1. Create mock tools and register them to Toolkit\n    2. Record historical tool execution results to tool memory\n    3. Retrieve tool usage guidelines from memory\n    4. Inject guidelines into ReActAgent's system prompt\n    5. Use ReActAgent to complete tasks with tool memory\n    \"\"\"\n    print(\"=\" * 70)\n    print(\"ReMeToolMemory + ReActAgent Integration Example\")\n    print(\"=\" * 70)\n    print()\n    print(\"This workflow demonstrates:\")\n    print(\"1. Mock tools → Register to Toolkit\")\n    print(\"2. Record tool execution history → Tool Memory\")\n    print(\"3. Retrieve guidelines → Enhance system prompt\")\n    print(\"4. Use ReActAgent with tool memory\")\n    print()\n\n    # Initialize tool memory\n    tool_memory = ReMeToolLongTermMemory(\n        agent_name=\"ToolBot\",\n        user_name=\"tool_workspace_demo\",\n        model=DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            stream=False,\n        ),\n        embedding_model=DashScopeTextEmbedding(\n            model_name=\"text-embedding-v4\",\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            dimensions=1024,\n        ),\n    )\n\n    # Create and register tools to toolkit\n    toolkit = Toolkit()\n    toolkit.register_tool_function(web_search)\n    toolkit.register_tool_function(calculate)\n    print()\n\n    # Use async context manager for tool memory\n    async with tool_memory:\n        # Step 1: Record historical tool executions to memory\n        await record_tool_history(tool_memory)\n\n        # Step 2: Retrieve tool usage guidelines\n        tool_names = [\"web_search\", \"calculate\"]\n        tool_guidelines = await retrieve_tool_guidelines(\n            tool_memory,\n            tool_names,\n        )\n\n        # Step 3: Use ReActAgent with enhanced system prompt\n        await use_react_agent_with_tool_memory(\n            toolkit,\n            tool_guidelines,\n        )\n\n    # Alternative way: manually call __aenter__ and __aexit__\n    # This is equivalent to using \"async with tool_memory\" above\n    # await tool_memory.__aenter__()\n    # tool_guidelines = await retrieve_tool_guidelines(tool_memory, tool_names)\n    # await tool_memory.__aexit__()\n\n    print(\"=\" * 70)\n    print(\"Workflow Complete!\")\n    print(\"=\" * 70)\n    print()\n    print(\"Summary:\")\n    print(\"✓ Mock tools created and registered to Toolkit\")\n    print(\"✓ Historical tool executions recorded to tool memory\")\n    print(\"✓ Tool usage guidelines retrieved from memory\")\n    print(\"✓ ReActAgent system prompt enhanced with guidelines\")\n    print(\n        \"✓ ReActAgent successfully used tools with memory guidance\",\n    )\n    print()\n    print(\"Benefits:\")\n    print(\"- Agent learns from past tool usage patterns\")\n    print(\"- Reduced errors by following proven guidelines\")\n    print(\"- Better tool parameter selection\")\n    print(\"- Improved success rate over time\")\n    print(\"=\" * 70)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/mcp/README.md",
    "content": "# MCP in AgentScope\n\nThis example demonstrates how to\n\n- create MCP client with different transports (SSE and Streamable HTTP) and type (Stateless and Stateful),\n- register MCP tool functions and use them in a ReAct agent, and\n- get MCP tool function as a local callable object from the MCP client.\n\n\n## Prerequisites\n\n- Python 3.10 or higher\n- DashScope API key from Alibaba Cloud\n\n## Installation\n\n### Install AgentScope\n\n```bash\n# Install from source\ncd {PATH_TO_AGENTSCOPE}\npip install -e .\n```\n\n## QuickStart\n\nInstall agentscope and ensure you have a valid DashScope API key in your environment variables.\n\n> Note: The example is built with DashScope chat model. If you want to change the model in this example, don't forget\n> to change the formatter at the same time! The corresponding relationship between built-in models and formatters are\n> list in [our tutorial](https://doc.agentscope.io/tutorial/task_prompt.html#id1)\n\n```bash\npip install agentscope\n```\n\nStart the MCP servers by the following commands in two separate terminals:\n\n```bash\n# In one terminal, run:\npython mcp_add.py\n\n# In another terminal, run:\npython mcp_multiply.py\n```\n\nTwo MCP servers will be started on `http://127.0.0.1:8001` (SSE server) and `http://127.0.0.1:8002` (streamable\nHTTP server).\n\nAfter starting the MCP servers, you can run the agent example:\n\n```bash\npython main.py\n```\n\nThe agent will:\n1. Register the MCP tools from the servers\n2. Use a ReAct agent to solve a calculation problem (multiplying two numbers and then adding another number)\n3. Return structured output with the final result\n"
  },
  {
    "path": "examples/functionality/mcp/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nDemo showcasing ReAct agent with MCP tools using different transports.\n\nThis example demonstrates:\n- Registering MCP tools with different transports (sse and streamable_http)\n- Using a ReAct agent with registered MCP tools\n- Getting structured output from the agent\n\nBefore running this demo, please execute:\n    python mcp_servers.py\n\"\"\"\n\nimport asyncio\nimport json\nimport os\n\nfrom pydantic import BaseModel, Field\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.mcp import HttpStatelessClient, HttpStatefulClient\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import Toolkit\n\n\nclass NumberResult(BaseModel):\n    \"\"\"A simple number result model for structured output.\"\"\"\n\n    result: int = Field(description=\"The result of the calculation\")\n\n\nasync def main() -> None:\n    \"\"\"The main entry of the MCP example.\"\"\"\n\n    toolkit = Toolkit()\n\n    # Create a stateful MCP client to connect to the SSE MCP server\n    # note you can also use the stateless client\n    add_mcp_client = HttpStatefulClient(\n        name=\"add_mcp\",\n        transport=\"sse\",\n        url=\"http://127.0.0.1:8001/sse\",\n    )\n\n    # Create a stateless MCP client to connect to the StreamableHTTP MCP server\n    # note you can also use the stateful client\n    multiply_mcp_client = HttpStatelessClient(\n        name=\"multiply_mcp\",\n        transport=\"streamable_http\",\n        url=\"http://127.0.0.1:8002/mcp\",\n    )\n\n    # The stateful client must be connected before using\n    await add_mcp_client.connect()\n\n    # Register the MCP clients to the toolkit\n    await toolkit.register_mcp_client(add_mcp_client)\n    await toolkit.register_mcp_client(multiply_mcp_client)\n\n    # Initialize the agent\n    agent = ReActAgent(\n        name=\"Jarvis\",\n        sys_prompt=\"You're a helpful assistant named Jarvis.\",\n        model=DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n    )\n\n    # Run the agent with a calculation task\n    res = await agent(\n        Msg(\n            \"user\",\n            \"Calculate 2345 multiplied by 3456, then add 4567 to the result,\"\n            \" what is the final outcome?\",\n            \"user\",\n        ),\n        structured_model=NumberResult,\n    )\n\n    print(\n        \"Structured Output:\\n\"\n        \"```\\n\"\n        f\"{json.dumps(res.metadata, indent=4, ensure_ascii=False)}\\n\"\n        \"```\",\n    )\n\n    # AgentScope also allows developers to obtain the MCP tool as a local\n    #  callable object, and use it directly.\n    add_tool_function = await add_mcp_client.get_callable_function(\n        \"add\",\n        # If wrap the MCP tool result into the ToolResponse object in\n        #  AgentScope\n        wrap_tool_result=True,\n    )\n\n    # Call it manually\n    manual_res = await add_tool_function(a=5, b=10)\n    print(\"When manually calling the MCP tool function:\")\n    print(manual_res)\n\n    # The stateful client should be disconnected manually!\n    await add_mcp_client.close()\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/mcp/mcp_add.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"An SSE MCP server with a simple add tool function.\"\"\"\n\nfrom mcp.server import FastMCP\n\n\nmcp = FastMCP(\"Add\", port=8001)\n\n\n@mcp.tool()\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers.\"\"\"\n    return a + b\n\n\nmcp.run(transport=\"sse\")\n"
  },
  {
    "path": "examples/functionality/mcp/mcp_multiply.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"An SSE MCP server with a simple multiply tool function.\"\"\"\n\nfrom mcp.server import FastMCP\n\n\nmcp = FastMCP(\"Multiply\", port=8002)\n\n\n@mcp.tool()\ndef multiply(c: int, d: int) -> int:\n    \"\"\"Multiply two numbers.\"\"\"\n    return c * d\n\n\nmcp.run(transport=\"streamable-http\")\n"
  },
  {
    "path": "examples/functionality/plan/README.md",
    "content": "# Plan with ReAct Agent\n\nThis example demonstrates how to use the plan module in AgentScope to make an agent create and manage a plan formally.\n\nSpecifically, we provide two examples: manual specification plan and Agent-managed plan.\n\n## Manual Specification Plan\n\nIn this example, we first manually specify a plan for the agent to follow, then we let the agent execute the plan step by step.\n\nTo execute this example, run:\n\n```bash\npython main_manual_plan.py\n```\n\n## Agent-managed Plan\n\nIn this example, we let the agent create and manage its own plan.\nSpecifically, we use a query \"Review the recent changes in AgentScope GitHub repository over the past month.\"\n\nTo run the example, execute:\n\n```bash\npython main_agent_managed_plan.py\n```\n\n> Note: The example is built with DashScope chat model. If you want to change the model in this example, don't forget\n> to change the **formatter** at the same time! The corresponding relationship between built-in models and formatters\n> are list in [our tutorial](https://doc.agentscope.io/tutorial/task_prompt.html#id1)"
  },
  {
    "path": "examples/functionality/plan/main_agent_managed_plan.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The main entry point of the plan example.\"\"\"\nimport asyncio\nimport os\n\nfrom agentscope.agent import ReActAgent, UserAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.plan import PlanNotebook\nfrom agentscope.tool import (\n    Toolkit,\n    execute_shell_command,\n    execute_python_code,\n    write_text_file,\n    insert_text_file,\n    view_text_file,\n)\n\n\nasync def main() -> None:\n    \"\"\"The main entry point for the plan example.\"\"\"\n    toolkit = Toolkit()\n    toolkit.register_tool_function(execute_shell_command)\n    toolkit.register_tool_function(execute_python_code)\n    toolkit.register_tool_function(write_text_file)\n    toolkit.register_tool_function(insert_text_file)\n    toolkit.register_tool_function(view_text_file)\n\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"\"\"You're a helpful assistant named Friday.\n\n# Target\nYour target is to finish the given task with careful planning.\n\n# Note\n- You can equip yourself with plan related tools to help you plan and execute the given task.\n- The resouces from search engines are not always correct, you should collect information from multiple sources and give the final answer after careful consideration.\n\"\"\",  # noqa\n        model=DashScopeChatModel(\n            model_name=\"qwen3-max-preview\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n        enable_meta_tool=True,\n        plan_notebook=PlanNotebook(),\n    )\n    user = UserAgent(name=\"user\")\n\n    msg = Msg(\n        \"user\",\n        \"Review the recent changes in AgentScope GitHub repository \"\n        \"over the past month.\",\n        \"user\",\n    )\n    while True:\n        msg = await agent(msg)\n        msg = await user(msg)\n        if msg.get_text_content() == \"exit\":\n            break\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/plan/main_manual_plan.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Manual specification plan example.\"\"\"\nimport asyncio\nimport os\n\nfrom agentscope.agent import ReActAgent, UserAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.plan import PlanNotebook, SubTask\nfrom agentscope.tool import (\n    Toolkit,\n    execute_shell_command,\n    execute_python_code,\n    write_text_file,\n    insert_text_file,\n    view_text_file,\n)\n\nplan_notebook = PlanNotebook()\n\n\nasync def main() -> None:\n    \"\"\"The main entry point for the manual plan example.\"\"\"\n\n    # Create the plan manually\n    await plan_notebook.create_plan(\n        name=\"Comprehensive Report on AgentScope\",\n        description=\"Study the code of AgentScope and write a comprehensive \"\n        \"report about this framework.\",\n        expected_outcome=\"A markdown format report summarizing the features, \"\n        \"architecture, advantages/disadvantages, and \"\n        \"potential improvements of AgentScope.\",\n        subtasks=[\n            SubTask(\n                name=\"Clone the repository\",\n                description=\"Clone the AgentScope GitHub repository from \"\n                \"agentscope-ai/agentscope, and ensure it's the \"\n                \"latest version.\",\n                expected_outcome=\"A local copy of the AgentScope repository.\",\n            ),\n            SubTask(\n                name=\"View the documentation\",\n                description=\"View the documentation of AgentScope in the \"\n                \"repository.\",\n                expected_outcome=\"A comprehensive understanding of the \"\n                \"features and usage of AgentScope.\",\n            ),\n            SubTask(\n                name=\"Study the code\",\n                description=\"Study the code of AgentScope, focusing on the \"\n                \"core modules and their interactions.\",\n                expected_outcome=\"A deep understanding of the architecture \"\n                \"and implementation of AgentScope.\",\n            ),\n            SubTask(\n                name=\"Summarize the findings\",\n                description=\"Summarize the findings from the documentation \"\n                \"and code study, and write a comprehensive report \"\n                \"in markdown format.\",\n                expected_outcome=\"A markdown format report\",\n            ),\n        ],\n    )\n\n    # Add basic tools\n    toolkit = Toolkit()\n    toolkit.register_tool_function(execute_shell_command)\n    toolkit.register_tool_function(execute_python_code)\n    toolkit.register_tool_function(write_text_file)\n    toolkit.register_tool_function(insert_text_file)\n    toolkit.register_tool_function(view_text_file)\n\n    # Create the agent\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"You're a helpful assistant named Friday. Your target is \"\n        \"to finish the given task with careful planning.\",\n        model=DashScopeChatModel(\n            model_name=\"qwen3-max-preview\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n        plan_notebook=plan_notebook,\n    )\n    user = UserAgent(name=\"user\")\n\n    msg = Msg(\n        \"user\",\n        \"Now start to finish the task by the given plan\",\n        \"user\",\n    )\n    while True:\n        msg = await agent(msg)\n        msg = await user(msg)\n        if msg.get_text_content() == \"exit\":\n            break\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/rag/README.md",
    "content": "# RAG in AgentScope\n\nThis example includes three scripts to demonstrate how to use Retrieval-Augmented Generation (RAG) in AgentScope:\n\n- the basic usage of RAG module in AgentScope in ``basic_usage.py``,\n- a simple agentic use case of RAG in ``agentic_usage.py``, and\n- integrate RAG into ``ReActAgent`` class by retrieving input message(s) at the beginning of each reply in ``react_agent_integration.py``.\n- build multimodal RAG in ``multimodal_rag.py``.\n\n> The agentic usage and static integration has their own advantages and limitations.\n>  - The agentic usage requires more powerful LLMs to manage the retrieval process, but it's more flexible and the agent can adjust the retrieval strategy dynamically\n>  - The static integration is more straightforward and easier to implement, but it's less flexible and the input message maybe not specific enough, leading to less relevant retrieval results.\n\n> Note: The example is built with DashScope chat model. If you want to change the model in this example, don't forget\n> to change the formatter at the same time! The corresponding relationship between built-in models and formatters are\n> list in [our tutorial](https://doc.agentscope.io/tutorial/task_prompt.html#id1)\n\n## Quick Start\n\nInstall the latest agentscope library from PyPI or source, then run the following command to run the example:\n\n- the basic usage:\n```bash\npython basic_usage.py\n```\n\n- the agentic usage:\n```bash\npython agentic_usage.py\n```\n\n- the static integration:\n```bash\npython react_agent_integration.py\n```\n\n- the multimodal RAG:\n```bash\npython multimodal_rag.py\n```\n"
  },
  {
    "path": "examples/functionality/rag/agentic_usage.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The agentic usage example for RAG in AgentScope, where the agent is\nequipped with RAG tools to answer questions based on a knowledge base.\n\nThe example is more challenging for the agent, requiring the agent to\nadjust the retrieval parameters to get relevant results.\n\"\"\"\nimport asyncio\nimport os\n\nfrom agentscope.agent import ReActAgent, UserAgent\nfrom agentscope.embedding import DashScopeTextEmbedding\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.rag import SimpleKnowledge, QdrantStore, TextReader\nfrom agentscope.tool import Toolkit\n\n# Create a knowledge base instance\nknowledge = SimpleKnowledge(\n    embedding_store=QdrantStore(\n        location=\":memory:\",\n        collection_name=\"test_collection\",\n        dimensions=1024,  # The dimension of the embedding vectors\n    ),\n    embedding_model=DashScopeTextEmbedding(\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        model_name=\"text-embedding-v4\",\n    ),\n)\n\n\nasync def main() -> None:\n    \"\"\"The main entry of the agent usage example for RAG in AgentScope.\"\"\"\n\n    # Store some things into the knowledge base for demonstration\n    # In practice, the VDB store would be pre-filled with relevant data\n    reader = TextReader(chunk_size=1024, split_by=\"sentence\")\n    documents = await reader(\n        text=(\n            # Fake personal profile for demonstration\n            \"I'm John Doe, 28 years old. My best friend is James \"\n            \"Smith. I live in San Francisco. I work at OpenAI as a \"\n            \"software engineer. I love hiking and photography. \"\n            \"My father is Michael Doe, a doctor. I'm very proud of him. \"\n            \"My mother is Sarah Doe, a teacher. She is very kind and \"\n            \"always helps me with my studies.\\n\"\n            \"I'm now a PhD student at Stanford University, majoring in \"\n            \"Computer Science. My advisor is Prof. Jane Williams, who is \"\n            \"a leading expert in artificial intelligence. I have published \"\n            \"several papers in top conferences, such as NeurIPS and ICML. \"\n        ),\n    )\n    await knowledge.add_documents(documents)\n\n    # Create a toolkit and register the RAG tool function\n    toolkit = Toolkit()\n    toolkit.register_tool_function(\n        knowledge.retrieve_knowledge,\n        func_description=(  # Provide a clear description for the tool\n            \"Retrieve relevant documents from the knowledge base, which is \"\n            \"relevant to John Doe's profile. Note the `query` parameter is \"\n            \"very important for the retrieval quality, and you can try many \"\n            \"different queries to get the best results. Adjust the `limit` \"\n            \"and `score_threshold` parameters to get more or fewer results.\"\n        ),\n    )\n\n    # Create an agent and a user\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=(\n            \"You're a helpful assistant named Friday. \"\n            \"You're equipped with a 'retrieve_knowledge' tool to help you \"\n            \"know about the user named John Doe. \"\n            \"NOTE to adjust the `score_threshold` parameters when you cannot \"\n            \"get relevant results. \"\n        ),\n        toolkit=toolkit,\n        model=DashScopeChatModel(\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            model_name=\"qwen3-max-preview\",\n        ),\n        formatter=DashScopeChatFormatter(),\n    )\n    user = UserAgent(name=\"User\")\n\n    # A simple conversation loop beginning with a preset question\n    msg = Msg(\n        \"user\",\n        \"I'm John Doe. Do you know my father?\",\n        \"user\",\n    )\n    while True:\n        msg = await agent(msg)\n        msg = await user(msg)\n        if msg.get_text_content() == \"exit\":\n            break\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/rag/basic_usage.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The main entry point of the RAG example.\"\"\"\nimport asyncio\nimport os\n\nfrom agentscope.embedding import DashScopeTextEmbedding\nfrom agentscope.rag import (\n    TextReader,\n    PDFReader,\n    QdrantStore,\n    SimpleKnowledge,\n)\n\n\nasync def main() -> None:\n    \"\"\"The main entry point of the RAG example.\"\"\"\n\n    # Create readers with chunking arguments\n    reader = TextReader(chunk_size=1024)\n    pdf_reader = PDFReader(chunk_size=1024, split_by=\"sentence\")\n\n    # Read documents\n    documents = await reader(\n        text=\"I'm Tony Stank, my password is 123456. My best friend is James \"\n        \"Rhodes.\",\n    )\n\n    # Read a sample PDF file\n    pdf_path = os.path.join(\n        os.path.abspath(os.path.dirname(__file__)),\n        \"example.pdf\",\n    )\n    pdf_documents = await pdf_reader(pdf_path=pdf_path)\n\n    # Create a knowledge base with Qdrant as the embedding store and\n    # DashScope as the embedding model\n    knowledge = SimpleKnowledge(\n        embedding_store=QdrantStore(\n            location=\":memory:\",\n            collection_name=\"test_collection\",\n            dimensions=1024,  # The dimension of the embedding vectors\n        ),\n        embedding_model=DashScopeTextEmbedding(\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            model_name=\"text-embedding-v4\",\n        ),\n    )\n\n    # Insert documents into the knowledge base\n    await knowledge.add_documents(documents + pdf_documents)\n\n    # Retrieve relevant documents based on a given query\n    docs = await knowledge.retrieve(\n        query=\"What is Tony Stank's password?\",\n        limit=3,\n        score_threshold=0.7,\n    )\n    print(\"Q1: What is Tony Stank's password?\")\n    for doc in docs:\n        print(\n            f\"Document ID: {doc.id}, Score: {doc.score}, \"\n            f\"Content: {doc.metadata.content['text']}\",\n        )\n\n    # Retrieve documents from the PDF file based on a query\n    docs = await knowledge.retrieve(\n        query=\"climate change\",\n        limit=3,\n        score_threshold=0.2,\n    )\n    print(\"\\n\\nQ2: climate change\")\n    for doc in docs:\n        print(\n            f\"Document ID: {doc.id}, Score: {doc.score}, \"\n            f\"Content: {repr(doc.metadata.content['text'])}\",\n        )\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/rag/multimodal_rag.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The example of how to use multimodal RAG in AgentScope\"\"\"\nimport asyncio\nimport json\nimport os\n\nfrom matplotlib import pyplot as plt\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.embedding import DashScopeMultiModalEmbedding\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.rag import ImageReader, SimpleKnowledge, QdrantStore\n\n\npath_image = \"./example.png\"\nplt.figure(figsize=(8, 3))\nplt.text(0.5, 0.5, \"My name is Ming Li\", ha=\"center\", va=\"center\", fontsize=30)\nplt.axis(\"off\")\nplt.savefig(path_image, bbox_inches=\"tight\", pad_inches=0.1)\nplt.close()\n\n\nasync def example_multimodal_rag() -> None:\n    \"\"\"Example for multimodal RAG\"\"\"\n    # Reading the image and converting it to documents\n    reader = ImageReader()\n    docs = await reader(image_url=path_image)\n\n    # Create a knowledge base and add documents\n    knowledge = SimpleKnowledge(\n        embedding_model=DashScopeMultiModalEmbedding(\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            model_name=\"multimodal-embedding-v1\",\n            dimensions=1024,\n        ),\n        embedding_store=QdrantStore(\n            location=\":memory:\",\n            collection_name=\"test_collection\",\n            dimensions=1024,\n        ),\n    )\n\n    await knowledge.add_documents(docs)\n\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"You're a helpful assistant named Friday.\",\n        model=DashScopeChatModel(\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            model_name=\"qwen3-vl-plus\",\n        ),\n        formatter=DashScopeChatFormatter(),\n        knowledge=knowledge,\n    )\n\n    await agent(\n        Msg(\n            \"user\",\n            \"Do you know my name?\",\n            \"user\",\n        ),\n    )\n\n    # Let's see if the agent has stored the retrieved document in its memory\n    print(\"\\nThe retrieved document stored in the agent's memory:\")\n    content = (await agent.memory.get_memory())[-4].content\n    print(json.dumps(content, indent=2, ensure_ascii=False))\n\n\nasyncio.run(example_multimodal_rag())\n"
  },
  {
    "path": "examples/functionality/rag/react_agent_integration.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The example of integrating ReAct agent with RAG.\"\"\"\nimport asyncio\nimport os\n\nfrom agentscope.agent import ReActAgent, UserAgent\nfrom agentscope.embedding import DashScopeTextEmbedding\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.rag import SimpleKnowledge, QdrantStore, TextReader\n\n\nasync def main() -> None:\n    \"\"\"The main entry point for the ReAct agent with RAG example.\"\"\"\n\n    # Create an in-memory knowledge base instance\n    print(\"Creating the knowledge base...\")\n    knowledge = SimpleKnowledge(\n        embedding_store=QdrantStore(\n            location=\":memory:\",\n            collection_name=\"test_collection\",\n            dimensions=1024,  # The dimension of the embedding vectors\n        ),\n        embedding_model=DashScopeTextEmbedding(\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            model_name=\"text-embedding-v4\",\n        ),\n    )\n\n    # Insert some documents into the knowledge base\n    # This could be done offline and only once\n    print(\"Inserting documents into the knowledge base...\")\n    reader = TextReader(chunk_size=100, split_by=\"char\")\n    documents = await reader(\n        # Fake personal profile for demonstration\n        \"I'm John Doe, 28 years old. My best friend is James \"\n        \"Smith. I live in San Francisco. I work at OpenAI as a \"\n        \"software engineer. I love hiking and photography. \"\n        \"My father is Michael Doe, a doctor. I'm very proud of him. \"\n        \"My mother is Sarah Doe, a teacher. She is very kind and \"\n        \"always helps me with my studies.\\n\"\n        \"I'm now a PhD student at Stanford University, majoring in \"\n        \"Computer Science. My advisor is Prof. Jane Williams, who is \"\n        \"a leading expert in artificial intelligence. I have published \"\n        \"several papers in top conferences, such as NeurIPS and ICML. \",\n    )\n\n    print(\"Inserting documents into the knowledge base...\")\n    await knowledge.add_documents(documents)\n\n    # Integrate into the ReActAgent by the `knowledge` argument\n    print(\"Creating the agent...\")\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"You are a helpful assistant named Friday.\",\n        model=DashScopeChatModel(\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            model_name=\"qwen-max\",\n        ),\n        formatter=DashScopeChatFormatter(),\n        # Equip the agent with the knowledge base\n        knowledge=knowledge,\n        print_hint_msg=True,\n    )\n    user = UserAgent(name=\"user\")\n\n    # Start the conversation\n    print(\"Start the conversation...\")\n    msg = Msg(\"user\", \"Do you know who is my best friend?\", \"user\")\n    while True:\n        msg = await agent(msg)\n        msg = await user(msg)\n        if msg.get_text_content() == \"exit\":\n            break\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/session_with_sqlite/README.md",
    "content": "# Session Management with Sqlite DB\n\nThis example demonstrates how to implement session management with a database backend. We use SQLite for simplicity,\nbut the approach can be adapted for other databases.\n\nSpecifically, we implement a ``SqliteSession`` class that persists and retrieves session data from a SQLite table.\nThe table schema includes fields for session ID, session data (stored as JSON), and timestamps for creation and last\nupdate.\n\nWe will create a simple agent and chat with it, then store the session data in the SQLite database. Then in the\n``test_load_session`` function, we will load the session data from the database and continue the chat.\n\n## Quick Start\n\nInstall agentscope from Pypi or source code.\n\n```bash\npip install agentscope\n```\n\nRun the example by the following command\n\n```bash\npython main.py\n```"
  },
  {
    "path": "examples/functionality/session_with_sqlite/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The main entry point for the session with SQLite example.\"\"\"\nimport asyncio\nimport os\n\nfrom sqlite_session import SqliteSession\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\n\nSQLITE_PATH = \"./session.db\"\n\n\nasync def main(username: str, query: str) -> None:\n    \"\"\"Create an agent, load from session, chat with it, and save its state\n    to SQLite.\n\n    Args:\n        username (`str`):\n            The username to identify the session.\n        query (`str`):\n            The user input query.\n    \"\"\"\n\n    agent = ReActAgent(\n        name=\"friday\",\n        sys_prompt=\"You are a helpful assistant named Friday.\",\n        model=DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        ),\n        formatter=DashScopeChatFormatter(),\n    )\n\n    # Create the SQLite session\n    session = SqliteSession(SQLITE_PATH)\n\n    # Load the agent state by the given key \"friday_of_user\"\n    # The load_session_state supports multiple state modules\n    await session.load_session_state(\n        session_id=username,\n        friday_of_user=agent,\n    )\n\n    # Chat with it to generate some state\n    await agent(\n        Msg(\"user\", query, \"user\"),\n    )\n\n    # Save the agent state by the given key \"friday_of_user\"\n    # Also support multiple state modules (e.g. multiple agents)\n    await session.save_session_state(\n        session_id=username,\n        friday_of_user=agent,\n    )\n\n\nprint(\"User named Alice chats with the agent ...\")\nasyncio.run(main(\"alice\", \"What's the capital of America?\"))\n\nprint(\"User named Bob chats with the agent ...\")\nasyncio.run(main(\"bob\", \"What's the capital of China?\"))\n\nprint(\n    \"\\nNow, let's recover the session for Alice and ask about what the user \"\n    \"asked before.\",\n)\nasyncio.run(\n    main(\n        \"alice\",\n        \"What did I ask you before, what's your answer and how many \"\n        \"questions have I asked you?\",\n    ),\n)\n"
  },
  {
    "path": "examples/functionality/session_with_sqlite/sqlite_session.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The SQLite session class.\"\"\"\nimport json\nimport os\nimport sqlite3\n\nfrom agentscope import logger\nfrom agentscope.module import StateModule\nfrom agentscope.session import SessionBase\n\n\nclass SqliteSession(SessionBase):\n    \"\"\"A session that uses SQLite for storage.\"\"\"\n\n    def __init__(\n        self,\n        sqlite_path: str,\n    ) -> None:\n        \"\"\"Initialize the session.\n\n        Args:\n            sqlite_path (`str`):\n                The path to the SQLite database file.\n        \"\"\"\n        self.sqlite_path = sqlite_path\n\n    async def save_session_state(\n        self,\n        session_id: str,\n        **state_modules_mapping: StateModule,\n    ) -> None:\n        \"\"\"Save the session state to the SQLite database.\"\"\"\n        with sqlite3.connect(self.sqlite_path) as conn:\n            cursor = conn.cursor()\n            # Prepare the session data as a dictionary\n            session_data = {\n                name: module.state_dict()\n                for name, module in state_modules_mapping.items()\n            }\n\n            json_data = json.dumps(session_data)\n\n            cursor.execute(\n                \"\"\"\n                CREATE TABLE IF NOT EXISTS as_session (\n                    session_id TEXT,\n                    session_data JSON,\n                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n                    PRIMARY KEY (session_id)\n                )\n                \"\"\",\n            )\n\n            # Insert or replace the session data\n            cursor.execute(\n                \"\"\"\n                INSERT INTO as_session (session_id, session_data, updated_at)\n                VALUES (?, json(?), CURRENT_TIMESTAMP)\n                ON CONFLICT(session_id) DO UPDATE SET\n                    session_data = excluded.session_data,\n                    updated_at = excluded.updated_at\n                \"\"\",\n                (session_id, json_data),\n            )\n            conn.commit()\n            cursor.close()\n\n    async def load_session_state(\n        self,\n        session_id: str,\n        allow_not_exist: bool = True,\n        **state_modules_mapping: StateModule,\n    ) -> None:\n        \"\"\"Get the state dictionary from the SQLite database.\n\n        Args:\n            session_id (`str`):\n                The session id.\n            allow_not_exist (`bool`, defaults to `True`):\n                Whether to allow the session to not exist. If `False`, raises\n                an error if the session does not exist.\n            **state_modules_mapping (`list[StateModule]`):\n                The list of state modules to be loaded.\n        \"\"\"\n        if not os.path.exists(self.sqlite_path):\n            if allow_not_exist:\n                logger.info(\n                    \"SQLite database %s does not exist. \"\n                    \"Skipping load for session_id %s.\",\n                    self.sqlite_path,\n                    session_id,\n                )\n                return\n            raise ValueError(\n                \"Failed to load session state because the SQLite database \"\n                f\"file '{self.sqlite_path}' does not exist.\",\n            )\n\n        with sqlite3.connect(self.sqlite_path) as conn:\n            cursor = conn.cursor()\n\n            try:\n                # If the table does not exist, return\n                cursor.execute(\n                    \"\"\"\n                    SELECT name FROM sqlite_master WHERE type='table' AND\n                        name='as_session';\n                    \"\"\",\n                )\n                if cursor.fetchone() is None:\n                    if allow_not_exist:\n                        logger.info(\n                            \"Session table does not exist in database %s. \"\n                            \"Skipping load for session_id %s.\",\n                            self.sqlite_path,\n                            session_id,\n                        )\n                        return\n\n                    raise ValueError(\n                        \"Failed to load session state because the session \"\n                        \"table 'as_session' does not exist in database \"\n                        f\"{self.sqlite_path}.\",\n                    )\n\n                # Query the session data\n                cursor.execute(\n                    \"SELECT session_data FROM as_session WHERE session_id = ?\",\n                    (session_id,),\n                )\n                row = cursor.fetchone()\n\n                if row is None:\n                    if allow_not_exist:\n                        logger.info(\n                            \"Session_id %s does not exist in database %s. \"\n                            \"Skip loading.\",\n                            session_id,\n                            self.sqlite_path,\n                        )\n                        return\n\n                    raise ValueError(\n                        f\"Failed to load session state for session_id \"\n                        f\"{session_id} does not exist.\",\n                    )\n\n                session_data = json.loads(row[0])\n\n                for name, module in state_modules_mapping.items():\n                    if name in session_data:\n                        module.load_state_dict(session_data[name])\n                    else:\n                        raise ValueError(\n                            f\"State module '{name}' not found in session \"\n                            \"data.\",\n                        )\n                logger.info(\n                    \"Load session state for session_id %s from \"\n                    \"database %s successfully.\",\n                    session_id,\n                    self.sqlite_path,\n                )\n\n            finally:\n                cursor.close()\n"
  },
  {
    "path": "examples/functionality/short_term_memory/memory_compression/README.md",
    "content": "# MemoryWithCompress\n\n- [ ] TODO: The memory module with compression will be added to the agentscope library in the future.\n\n## Overview\n\nMemoryWithCompress is a memory management system designed for AgentScope's `ReActAgent`. It automatically compresses conversation history when the memory size exceeds a specified token limit, using a Large Language Model (LLM) to create concise summaries that preserve key information. This allows agents to maintain context over long conversations while staying within token constraints.\n\nThe system maintains two separate storage mechanisms:\n- **`chat_history_storage`**: Stores the complete, unmodified conversation history (uses `MessageStorageBase` interface)\n- **`memory_storage`**: Stores messages that may be compressed when token limits are exceeded (uses `MessageStorageBase` interface)\n\nBoth storage mechanisms are abstracted through the `MessageStorageBase` interface, allowing for flexible storage backends. By default, `InMemoryMessageStorage` is used for both.\n\n## Core Features\n\n### Automatic Memory Compression\n- **Token-based Triggering**: Automatically compresses memory when the total token count exceeds `max_token`\n- **LLM-Powered Summarization**: Uses an LLM to intelligently compress conversation history while preserving essential information\n- **Structured Output**: Uses Pydantic schemas to ensure consistent compression format\n\n### Dual Storage System\n- **Complete History**: Maintains original, unmodified messages in `_chat_history` for reference\n- **Compressed Memory**: Stores potentially compressed messages in `_memory` for efficient context management\n\n### Flexible Memory Management\n- **Filtering Support**: Provides `filter_func` parameter for custom memory filtering\n- **Recent N Retrieval**: Supports retrieving only the most recent N messages\n- **State Persistence**: Includes `state_dict()` and `load_state_dict()` methods for saving and loading memory state\n- **Storage Abstraction**: Uses `MessageStorageBase` interface for flexible storage backends\n- **Compression Triggers**: Supports both token-based and custom trigger functions for compression\n- **Compression Timing Control**: Configurable compression on add (`compression_on_add`) and get (`compression_on_get`) operations\n\n## File Structure\n\n```\nmemory_with_compression/\n├── README.md                   # This documentation file\n├── main.py                     # Example demonstrating MemoryWithCompress usage\n├── _memory_with_compress.py    # Core MemoryWithCompress implementation\n├── _memory_storage.py          # Storage abstraction layer (MessageStorageBase, InMemoryMessageStorage)\n├── _mc_utils.py                # Utility functions (formatting, token counting, compression schema)\n\n```\n\n## Prerequisites\n\n### Clone the AgentScope Repository\nThis example depends on AgentScope. Please clone the full repository to your local machine.\n\n### Install Dependencies\n**Recommended**: Python 3.10+\n\nInstall the required dependencies:\n```bash\npip install agentscope\n```\n\n### API Keys\nThis example uses DashScope APIs by default. You need to set your API key as an environment variable:\n```bash\nexport DASHSCOPE_API_KEY='YOUR_API_KEY'\n```\n\nYou can easily switch to other models by modifying the configuration in `main.py`.\n\n## How It Works\n\n### 1. Memory Addition Flow\n1. **Message Input**: New messages are added via the async `add()` method\n2. **Dual Storage**: Messages are deep-copied and added to both `chat_history_storage` and `memory_storage`\n3. **Optional Compression on Add**: If `compression_on_add=True`, compression may be triggered immediately after adding messages\n\n### 2. Memory Retrieval and Compression Flow\nWhen `get_memory()` is called (if `compression_on_get=True`):\n1. **Token Counting**: The system calculates the total token count of all messages in `memory_storage`\n2. **Compression Check**:\n   - First checks if token count exceeds `max_token` (automatic compression)\n   - Then checks if `compression_trigger_func` returns `True` (custom trigger)\n3. **LLM Compression**: If compression is needed, all messages in `memory_storage` are sent to the LLM with a compression prompt\n4. **Structured Output**: The LLM returns a structured response containing the compressed summary\n5. **Memory Replacement**: The entire `memory_storage` is updated with the compressed message(s)\n6. **Filtering & Selection**: Optional filtering and recent_n selection are applied\n7. **Return**: The processed memory is returned\n\n### 3. Compression Process\nThe compression uses a structured output approach:\n- **Prompt**: Instructs the LLM to summarize conversation history while preserving key information\n- **Customizable Prompt**: Supports `customized_compression_prompt` parameter for custom prompt templates\n- **Schema**: Uses `MemoryCompressionSchema` (Pydantic model) to ensure consistent output format\n- **Output Format**: Returns a message with content wrapped in `<compressed_memory>` tags\n- **Async Support**: All compression operations are asynchronous\n\n## Usage Examples\n\n### Running the Example\nTo see `MemoryWithCompress` in action, run the example script:\n```bash\npython ./main.py\n```\n\n### Basic Initialization\nHere is a snippet from `main.py` showing how to set up the agent and memory:\n\n```python\nfrom agentscope.agent import ReActAgent\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.token import OpenAITokenCounter\nfrom agentscope.message import Msg\nfrom _memory_with_compress import MemoryWithCompress\n\n# 1. Create the model for agent and memory compression\nmodel = DashScopeChatModel(\n    api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n    model_name=\"qwen-max\",\n    stream=False,\n)\n\n# 2. Optional: Define a custom compression trigger function\nasync def trigger_compression(msgs: list[Msg]) -> bool:\n    # Trigger compression if the number of messages exceeds 2\n    # and the last message is from the assistant\n    return len(msgs) > 2 and msgs[-1].role == \"assistant\"\n\n# 3. Initialize MemoryWithCompress\nmemory_with_compress = MemoryWithCompress(\n    model=model,\n    formatter=DashScopeChatFormatter(),\n    max_token=3000,  # Compress when memory exceeds 3000 tokens\n    token_counter=OpenAITokenCounter(model_name=\"qwen-max\"),\n    compression_trigger_func=trigger_compression,  # Optional custom trigger\n    compression_on_add=False,  # Don't compress on add (default)\n    compression_on_get=True,   # Compress on get (default)\n)\n\n# 4. Initialize ReActAgent with the memory instance\nagent = ReActAgent(\n    name=\"Friday\",\n    sys_prompt=\"You are a helpful assistant named Friday.\",\n    model=model,\n    formatter=DashScopeChatFormatter(),\n    memory=memory_with_compress,\n)\n```\n\n### Custom Compression Function\nYou can provide a custom compression function:\n\n```python\nasync def custom_compress(messages: List[Msg]) -> List[Msg]:\n    # Your custom compression logic\n    # Must return a List[Msg], not a single Msg\n    compressed_content = \"...\"\n    return [Msg(\"assistant\", compressed_content, \"assistant\")]\n\nmemory_with_compress = MemoryWithCompress(\n    model=model,\n    formatter=formatter,\n    max_token=300,\n    compress_func=custom_compress,\n)\n```\n\n### Custom Storage Backend\nYou can provide custom storage backends by implementing the `MessageStorageBase` interface:\n\n```python\nfrom _memory_storage import MessageStorageBase\n\nclass CustomStorage(MessageStorageBase):\n    # Implement required methods: start, stop, health, add, delete, clear, get, replace, __aenter__, __aexit__\n    ...\n\nmemory_with_compress = MemoryWithCompress(\n    model=model,\n    formatter=formatter,\n    max_token=300,\n    chat_history_storage=CustomStorage(),\n    memory_storage=CustomStorage(),\n)\n```\n\n## API Reference\n\n### MemoryWithCompress Class\n\n#### `__init__(...)`\nInitializes the memory system. Key parameters include:\n\n- `model` (ChatModelBase): The LLM model to use for compression\n- `formatter` (FormatterBase): The formatter to use for formatting messages\n- `max_token` (int): The maximum token count for `memory_storage`. Default: 28000. Compression is triggered when exceeded\n- `chat_history_storage` (MessageStorageBase): Storage backend for complete chat history. Default: `InMemoryMessageStorage()`\n- `memory_storage` (MessageStorageBase): Storage backend for compressed memory. Default: `InMemoryMessageStorage()`\n- `token_counter` (Optional[TokenCounterBase]): The token counter for counting tokens. Default: None. If None, it will return the character count of the JSON string representation of messages (i.e., len(json.dumps(messages, ensure_ascii=False))).\n- `compress_func` (Callable[[List[Msg]], Awaitable[List[Msg]]] | None): Custom compression function. Must be async and return `List[Msg]`. If None, uses the default `_compress_memory` method\n- `compression_trigger_func` (Callable[[List[Msg]], Awaitable[bool]] | None): Optional function to trigger compression when token count is below `max_token`. Must be async and return `bool`. If None, compression only occurs when token count exceeds `max_token`\n- `compression_on_add` (bool): Whether to check and compress memory when adding messages. Default: False\n- `compression_on_get` (bool): Whether to check and compress memory when getting messages. Default: True\n- `customized_compression_prompt` (str | None): Optional customized compression prompt template. Should include placeholders: `{max_token}`, `{messages_list_json}`, `{schema_json}`. Default: None (uses default template)\n\n#### Main Methods\n\n**`async add(msgs: Union[Sequence[Msg], Msg, None], compress_func=None, compression_trigger_func=None)`**\n- Adds new messages to both `chat_history_storage` and `memory_storage`\n- Messages are deep-copied to avoid modifying originals\n- Raises `TypeError` if non-Msg objects are provided\n- Parameters:\n  - `msgs`: Messages to be added\n  - `compress_func` (Optional): Override the instance-level compression function for this call\n  - `compression_trigger_func` (Optional): Override the instance-level trigger function for this call\n- If `compression_on_add=True`, may trigger compression after adding\n\n**`async direct_update_memory(msgs: Union[Sequence[Msg], Msg, None])`**\n- Directly updates the `memory_storage` with new messages (does not update `chat_history_storage`)\n- Useful for replacing memory content directly\n\n**`async get_memory(recent_n=None, filter_func=None, compress_func=None, compression_trigger_func=None)`**\n- Retrieves memory content, automatically compressing if token limit is exceeded (if `compression_on_get=True`)\n- Parameters:\n  - `recent_n` (Optional[int]): Return only the most recent N messages\n  - `filter_func` (Optional[Callable[[int, Msg], bool]]): Custom filter function that takes (index, message) and returns bool\n  - `compress_func` (Optional): Override the instance-level compression function for this call\n  - `compression_trigger_func` (Optional): Override the instance-level trigger function for this call\n- Returns: `list[Msg]` - The memory content (potentially compressed)\n\n**`async delete(indices: Union[Iterable[int], int])`**\n- Deletes memory fragments from `memory_storage` (note: does not delete from `chat_history_storage`)\n- Indices can be a single int or an iterable of ints\n\n**`async size() -> int`**\n- Returns the number of messages in `chat_history_storage`\n\n**`async clear()`**\n- Clears all memory from both `chat_history_storage` and `memory_storage`\n\n**`state_dict() -> dict`**\n- Returns a dictionary containing the serialized state:\n  - `chat_history_storage`: List of message dictionaries from chat history\n  - `memory_storage`: List of message dictionaries from memory\n  - `max_token`: The max_token setting\n- Note: This method handles async operations internally, so it can be called from both sync and async contexts\n\n**`load_state_dict(state_dict: dict, strict: bool = True)`**\n- Loads memory state from a dictionary\n- Restores `chat_history_storage`, `memory_storage`, and `max_token` settings\n- Note: This method handles async operations internally, so it can be called from both sync and async contexts\n\n**`async retrieve(*args, **kwargs)`**\n- Not implemented. Use `get_memory()` instead.\n- Raises `NotImplementedError`\n\n## Internal Methods\n\n**`async _compress_memory(msgs: List[Msg]) -> List[Msg]`**\n- Internal method that compresses messages using the LLM\n- Uses structured output with `MemoryCompressionSchema`\n- Returns a `List[Msg]` containing the compressed summary (typically a single message)\n- Supports both streaming and non-streaming models\n\n**`async _check_length_and_compress(compress_func=None) -> bool`**\n- Checks if memory token count exceeds `max_token` and compresses if needed\n- Returns `True` if compression was triggered, `False` otherwise\n\n**`async check_and_compress(compress_func=None, compression_trigger_func=None, memory=None) -> tuple[bool, List[Msg]]`**\n- Checks if compression is needed based on `compression_trigger_func`\n- Returns a tuple: (was_compressed: bool, compressed_memory: List[Msg])\n- If `memory` is provided, checks that instead of `memory_storage`\n\n## Utility Functions\n\nThe `_mc_utils.py` module provides:\n\n- **`format_msgs(msgs)`**: Formats a list of `Msg` objects into a list of dictionaries\n- **`async count_words(token_counter, text)`**: Counts tokens in text (supports both string and list[dict] formats). Must be awaited.\n- **`MemoryCompressionSchema`**: Pydantic model for structured compression output\n- **`DEFAULT_COMPRESSION_PROMPT_TEMPLATE`**: Default prompt template for compression (includes placeholders: `{max_token}`, `{messages_list_json}`, `{schema_json}`)\n\n## Storage Abstraction\n\nThe `_memory_storage.py` module provides:\n\n- **`MessageStorageBase`**: Abstract base class for message storage backends\n  - Required async methods: `start()`, `stop()`, `health()`, `add()`, `delete()`, `clear()`, `get()`, `replace()`, `__aenter__()`, `__aexit__()`\n- **`InMemoryMessageStorage`**: Default in-memory implementation\n  - Stores messages in a simple list\n  - Suitable for most use cases\n\n## Best Practices\n\n- **Token Limit Selection**: Choose `max_token` based on your model's context window and typical conversation length\n- **Compression Timing**:\n  - Set `compression_on_get=True` (default) for compression during retrieval\n  - Set `compression_on_add=False` (default) to avoid compression during add operations, as it may not complete before `get_memory()` is called\n- **Async Operations**: All main methods are async, so use `await` when calling them\n- **State Persistence**: Use `state_dict()` and `load_state_dict()` to save/restore conversation state between sessions\n- **Custom Compression**: For domain-specific compression needs, implement a custom `compress_func` (must be async and return `List[Msg]`)\n- **Compression Triggers**: Use `compression_trigger_func` for custom compression logic beyond token limits (e.g., compress after N messages, compress on specific conditions)\n- **Storage Backends**: Implement custom `MessageStorageBase` subclasses for persistent storage (e.g., database, file system)\n\n## Troubleshooting\n\n- **Compression Not Triggering**:\n  - Check that `compression_on_get=True` if you expect compression during retrieval\n  - Verify that `max_token` is set appropriately\n  - Ensure `get_memory()` is being called (and awaited)\n  - If using `compression_trigger_func`, verify it returns `True` when compression should occur\n- **Structured Output Errors**: Ensure your model supports structured output (e.g., DashScope models with `structured_model` parameter)\n- **Token Counting Issues**: Verify that your `token_counter` is compatible with your model and correctly configured\n- **Async/Await Errors**: Remember that most methods are async - use `await` when calling them\n- **Storage Issues**: If using custom storage backends, ensure all required methods are properly implemented and async\n\n## Reference\n\n- [AgentScope Documentation](https://github.com/agentscope-ai/agentscope)\n- [Pydantic Documentation](https://docs.pydantic.dev/)\n"
  },
  {
    "path": "examples/functionality/short_term_memory/memory_compression/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The main entry point of the MemoryWithCompress example.\"\"\"\nimport asyncio\nimport os\nfrom agentscope.agent import ReActAgent, UserAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.token import CharTokenCounter\n\n\nasync def main() -> None:\n    \"\"\"The main entry point of the MemoryWithCompress example.\"\"\"\n\n    # Create model for agent and memory compression\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"You are a helpful assistant named Friday.\",\n        model=DashScopeChatModel(\n            api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n            model_name=\"qwen3-max\",\n        ),\n        formatter=DashScopeChatFormatter(),\n        compression_config=ReActAgent.CompressionConfig(\n            enable=True,\n            agent_token_counter=CharTokenCounter(),\n            # We set a small trigger threshold for demonstration purposes.\n            trigger_threshold=1000,\n            keep_recent=3,\n        ),\n    )\n    user = UserAgent(\"User\")\n\n    # Simulate a conversation to trigger memory compression\n    msg = None\n    while True:\n        msg = await user(msg)\n        if msg.get_text_content() == \"exit\":\n            break\n        msg = await agent(msg)\n\n    print(\"The memory of the agent:\")\n    for msg in await agent.memory.get_memory():\n        print(msg.to_dict(), end=\"\\n\")\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/short_term_memory/reme/README.md",
    "content": "# ReMe Short-Term Memory in AgentScope\n\nThis example demonstrates how to\n\n- use ReMeShortTermMemory to provide automatic working memory management for AgentScope agents,\n- handle long conversation contexts with intelligent summarization and compaction,\n- integrate short-term memory with ReAct agents for efficient tool usage and context management, and\n- configure DashScope models for memory operations.\n\n## Why Short-Term Memory?\n\n### The Challenge: From Prompt Engineering to Context Engineering\n\nAs AI agents evolved from simple chatbots to sophisticated autonomous systems, the focus shifted from \"prompt engineering\" to \"context engineering\". While prompt engineering focused on crafting effective instructions for language models, context engineering addresses a more fundamental challenge: **managing the ever-growing conversation and tool execution history that agents accumulate**.\n\n### The Core Problem: Context Explosion\n\nAgentic systems work by binding LLMs with tools and running them in a loop where the agent decides which tools to call and feeds results back into the message history. This creates a snowball effect:\n\n- **Rapid Growth**: A seemingly simple task can trigger 50+ tool calls, with production agents often running hundreds of conversation turns\n- **Large Outputs**: Each tool call can return substantial text, consuming massive amounts of tokens\n- **Memory Pressure**: The context window quickly fills up as messages and tool results accumulate chronologically\n\n### The Consequence: Context Rot\n\nWhen context grows too large, model performance degrades significantly—a phenomenon known as **\"context rot\"**:\n\n- **Repetitive Responses**: The model starts generating redundant or circular answers\n- **Slower Reasoning**: Inference becomes noticeably slower as context length increases\n- **Quality Degradation**: Overall response quality and coherence decline\n- **Lost Focus**: The model struggles to identify relevant information in the bloated context\n\n### The Fundamental Paradox\n\nAgents face a critical tension:\n\n- **Need Rich Context**: Agents require comprehensive historical information to make informed decisions\n- **Suffer from Large Context**: Excessive context causes performance degradation and inefficiency\n\n**Context management aims to keep \"just enough\" information in the window**—sufficient for effective decision-making while leaving room for retrieval and expansion, without overwhelming the model.\n\n### Why Short-Term Memory Management Matters\n\nEffective short-term memory management is essential for:\n\n1. **Maintaining Performance**: Keeping context within optimal size prevents quality degradation\n2. **Enabling Long-Running Tasks**: Agents can handle complex, multi-step workflows without hitting context limits\n3. **Cost Efficiency**: Reducing token usage directly lowers API costs\n4. **Preserving Reasoning Quality**: Clean, focused context helps models maintain coherent reasoning chains\n5. **Scalability**: Proper memory management allows agents to scale to production workloads\n\n### The Solution: Intelligent Context Management\n\nReMeShortTermMemory implements proven context management strategies:\n\n- **Context Offloading**: Moving large tool outputs to external storage while keeping references\n- **Context Reduction**: Compacting tool results into minimal representations and summarizing when necessary\n- **Smart Retention**: Keeping recent messages intact to maintain continuity and provide usage examples\n- **Automatic Triggering**: Monitoring token usage and applying strategies before performance degrades\n\nBy implementing these strategies, ReMeShortTermMemory enables agents to handle arbitrarily long conversations and complex tasks while maintaining optimal performance throughout.\n\n## Prerequisites\n\n- Python 3.10 or higher\n- DashScope API key from Alibaba Cloud\n\n\n## QuickStart\n\nInstall agentscope and ensure you have a valid DashScope API key in your environment variables.\n\n> Note: The example is built with DashScope chat model. If you want to use OpenAI models instead,\n> modify the model initialization in the example code accordingly.\n\n```bash\n# Install agentscope from source\ncd {PATH_TO_AGENTSCOPE}\npip install -e .\n# Install dependencies\npip install reme-ai python-dotenv\n```\n\nSet up your API key:\n\n```bash\nexport DASHSCOPE_API_KEY='YOUR_API_KEY'\n```\n\nOr create a `.env` file:\n\n```bash\nDASHSCOPE_API_KEY=YOUR_API_KEY\n```\n\nRun the example:\n\n```bash\npython short_term_memory_example.py\n```\n\nThe example will:\n1. Initialize a ReMeShortTermMemory instance with DashScope models\n2. Demonstrate automatic memory compaction for long tool responses\n3. Show integration with ReActAgent for context-aware conversations\n4. Use grep and read_file tools to search and retrieve information from files\n\n## Key Features\n\n- **Automatic Memory Management**: Intelligent summarization and compaction of working memory to handle long contexts\n- **Tool Response Optimization**: Automatic truncation and summarization of large tool responses to stay within token limits\n- **Flexible Configuration**: Configurable thresholds for compaction ratio, token limits, and recent message retention\n- **ReAct Agent Integration**: Seamless integration with AgentScope's ReActAgent and tool system\n- **Async Operations**: Full async support for non-blocking memory operations\n\n## Basic Usage\n\nThis section provides a detailed walkthrough of the `short_term_memory_example.py` code, explaining how each component works together to create an agent with intelligent context management.\n\n### Configuration Parameters\n\n#### `ReMeShortTermMemory` Class Parameters\n\nThe `ReMeShortTermMemory` class accepts the following initialization parameters:\n\n- **`model`** (`DashScopeChatModel | OpenAIChatModel | None`): Language model for compression operations. Must be either `DashScopeChatModel` or `OpenAIChatModel`. This model is used for LLM-based compression when generating compact state snapshots. **Required**.\n\n- **`reme_config_path`** (`str | None`): Optional path to ReMe configuration file for custom settings. Use this to provide advanced ReMe configurations beyond the standard parameters. Default: `None`.\n\n- **`working_summary_mode`** (`str`): Strategy for working memory management. Controls how the memory system handles context overflow:\n  - `\"compact\"`: Only compact verbose tool messages by storing full content externally and keeping short previews in the active context.\n  - `\"compress\"`: Only apply LLM-based compression to generate compact state snapshots of conversation history.\n  - `\"auto\"`: First run compaction, then optionally run compression if the compaction ratio exceeds `compact_ratio_threshold`. This is the recommended mode for most use cases.\n\n  Default: `\"auto\"`.\n\n- **`compact_ratio_threshold`** (`float`): Threshold for compaction effectiveness in AUTO mode. If `(compacted_tokens / original_tokens) > compact_ratio_threshold`, compression is applied after compaction. This ensures compression only runs when compaction alone isn't sufficient. Valid range: 0.0 to 1.0. Default: `0.75`.\n\n- **`max_total_tokens`** (`int`): Maximum token count threshold before compression is triggered. This limit does **not** include `keep_recent_count` messages or system messages, which are always preserved. Should be set to 20%-50% of your model's context window size to leave room for new tool calls and responses. Default: `20000`.\n\n- **`max_tool_message_tokens`** (`int`): Maximum token count for individual tool messages before compaction. Tool messages exceeding this limit are stored externally in files, with only a short preview kept in the active context. This is the maximum tolerable length for a single tool response. Default: `2000`.\n\n- **`group_token_threshold`** (`int | None`): Maximum token count per compression group when splitting messages for LLM compression. When set to a positive integer, long message sequences are split into smaller batches for compression. If `None` or `0`, all messages are compressed in a single group. Use this to control the granularity of compression operations. Default: `None`.\n\n- **`keep_recent_count`** (`int`): Number of most recent messages to preserve without compression or compaction. These messages remain in full in the active context to maintain conversation continuity and provide usage examples for the agent. The example uses `1` for demonstration purposes; **in production, a value of `10` is recommended** to maintain better conversation flow. Default: `10`.\n\n- **`store_dir`** (`str`): Directory path for storing offloaded message content and compressed history files. This is where external files containing full tool responses and compressed message history are saved. The directory will be created automatically if it doesn't exist. Default: `\"inmemory\"`.\n\n- **`**kwargs`** (`Any`): Additional arguments passed to `ReMeApp` initialization. Use this to pass any extra configuration options supported by the underlying ReMe application.\n\n#### Parameter Relationships and Best Practices\n\n- **Token Budget Strategy**: Set `max_total_tokens` to 20%-50% of your model's context window. For example, if your model has a 128K context window, set `max_total_tokens` between 25,600 and 64,000 tokens.\n\n- **Compaction vs Compression**:\n  - Compaction is fast and lossless (full content is stored externally)\n  - Compression is slower but more aggressive (uses LLM to summarize)\n  - Use `\"auto\"` mode to benefit from both strategies\n\n- **Recent Message Retention**: Higher `keep_recent_count` values (e.g., 10) provide better context continuity but consume more tokens. Lower values (e.g., 1) are more aggressive but may lose important recent context.\n\n- **Tool Message Handling**: Adjust `max_tool_message_tokens` based on your typical tool response sizes. If your tools frequently return large outputs (e.g., file contents, API responses), consider a higher threshold or ensure compaction is enabled.\n\n### Code Flow Diagram\n\n```mermaid\nflowchart TD\n    A[Start: Load Environment] --> B[Create Toolkit]\n    B --> C[Register Tools: grep & read_file]\n    C --> D[Initialize LLM Model]\n    D --> E[Create ReMeShortTermMemory]\n    E --> F[Enter Async Context Manager]\n    F --> G[Add Initial Messages with Large Tool Response]\n    G --> H[Memory Auto-Compacts Large Content]\n    H --> I[Create ReActAgent with Memory]\n    I --> J[User Sends Query]\n    J --> K[Agent Uses Tools to Search/Read]\n    K --> L[Tool Responses Added to Memory]\n    L --> M{Memory Token Limit?}\n    M -->|Exceeded| N[Auto-Compact/Summarize]\n    M -->|OK| O[Agent Generates Response]\n    N --> O\n    O --> P[Return Response to User]\n    P --> Q[Exit Context Manager]\n    Q --> End[End]\n\n    style H fill:#e1f5ff\n    style N fill:#ffe1e1\n    style O fill:#e1ffe1\n```\n\n### Step-by-Step Code Walkthrough\n\nThe example demonstrates a complete workflow from tool registration to agent interaction. Here's a detailed breakdown:\n\n#### 1. Environment Setup and Imports\n\n```python\nimport asyncio\nimport os\nfrom dotenv import load_dotenv\n\nload_dotenv()\n```\n\nThe code starts by loading environment variables (including the DashScope API key) from a `.env` file.\n\n#### 2. Tool Registration\n\nThe example defines two custom tools that demonstrate how to integrate retrieval operations:\n\n**`grep` Tool**: Searches for patterns in files using regular expressions\n```python\nasync def grep(file_path: str, pattern: str, limit: str) -> ToolResponse:\n    \"\"\"A powerful search tool for finding patterns in files...\"\"\"\n    from reme_ai.retrieve.working import GrepOp\n\n    op = GrepOp()\n    await op.async_call(file_path=file_path, pattern=pattern, limit=limit)\n    return ToolResponse(\n        content=[TextBlock(type=\"text\", text=op.output)],\n    )\n```\n\n**`read_file` Tool**: Reads specific line ranges from files\n```python\nasync def read_file(file_path: str, offset: int, limit: int) -> ToolResponse:\n    \"\"\"Reads and returns the content of a specified file...\"\"\"\n    from reme_ai.retrieve.working import ReadFileOp\n\n    op = ReadFileOp()\n    await op.async_call(file_path=file_path, offset=offset, limit=limit)\n    return ToolResponse(\n        content=[TextBlock(type=\"text\", text=op.output)],\n    )\n```\n\n> **Important Note on Tool Replaceability**:\n> - The `grep` and `read_file` tools shown here are **example implementations** using ReMe's built-in operations\n> - You can **replace them with your own retrieval tools**, such as:\n>   - Vector database embedding retrieval (e.g., ChromaDB, Pinecone, Weaviate)\n>   - Web search APIs (e.g., Google Search, Bing Search)\n>   - Database query tools (e.g., SQL queries, MongoDB queries)\n>   - Custom domain-specific search solutions\n> - Similarly, the **offline write operations** (used internally by ReMeShortTermMemory to store compacted content) can be customized by modifying the `write_text_file` function in AgentScope's tool system\n> - The key requirement is that your tools return `ToolResponse` objects with appropriate content blocks\n\n#### 3. LLM Model Initialization\n\n```python\nllm = DashScopeChatModel(\n    model_name=\"qwen3-coder-30b-a3b-instruct\",\n    api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n    stream=False,\n    generate_kwargs={\n        \"temperature\": 0.001,\n        \"seed\": 0,\n    },\n)\n```\n\nThe model is configured with low temperature for consistent, deterministic responses. This same model will be used for both agent reasoning and memory summarization operations.\n\n#### 4. Short-Term Memory Initialization\n\n```python\nshort_term_memory = ReMeShortTermMemory(\n    model=llm,\n    working_summary_mode=\"auto\",           # Automatic memory management\n    compact_ratio_threshold=0.75,          # Trigger compaction at 75% capacity\n    max_total_tokens=20000,                # Set to 20%-50% of model's context window\n    max_tool_message_tokens=2000,          # Maximum tolerable tool response length\n    group_token_threshold=None,            # Max tokens per LLM compression batch; None means no splitting\n    keep_recent_count=1,                   # Keep 1 recent message intact (set to 1 for demo; use 10 in production)\n    store_dir=\"inmemory\",            # Storage directory for offloaded content\n)\n```\n\nThis configuration enables automatic memory management that will:\n- Monitor token usage\n- Automatically compact large tool responses when they exceed `max_tool_message_tokens`\n- Trigger summarization when total tokens exceed `max_total_tokens` and compaction ratio exceeds `compact_ratio_threshold`\n\n#### 5. Async Context Manager Usage\n\n```python\nasync with short_term_memory:\n    # All memory operations happen here\n```\n\nThe `async with` statement ensures proper initialization and cleanup of memory resources. This is the recommended approach for using `ReMeShortTermMemory`.\n\n#### 6. Simulating Long Context\n\nThe example demonstrates memory compaction by adding a large tool response:\n\n```python\n# Read README content and multiply it 10 times to simulate a large response\nf = open(\"../../../../README.md\", encoding=\"utf-8\")\nreadme_content = f.read()\nf.close()\n\nmemories = [\n    {\n        \"role\": \"user\",\n        \"content\": \"搜索下项目资料\",\n    },\n    {\n        \"role\": \"assistant\",\n        \"content\": None,\n        \"tool_calls\": [...],  # Tool call metadata\n    },\n    {\n        \"role\": \"tool\",\n        \"content\": readme_content * 10,  # Large tool response (10x README)\n        \"tool_call_id\": \"call_6596dafa2a6a46f7a217da\",\n    },\n]\n\nawait short_term_memory.add(\n    ReMeShortTermMemory.list_to_msg(memories),\n    allow_duplicates=True,\n)\n```\n\nWhen this large content is added, `ReMeShortTermMemory` will:\n1. Detect that the tool response exceeds `max_tool_message_tokens` (the maximum tolerable tool response length, set to 2000 in this example)\n2. Automatically compact it by storing the full content in an external file\n3. Keep only a short preview in the active memory\n4. This happens transparently without manual intervention\n\n#### 7. ReAct Agent Creation\n\n```python\nagent = ReActAgent(\n    name=\"react\",\n    sys_prompt=(\n        \"You are a helpful assistant. \"\n        \"工具调用的调用可能会被缓存到本地。\"\n        \"可以先使用`Grep`匹配关键词或者正则表达式所在行数，然后通过`ReadFile`读取位置附近的代码。\"\n        # ... more instructions\n    ),\n    model=llm,\n    formatter=DashScopeChatFormatter(),\n    toolkit=toolkit,\n    memory=short_term_memory,  # Memory is integrated here\n    max_iters=20,\n)\n```\n\nThe agent is configured with:\n- The same LLM model used for memory operations\n- The toolkit containing `grep` and `read_file` tools\n- The `short_term_memory` instance for automatic context management\n- A system prompt that guides the agent on tool usage patterns\n\n#### 8. Agent Interaction\n\n```python\nmsg = Msg(\n    role=\"user\",\n    content=(\"项目资料中，agentscope_v1论文的一作是谁？\"),\n    name=\"user\",\n)\nmsg = await agent(msg)\nprint(f\"✓ Agent response: {msg.get_text_content()}\\n\")\n```\n\nWhen the agent processes this message:\n1. It receives the user query\n2. Decides to use tools (e.g., `grep` to search for \"agentscope_v1\")\n3. Tool responses are automatically added to memory\n4. If memory grows too large, automatic compaction occurs\n5. The agent generates a response based on the managed context\n6. The response is returned to the user\n\n### Complete Example Code Structure\n\n```python\nasync def main() -> None:\n    # 1. Create toolkit and register tools\n    toolkit = Toolkit()\n    toolkit.register_tool_function(grep)\n    toolkit.register_tool_function(read_file)\n\n    # 2. Initialize LLM\n    llm = DashScopeChatModel(...)\n\n    # 3. Create short-term memory\n    short_term_memory = ReMeShortTermMemory(...)\n\n    # 4. Use async context manager\n    async with short_term_memory:\n        # 5. Add initial messages (with large content to demo compaction)\n        await short_term_memory.add(messages, allow_duplicates=True)\n\n        # 6. Create agent with memory\n        agent = ReActAgent(..., memory=short_term_memory, ...)\n\n        # 7. Interact with agent\n        response = await agent(user_message)\n```\n\n### Key Takeaways\n\n1. **Automatic Memory Management**: Memory compaction and summarization happen automatically when thresholds are exceeded\n2. **Tool Integration**: Tools return `ToolResponse` objects that are seamlessly integrated into memory\n3. **Async Context Manager**: Always use `async with short_term_memory:` to ensure proper resource management\n4. **Flexible Tool System**: The `grep` and `read_file` tools are examples—you can replace them with any retrieval mechanism that fits your use case\n5. **Transparent Operation**: Memory management is transparent to the agent—it just sees a clean, focused context\n\n### Using Async Context Manager\n\n`ReMeShortTermMemory` implements the async context manager protocol, which ensures proper initialization and cleanup of resources. There are two ways to use it:\n\n#### Recommended: Using `async with` Statement\n\nThe recommended approach is to use the `async with` statement, which automatically handles resource management:\n\n```python\nasync with short_term_memory:\n    # Memory is initialized here\n    await short_term_memory.add(messages)\n    response = await agent(msg)\n    # Memory is automatically cleaned up when exiting the block\n```\n\n#### Alternative: Manual `__aenter__` and `__aexit__` Calls\n\nYou can also manually call `__aenter__` and `__aexit__` if you need more control:\n\n```python\n# Manually initialize\nawait short_term_memory.__aenter__()\n\ntry:\n    # Use the memory\n    await short_term_memory.add(messages)\n    response = await agent(msg)\nfinally:\n    # Manually cleanup\n    await short_term_memory.__aexit__(None, None, None)\n```\n\n> **Note**: It's recommended to use the `async with` statement as it ensures proper resource cleanup even if an exception occurs.\n\n## Advanced Configuration\n\nYou can customize the ReMe config by passing a config path:\n\n```python\nshort_term_memory = ReMeShortTermMemory(\n    model=llm,\n    reme_config_path=\"path/to/your/config.yaml\",  # Pass your custom ReMe configuration\n    # ... other parameters\n)\n```\n\nFor more configuration options, refer to the [ReMe documentation](https://github.com/agentscope-ai/ReMe).\n\n## What's in the Example\n\nThe `short_term_memory_example.py` file demonstrates:\n\n1. **Tool Integration**: Registering `grep` and `read_file` tools for searching and reading files\n2. **Memory Initialization**: Setting up ReMeShortTermMemory with appropriate parameters for handling long contexts\n3. **Long Context Handling**: Adding a large tool response (README content × 10) to demonstrate automatic memory compaction\n4. **ReAct Agent Usage**: Using the agent with short-term memory to answer questions based on retrieved information\n\n## Example Workflow\n\nThe example shows a typical workflow:\n\n1. User asks to search for project information\n2. Agent uses `grep` tool to find relevant content\n3. Agent uses `read_file` tool to read specific sections\n4. Large tool responses are automatically compacted by the memory system\n5. Agent answers the user's question based on the retrieved information\n\n"
  },
  {
    "path": "examples/functionality/short_term_memory/reme/reme_short_term_memory.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"ReMe-based short-term memory implementation for AgentScope.\"\"\"\nimport json\nfrom pathlib import Path\nfrom typing import Any, List\nfrom uuid import uuid4\n\nfrom agentscope import logger\nfrom agentscope._utils._common import _json_loads_with_repair\nfrom agentscope.formatter import DashScopeChatFormatter, OpenAIChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.message import Msg, TextBlock, ToolUseBlock, ToolResultBlock\nfrom agentscope.model import DashScopeChatModel, OpenAIChatModel\nfrom agentscope.tool import write_text_file\n\n\nclass ReMeShortTermMemory(InMemoryMemory):\n    \"\"\"Short-term memory implementation using ReMe for message management.\n\n    This class provides automatic working-memory management through a\n    multi-stage pipeline that reduces token usage while preserving\n    essential information:\n\n    1. **Compaction**: Truncates large tool messages by storing full\n       content in external files and keeping only short previews in the\n       active context.\n    2. **Compression**: Uses LLM to generate dense summaries of older\n       conversation history, creating a compact state snapshot.\n    3. **Offload**: Orchestrates compaction and optional compression\n       based on the configured working_summary_mode (COMPACT, COMPRESS,\n       or AUTO).\n\n    The memory management is triggered automatically when `get_memory()`\n    is called, ensuring the agent's context stays within token limits\n    while maintaining access to detailed historical information through\n    external storage.\n    \"\"\"\n\n    def __init__(\n        self,\n        model: DashScopeChatModel | OpenAIChatModel | None = None,\n        reme_config_path: str | None = None,\n        working_summary_mode: str = \"auto\",\n        compact_ratio_threshold: float = 0.75,\n        max_total_tokens: int = 20000,\n        max_tool_message_tokens: int = 2000,\n        group_token_threshold: int | None = None,\n        keep_recent_count: int = 10,\n        store_dir: str = \"inmemory\",\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Initialize ReMe-based short-term memory.\n\n        Args:\n            model: Language model for compression operations. Must be\n                either DashScopeChatModel or OpenAIChatModel.\n            reme_config_path: Optional path to ReMe configuration file\n                for custom settings.\n            working_summary_mode: Strategy for working memory management.\n                - \"compact\": Only compact verbose tool messages by\n                  storing full content externally and keeping short\n                  previews.\n                - \"compress\": Only apply LLM-based compression to\n                  generate compact state snapshots.\n                - \"auto\": First run compaction, then optionally run\n                  compression if the compaction ratio exceeds\n                  compact_ratio_threshold.\n                Defaults to \"auto\".\n            compact_ratio_threshold: Threshold for compaction\n                effectiveness in AUTO mode. If (compacted_tokens /\n                original_tokens) > this threshold, compression is\n                applied. Defaults to 0.75.\n            max_total_tokens: Maximum token count threshold before\n                compression is triggered. Does not include\n                keep_recent_count messages or system messages.\n                Defaults to 20000.\n            max_tool_message_tokens: Maximum token count for individual\n                tool messages before compaction. Tool messages exceeding\n                this are stored externally. Defaults to 2000.\n            group_token_threshold: Maximum token count per compression\n                group when splitting messages for LLM compression. If\n                None or 0, all messages are compressed in a single\n                group. Defaults to None.\n            keep_recent_count: Number of most recent messages to\n                preserve without compression or compaction. These\n                messages remain in full in the active context.\n                Defaults to 1.\n            store_dir: Directory path for storing offloaded message\n                content and compressed history files. Defaults to\n                \"working_memory\".\n            **kwargs: Additional arguments passed to ReMeApp\n                initialization.\n\n        Raises:\n            ValueError: If model is not a DashScopeChatModel or\n                OpenAIChatModel.\n            ImportError: If reme_ai library is not installed.\n        \"\"\"\n        super().__init__()\n\n        # Store working memory parameters\n        self.working_summary_mode = working_summary_mode\n        self.compact_ratio_threshold = compact_ratio_threshold\n        self.max_total_tokens = max_total_tokens\n        self.max_tool_message_tokens = max_tool_message_tokens\n        self.group_token_threshold = group_token_threshold\n        self.keep_recent_count = keep_recent_count\n        self.store_dir = store_dir\n\n        config_args = []\n\n        if isinstance(model, DashScopeChatModel):\n            llm_api_base = \"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n            llm_api_key = model.api_key\n            self.formatter = DashScopeChatFormatter()\n\n        elif isinstance(model, OpenAIChatModel):\n            llm_api_base = str(getattr(model.client, \"base_url\", None))\n            llm_api_key = str(getattr(model.client, \"api_key\", None))\n            self.formatter = OpenAIChatFormatter()\n\n        else:\n            raise ValueError(\n                \"model must be a DashScopeChatModel or \"\n                \"OpenAIChatModel instance. \"\n                f\"Got {type(model).__name__} instead.\",\n            )\n\n        llm_model_name = model.model_name\n\n        if llm_model_name:\n            config_args.append(f\"llm.default.model_name={llm_model_name}\")\n\n        try:\n            from reme_ai import ReMeApp\n        except ImportError as e:\n            raise ImportError(\n                \"The 'reme_ai' library is required for ReMe-based \"\n                \"short-term memory. Please try `pip install reme-ai`,\"\n                \"and visit: https://github.com/agentscope-ai/ReMe for more \"\n                \"information.\",\n            ) from e\n\n        self.app = ReMeApp(\n            *config_args,\n            llm_api_key=llm_api_key,\n            llm_api_base=llm_api_base,\n            embedding_api_key=llm_api_key,  # fake api key\n            embedding_api_base=llm_api_base,  # fake api base\n            config_path=reme_config_path,\n            **kwargs,\n        )\n\n        self._app_started = False\n\n    async def __aenter__(self) -> \"ReMeShortTermMemory\":\n        \"\"\"Async context manager entry.\n\n        Initializes the ReMe application for async operations.\n        \"\"\"\n        if self.app is not None:\n            await self.app.__aenter__()\n            self._app_started = True\n        return self\n\n    async def __aexit__(\n        self,\n        exc_type: Any = None,\n        exc_val: Any = None,\n        exc_tb: Any = None,\n    ) -> None:\n        \"\"\"Async context manager exit.\n\n        Cleans up the ReMe application resources.\n        \"\"\"\n        if self.app is not None:\n            await self.app.__aexit__(exc_type, exc_val, exc_tb)\n        self._app_started = False\n\n    async def get_memory(self) -> list[Msg]:\n        \"\"\"Retrieve and manage working memory with automatic summarization.\n\n        This method performs the core working-memory management pipeline:\n\n        1. **Format messages**: Converts internal Msg objects to standard\n           message format using the appropriate formatter (DashScope or\n           OpenAI).\n        2. **Execute offload pipeline**: Calls ReMe's\n           summary_working_memory_for_as operation which orchestrates:\n           - Message compaction: Large tool messages are truncated and\n             stored externally with only previews kept in context.\n           - Message compression: If needed (based on\n             working_summary_mode), older messages are compressed using\n             LLM into dense summaries.\n           - File storage: Offloaded content is written to external\n             files for potential retrieval.\n        3. **Update content**: Replaces the internal message list with\n           the managed version, ensuring subsequent operations work with\n           the optimized context.\n\n        The operation respects configuration parameters like\n        max_total_tokens, keep_recent_count, and working_summary_mode to\n        balance context size with information preservation.\n\n        Returns:\n            List of Msg objects representing the managed working memory,\n            with large tool messages compacted and/or older history\n            compressed as needed.\n\n        Note:\n            This method automatically writes offloaded content to files\n            in the configured store_dir. The write_file_dict metadata\n            contains paths and content for all externally stored\n            messages.\n        \"\"\"\n        messages: list[dict[str, Any]] = await self.formatter.format(\n            msgs=self.content,  # type: ignore[has-type]\n        )\n        for message in messages:\n            if isinstance(message.get(\"content\"), list):\n                msg_content = message.get(\"content\")\n                logger.warning(\n                    \"Skipping message with content as list. content=%s\",\n                    msg_content,\n                )\n                message[\"content\"] = \"\"\n\n        # Execute ReMe's working memory offload pipeline\n        # This orchestrates compaction and/or compression based on\n        # working_summary_mode\n        result: dict = await self.app.async_execute(\n            name=\"summary_working_memory_for_as\",\n            messages=messages,\n            working_summary_mode=self.working_summary_mode,\n            compact_ratio_threshold=self.compact_ratio_threshold,\n            max_total_tokens=self.max_total_tokens,\n            max_tool_message_tokens=self.max_tool_message_tokens,\n            group_token_threshold=self.group_token_threshold,\n            keep_recent_count=self.keep_recent_count,\n            store_dir=self.store_dir,\n            chat_id=uuid4().hex,\n        )\n        logger.info(\n            \"summary_working_memory_for_as.result=%s\",\n            json.dumps(result, ensure_ascii=False, indent=2),\n        )\n\n        # Extract managed messages and file write operations from result\n        messages = result.get(\"answer\", [])\n        write_file_dict: dict = result.get(\"metadata\", {}).get(\n            \"write_file_dict\",\n            {},\n        )\n        # Write offloaded content to external files\n        # This includes full tool message content and compressed message\n        # history\n        if write_file_dict:\n            for path, content_str in write_file_dict.items():\n                file_dir = Path(path).parent\n                if not file_dir.exists():\n                    file_dir.mkdir(parents=True, exist_ok=True)\n                await write_text_file(path, content_str)\n\n        # Update internal content with managed messages\n        self.content = self.list_to_msg(messages)\n        return self.content\n\n    @staticmethod\n    def list_to_msg(messages: list[dict[str, Any]]) -> list[Msg]:\n        \"\"\"Convert a list of message dictionaries to Msg objects.\n\n        This method handles the conversion from standard message format\n        (used by ReMe and LLM APIs) back to AgentScope's Msg objects.\n        It properly handles:\n        - Text content for user, system, and assistant messages\n        - Tool result blocks (converting role=\"tool\" to role=\"system\")\n        - Tool use blocks from tool_calls in assistant messages\n\n        Args:\n            messages: List of message dictionaries with role, content,\n                and optional tool_calls or tool-related fields.\n\n        Returns:\n            List of Msg objects with properly structured content blocks.\n        \"\"\"\n        msg_list: list[Msg] = []\n        for msg_dict in messages:\n            role = msg_dict[\"role\"]\n            content_blocks: List[\n                TextBlock | ToolUseBlock | ToolResultBlock\n            ] = []\n            content = msg_dict.get(\"content\")\n\n            # Convert text content to appropriate content blocks\n            if content:\n                if role in [\"user\", \"system\", \"assistant\"]:\n                    content_blocks.append(TextBlock(type=\"text\", text=content))\n                elif role in [\"tool\"]:\n                    # Tool messages are converted to system messages with\n                    # ToolResultBlock\n                    role = \"system\"\n                    content_blocks.append(\n                        ToolResultBlock(\n                            type=\"tool_result\",\n                            name=msg_dict.get(\"name\"),\n                            id=msg_dict.get(\"tool_call_id\"),\n                            output=[TextBlock(type=\"text\", text=content)],\n                        ),\n                    )\n\n            # Convert tool_calls to ToolUseBlock content blocks\n            if msg_dict.get(\"tool_calls\"):\n                for tool_call in msg_dict[\"tool_calls\"]:\n                    # Parse tool arguments with repair for malformed JSON\n                    input_ = _json_loads_with_repair(\n                        tool_call[\"function\"].get(\n                            \"arguments\",\n                            \"{}\",\n                        )\n                        or \"{}\",\n                    )\n                    content_blocks.append(\n                        ToolUseBlock(\n                            type=\"tool_use\",\n                            name=tool_call[\"function\"][\"name\"],\n                            input=input_,\n                            id=tool_call[\"id\"],\n                        ),\n                    )\n\n            msg_obj = Msg(\n                name=role,\n                content=content_blocks,\n                role=role,\n                metadata=msg_dict.get(\"metadata\"),\n            )\n            msg_list.append(msg_obj)\n        return msg_list\n\n    async def retrieve(self, *args: Any, **kwargs: Any) -> None:\n        \"\"\"Retrieve operation is not implemented for ReMe short-term memory.\n\n        ReMe focuses on working memory management (compaction and compression)\n        rather than retrieval from long-term storage.\n\n        Raises:\n            NotImplementedError: This operation is not supported.\n        \"\"\"\n        raise NotImplementedError\n"
  },
  {
    "path": "examples/functionality/short_term_memory/reme/short_term_memory_example.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Example demonstrating ReMeShortTermMemory usage with ReActAgent.\"\"\"\n# noqa: E402\nimport asyncio\nimport os\n\nfrom dotenv import load_dotenv\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.message import Msg, TextBlock\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import ToolResponse, Toolkit, view_text_file\n\nload_dotenv()\n\n\nasync def main() -> None:\n    \"\"\"Main function demonstrating ReMeShortTermMemory with tool usage.\"\"\"\n    from reme_short_term_memory import ReMeShortTermMemory\n\n    toolkit = Toolkit()\n\n    async def grep(file_path: str, pattern: str, limit: str) -> ToolResponse:\n        \"\"\"A powerful search tool for finding patterns in files using regular\n        expressions.\n\n        Supports full regex syntax (e.g., \"log.*Error\", \"function\\\\s+\\\\w+\"),\n        glob pattern filtering, and result limiting. Ideal for searching code\n        or text content across multiple files.\n\n        Args:\n            file_path (`str`):\n                The path to the file to search in. Can be an absolute or\n                relative path.\n            pattern (`str`):\n                The search pattern or regular expression to match. Supports\n                full regex syntax for complex pattern matching.\n            limit (`str`):\n                The maximum number of matching results to return. Use this to\n                control output size for large files. Should not exceed 50.\n        \"\"\"\n        from reme_ai.retrieve.working import GrepOp\n\n        op = GrepOp()\n        await op.async_call(file_path=file_path, pattern=pattern, limit=limit)\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=op.output,\n                ),\n            ],\n        )\n\n    async def read_file(\n        file_path: str,\n        offset: int,\n        limit: int,\n    ) -> ToolResponse:\n        \"\"\"Reads and returns the content of a specified file.\n\n        For text files, it can read specific line ranges using the 'offset' and\n        'limit' parameters. Use offset and limit to paginate through large\n        files.\n\n        Note: It's recommended to use the `grep` tool first to locate the line\n        numbers of interest before calling this function.\n\n        Args:\n            file_path (`str`):\n                The path to the file to read. Can be an absolute or relative\n                path.\n            offset (`int`):\n                The starting line number to read from (0-indexed). Use this to\n                skip to a specific position in the file.\n            limit (`int`):\n                The maximum number of lines to read from the offset position.\n                Helps control memory usage when reading large files. Should\n                not exceed 100.\n        \"\"\"\n\n        return await view_text_file(file_path, ranges=[offset, offset + limit])\n\n    # These two tools are provided as examples. You can replace them with your\n    # own retrieval tools, such as vector database embedding retrieval or other\n    # search solutions that fit your use case.\n    toolkit.register_tool_function(grep)\n    toolkit.register_tool_function(read_file)\n\n    llm = DashScopeChatModel(\n        model_name=\"qwen3-max\",\n        # model_name=\"qwen3-coder-30b-a3b-instruct\",\n        api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n        stream=False,\n        generate_kwargs={\n            \"temperature\": 0.001,\n            \"seed\": 0,\n        },\n    )\n    short_term_memory = ReMeShortTermMemory(\n        model=llm,\n        working_summary_mode=\"auto\",\n        compact_ratio_threshold=0.75,\n        max_total_tokens=20000,\n        max_tool_message_tokens=2000,\n        group_token_threshold=None,  # Max tokens per compression batch\n        keep_recent_count=1,  # Set to 1 for demo; use 10 in production\n        store_dir=\"inmemory\",\n    )\n\n    async with short_term_memory:\n        # Simulate ultra long context\n        f = open(\"../../../../README.md\", encoding=\"utf-8\")\n        readme_content = f.read()\n        f.close()\n\n        memories = [\n            {\n                \"role\": \"user\",\n                \"content\": \"Search for project information\",\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": None,\n                \"tool_calls\": [\n                    {\n                        \"index\": 0,\n                        \"id\": \"call_6596dafa2a6a46f7a217da\",\n                        \"function\": {\n                            \"arguments\": \"{}\",\n                            \"name\": \"web_search\",\n                        },\n                        \"type\": \"function\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"content\": readme_content * 10,\n                \"tool_call_id\": \"call_6596dafa2a6a46f7a217da\",\n            },\n        ]\n        await short_term_memory.add(\n            ReMeShortTermMemory.list_to_msg(memories),\n            allow_duplicates=True,\n        )\n\n        agent = ReActAgent(\n            name=\"react\",\n            sys_prompt=(\n                \"You are a helpful assistant. \"\n                \"Tool calls may be cached locally. \"\n                \"You can first use `Grep` to match keywords or regular \"\n                \"expressions to find line numbers, then use `ReadFile` \"\n                \"to read the code near that location. \"\n                \"If no matches are found, never give up trying - try \"\n                \"other parameters or relax the matching conditions, such \"\n                \"as searching for only partial keywords. \"\n                \"After `Grep`, you can use the `ReadFile` command to \"\n                \"view content starting from a specified offset position \"\n                \"`offset` with length `limit`. \"\n                \"The maximum limit is 100. \"\n                \"If the current content is insufficient, the `ReadFile` \"\n                \"command can continuously try different `offset` and \"\n                \"`limit` parameters.\"\n            ),\n            model=llm,\n            formatter=DashScopeChatFormatter(),\n            toolkit=toolkit,\n            memory=short_term_memory,\n            max_iters=20,\n        )\n\n        msg = Msg(\n            role=\"user\",\n            content=(\n                \"In the project documentation, who is the first author \"\n                \"of the agentscope_v1 paper?\"\n            ),\n            name=\"user\",\n        )\n        msg = await agent(msg)\n        print(f\"✓ Agent response: {msg.get_text_content()}\\n\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/stream_printing_messages/README.md",
    "content": "# Stream Printing Messages\n\nThe AgentScope agent is designed to communicate with the user and the other agents by passing messages explicitly.\nHowever, we notice the requirements that obtain the printing messages from the agent in a streaming manner.\nTherefore, in example we demonstrate how to gather and yield the printing messages from a single agent and\nmulti-agent systems in a streaming manner.\n\n\n## Quick Start\n\nRun the following command to see the streaming printing messages from the agent.\nNote the messages with the same ID are the chunks of the same message in accumulated manner.\n\n- For single-agent:\n\n```bash\npython single_agent.py\n```\n\n- For multi-agent:\n\n```bash\npython multi_agent.py\n```\n\n> Note: The example is built with DashScope chat model. If you want to change the model in this example, don't forget\n> to change the formatter at the same time! The corresponding relationship between built-in models and formatters are\n> list in [our tutorial](https://doc.agentscope.io/tutorial/task_prompt.html#id1)"
  },
  {
    "path": "examples/functionality/stream_printing_messages/multi_agent.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Example for gather the printing messages from multiple agents.\"\"\"\nimport asyncio\nimport os\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeMultiAgentFormatter\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.pipeline import MsgHub, stream_printing_messages\n\n\ndef create_agent(name: str) -> ReActAgent:\n    \"\"\"Create an agent with the given name.\"\"\"\n    return ReActAgent(\n        name=name,\n        sys_prompt=f\"You are a student named {name}.\",\n        model=DashScopeChatModel(\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            model_name=\"qwen-max\",\n            stream=False,  # close streaming for simplicity\n        ),\n        formatter=DashScopeMultiAgentFormatter(),\n    )\n\n\nasync def workflow(\n    alice: ReActAgent,\n    bob: ReActAgent,\n    charlie: ReActAgent,\n) -> None:\n    \"\"\"The example workflow for multiple agents.\"\"\"\n    async with MsgHub(\n        participants=[alice, bob, charlie],\n        announcement=Msg(\n            \"user\",\n            \"Alice, Bob and Charlie, welcome to the meeting! Let's \"\n            \"meet each other first.\",\n            \"user\",\n        ),\n    ):\n        # agent speaks in turn\n        await alice()\n        await bob()\n        await charlie()\n\n\nasync def main() -> None:\n    \"\"\"The main entry for the example.\"\"\"\n    # Create agents\n    alice, bob, charlie = [\n        create_agent(_) for _ in [\"Alice\", \"Bob\", \"Charlie\"]\n    ]\n\n    async for msg, last in stream_printing_messages(\n        agents=[alice, bob, charlie],\n        coroutine_task=workflow(alice, bob, charlie),\n    ):\n        print(msg, last)\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/stream_printing_messages/single_agent.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The example demonstrating how to obtain the messages from the agent in a\nstreaming way.\"\"\"\nimport asyncio\nimport os\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.pipeline import stream_printing_messages\nfrom agentscope.tool import (\n    Toolkit,\n    execute_shell_command,\n    view_text_file,\n    execute_python_code,\n)\n\n\nasync def main() -> None:\n    \"\"\"The main function.\"\"\"\n    toolkit = Toolkit()\n    toolkit.register_tool_function(execute_shell_command)\n    toolkit.register_tool_function(execute_python_code)\n    toolkit.register_tool_function(view_text_file)\n\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"You are a helpful assistant named Friday.\",\n        # Change the model and formatter together if you want to try other\n        # models\n        model=DashScopeChatModel(\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            model_name=\"qwen-max\",\n            enable_thinking=False,\n            stream=True,\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n        memory=InMemoryMemory(),\n    )\n\n    # Prepare a user message\n    user_msg = Msg(\n        \"user\",\n        \"Hi! Who are you?\",\n        \"user\",\n    )\n\n    # We disable the terminal printing to avoid messy outputs\n    agent.set_console_output_enabled(False)\n\n    # obtain the printing messages from the agent in a streaming way\n    async for msg, last in stream_printing_messages(\n        agents=[agent],\n        coroutine_task=agent(user_msg),\n    ):\n        print(msg, last)\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/structured_output/README.md",
    "content": "# Structured Output Example\n\n## What This Example Demonstrates\n\nThis example showcases **structured output generation** using AgentScope with Pydantic models. It demonstrates how to constrain AI model outputs to follow specific data structures and formats, ensuring consistent and parseable responses.\n\n### Key Features:\n- **Structured Data Generation**: Forces agent responses to conform to\n  predefined schemas\n- **Pydantic Integration**: Uses Pydantic models to define output structure with validation\n- **Type Safety**: Ensures output data types match expected formats\n- **Field Validation**: Includes constraints like age limits (0-120) and enum choices\n- **JSON Output**: Generates clean, structured JSON responses\n\n### Example Models:\n\n1. **TableModel**: Structured person information\n   - `name`: Person's name (string)\n   - `age`: Person's age (integer,0-120)\n   - `intro`: One-sentence introduction (string)\n   - `honors`: List of honors/achievements (array of strings)\n\n2. **ChoiceModel**: Constrained choice selection\n   - `choice`: Must be one of \"apple\", \"banana\", or \"orange\"\n\n### Use Cases:\n- **Data Extraction**: Extract structured information from unstructured text\n- **Form Generation**: Generate consistent data for databases or APIs\n- **Survey Responses**: Ensure responses fit predefined categories\n- **Content Classification**: Categorize content into specific types\n\n## How to Run This Example\n1. **Set Environment Variable:**\n   ```bash\n   export DASHSCOPE_API_KEY=\"your_dashscope_api_key_here\"\n   ```\n2. **Run the script:**\n    ```bash\n   python main.py\n   ```\n3. **Expected Output:**\nThe program will generate two structured responses like below:\n```\nStructured Output 1:\n{\n    \"name\": \"Albert Einstein\",\n    \"age\": 76,\n    \"intro\": 1,\n    \"honors\": [\n        \"Nobel Prize in Physics (1921)\",\n        \"Copley Medal (1925)\"\n    ]\n}\nStructured Output 2:\n{\n    \"choice\": \"apple\"\n}\n```\n\n>💡**Note:** The specific content will vary with each run since the agent generates different responses, but the JSON structure will always conform to the predefined Pydantic models (`TableModel` and `ChoiceModel`).\n\n## How It Works:\n1. The agent receives a query along with a structured_model parameter\n2. The agent generates a response that conforms to the Pydantic model schema\n3. The structured data is returned in res.metadata as a validated JSON object\n4. Pydantic ensures all field types and constraints are satisfied\n\n## Custom Pydantic Models\nCreate your own structured output models for specific use cases, for example:\n\n```\nfrom typing import Optional\nfrom pydantic import BaseModel, Field, EmailStr\n\nclass BusinessModel(BaseModel):\n    \"\"\"Business information extraction model.\"\"\"\n\n    company_name: str = Field(description=\"Name of the company\")\n    industry: str = Field(description=\"Industry sector\")\n    founded_year: int = Field(description=\"Year founded\", ge=1800, le=2024)\n    headquarters: str = Field(description=\"Location of headquarters\")\n    employee_count: Optional[int] = Field(description=\"Number of employees\", ge=1)\n    email: Optional[EmailStr] = Field(description=\"Contact email address\")\n    website: Optional[str] = Field(description=\"Company website URL\")\n\n# Usage\nquery = Msg(\"user\", \"Tell me about Tesla Inc.\", \"user\")\nres = await agent(query, structured_model=BusinessModel)\n```\n\n## Best Practices for Structured Output\n\n1. **Use Descriptive Field Names:** Make field purposes clear\n2. **Add Field Descriptions:** Help the agent understand what data to generate\n3. **Set Validation Constraints:** Use Pydantic validators for data integrity\n4. **Choose Appropriate Types:** Use specific types like EmailStr, datetime, etc."
  },
  {
    "path": "examples/functionality/structured_output/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The main entry point of the structured output example.\"\"\"\nimport asyncio\nimport json\nimport os\nfrom typing import Literal\n\nfrom pydantic import BaseModel, Field\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import Toolkit\n\n\nclass TableModel(BaseModel):\n    \"\"\"A simple table model for structured output.\"\"\"\n\n    name: str = Field(description=\"The name of the person\")\n    age: int = Field(description=\"The age of the person\", ge=0, le=120)\n    intro: str = Field(description=\"A one-sentence introduction of the person\")\n    honors: list[str] = Field(\n        description=\"A list of honors received by this person\",\n    )\n\n\nclass ChoiceModel(BaseModel):\n    \"\"\"A simple choice model for structured output.\"\"\"\n\n    choice: Literal[\"apple\", \"banana\", \"orange\"] = Field(\n        description=\"Your choice of fruit\",\n    )\n\n\nasync def main() -> None:\n    \"\"\"The main entry point for the structured output example.\"\"\"\n    toolkit = Toolkit()\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"You are a helpful assistant named Friday.\",\n        model=DashScopeChatModel(\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            model_name=\"qwen-max\",\n            stream=True,\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n        memory=InMemoryMemory(),\n    )\n\n    query_msg_1 = Msg(\n        \"user\",\n        \"Please introduce Einstein\",\n        \"user\",\n    )\n    res = await agent(query_msg_1, structured_model=TableModel)\n    print(\n        \"Structured Output 1:\\n\"\n        \"```\\n\"\n        f\"{json.dumps(res.metadata, indent=4)}\\n\"\n        \"```\",\n    )\n\n    query_msg_2 = Msg(\n        \"user\",\n        \"Choose one of your favorite fruit\",\n        \"user\",\n    )\n    res = await agent(query_msg_2, structured_model=ChoiceModel)\n    print(\n        \"Structured Output 2:\\n\"\n        \"```\\n\"\n        f\"{json.dumps(res.metadata, indent=4)}\\n\"\n        \"```\",\n    )\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/tts/README.md",
    "content": "# TTS (Text-to-Speech) in AgentScope\n\nThis example demonstrates how to integrate DashScope Realtime TTS model with `ReActAgent` to enable audio output.\nThe agent can speak its responses in real-time.\n\nThis example uses DashScope's Realtime TTS model, you can also change to other TTS models supported by AgentScope, e.g.\nOpenAI, Gemini, etc.\n\nTo run the example, execute:\n\n```bash\npython main.py\n```\n"
  },
  {
    "path": "examples/functionality/tts/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The main entry point of the ReAct agent example.\"\"\"\nimport asyncio\nimport os\n\nfrom agentscope.agent import ReActAgent, UserAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import (\n    Toolkit,\n    execute_shell_command,\n    execute_python_code,\n    view_text_file,\n)\nfrom agentscope.tts import DashScopeRealtimeTTSModel\n\n\nasync def main() -> None:\n    \"\"\"The main entry point for the ReAct agent example.\"\"\"\n    toolkit = Toolkit()\n    toolkit.register_tool_function(execute_shell_command)\n    toolkit.register_tool_function(execute_python_code)\n    toolkit.register_tool_function(view_text_file)\n\n    agent = ReActAgent(\n        name=\"Friday\",\n        sys_prompt=\"You are a helpful assistant named Friday.\",\n        model=DashScopeChatModel(\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            model_name=\"qwen3-max\",\n            enable_thinking=False,\n            stream=True,\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n        memory=InMemoryMemory(),\n        # Specify the TTS model for real-time speech synthesis\n        tts_model=DashScopeRealtimeTTSModel(\n            model_name=\"qwen3-tts-flash-realtime\",\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            voice=\"Cherry\",\n            stream=False,\n        ),\n    )\n    user = UserAgent(\"User\")\n\n    msg = None\n    while True:\n        msg = await user(msg)\n        if msg.get_text_content() == \"exit\":\n            break\n        msg = await agent(msg)\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/vector_store/alibabacloud_mysql_vector/README.md",
    "content": "# AlibabaCloud MySQL Vector Store Example\n\nThis example demonstrates how to use the `AlibabaCloudMySQLStore` class in AgentScope's RAG system for vector storage and similarity search operations using AlibabaCloud MySQL (RDS) with native vector functions.\n\n## Features\n\nAlibabaCloudMySQLStore provides:\n- Vector storage using MySQL's native VECTOR data type\n- Automatic vector index creation (CREATE VECTOR INDEX) based on distance metric\n- Vector functions (VEC_FROMTEXT, VEC_DISTANCE_COSINE, VEC_DISTANCE_EUCLIDEAN)\n- Database-level distance calculation and sorting via ORDER BY\n- Two distance metrics: COSINE and EUCLIDEAN (supported by AlibabaCloud MySQL)\n- Metadata filtering support\n- CRUD operations (Create, Read, Update, Delete)\n- Support for chunked documents\n- Direct access to the underlying MySQL connection for advanced operations\n- Full integration with AlibabaCloud RDS MySQL instances\n\n## Prerequisites\n\n### 1. AlibabaCloud RDS MySQL Instance\n\nYou need an AlibabaCloud RDS MySQL instance with vector support:\n\n- **Version**: MySQL 8.0+\n- **Vector Plugin**: Ensure the vector search plugin is enabled (check `vidx_disabled` parameter is OFF)\n- **Network Access**: Configure security group and whitelist to allow access\n\n#### Create RDS MySQL Instance on AlibabaCloud:\n\n1. Go to [AlibabaCloud RDS Console](https://rdsnext.console.aliyun.com/)\n2. Click \"Create Instance\"\n3. Select MySQL 8.0 or higher\n4. Configure specifications based on your needs\n5. Set up network and security settings\n6. Note down the connection endpoint (e.g., `rm-xxxxx.mysql.rds.aliyuncs.com`)\n\n#### Configure Database:\n\n```sql\n-- Connect to your RDS MySQL instance\nmysql -h rm-xxxxx.mysql.rds.aliyuncs.com -P 3306 -u your_username -p\n\n-- Check if vector capability is enabled (vidx_disabled should be OFF)\nSHOW VARIABLES LIKE 'vidx_disabled';\n-- Expected result: vidx_disabled | OFF\n-- If OFF, vector capability is enabled\n-- If ON, contact AlibabaCloud support to enable vector search plugin\n\n-- Create database\nCREATE DATABASE agentscope_test;\n\n-- Use the database\nUSE agentscope_test;\n\n-- Verify vector functions are available\nSELECT VEC_FROMTEXT('[1,2,3]');\n```\n\n### 2. Python Dependencies\n\n```bash\npip install mysql-connector-python agentscope\n```\n\n### 3. Network Configuration\n\nEnsure your local machine or server can access the RDS instance:\n- Add your IP to the RDS whitelist\n- Configure security group rules\n- Use SSL connection if required\n\n## Configuration\n\nUpdate the connection parameters in `main.py`:\n\n```python\nstore = AlibabaCloudMySQLStore(\n    host=\"rm-xxxxx.mysql.rds.aliyuncs.com\",  # Your RDS endpoint\n    port=3306,\n    user=\"your_username\",        # Your RDS username\n    password=\"your_password\",    # Your RDS password\n    database=\"agentscope_test\",\n    table_name=\"test_vectors\",\n    dimensions=768,              # Set to your embedding dimension\n    distance=\"COSINE\",\n    # Optional: SSL configuration\n    # connection_kwargs={\n    #     \"ssl_ca\": \"/path/to/ca.pem\",\n    #     \"ssl_verify_cert\": True,\n    # }\n)\n```\n\n## Running the Example\n\n```bash\npython main.py\n```\n\n## Example Tests\n\nThe example includes three comprehensive tests:\n\n### 1. Basic CRUD Operations\n- Initialize AlibabaCloudMySQLStore\n- Add documents with embeddings\n- Search for similar documents\n- Delete documents\n- Get the underlying MySQL connection\n\n### 2. Search with Metadata Filtering\n- Add documents with different categories\n- Search with and without filters\n- Use SQL WHERE clauses for filtering\n\n### 3. Different Distance Metrics\n- Test COSINE similarity (best for normalized vectors)\n- Test EUCLIDEAN distance (best for absolute distance)\n\n## Key Features Explained\n\n### Distance Metrics\n\nAlibabaCloud MySQL supports two distance metrics:\n\n- **COSINE**: Measures the cosine of the angle between vectors. Values range from 0 (identical) to 2 (opposite). Best for text embeddings and normalized vectors.\n- **EUCLIDEAN**: Measures the straight-line Euclidean distance between vectors. Lower values indicate similarity. Best for absolute distance measurements.\n\n**Note**: Unlike some other vector databases, AlibabaCloud MySQL currently only supports COSINE and EUCLIDEAN distance functions. Inner Product (IP) is not supported.\n\n### Metadata Filtering\n\nUse SQL WHERE clauses to filter search results:\n\n```python\nresults = await store.search(\n    query_embedding=embedding,\n    limit=10,\n    filter='doc_id LIKE \"ai%\" AND chunk_id > 0',\n)\n```\n\n### Table Structure\n\nThe implementation automatically creates a table with the following structure:\n\n```sql\nCREATE TABLE IF NOT EXISTS table_name (\n    id VARCHAR(255) PRIMARY KEY,\n    embedding VECTOR(dimensions) NOT NULL,\n    doc_id VARCHAR(255) NOT NULL,\n    chunk_id INT NOT NULL,\n    content TEXT NOT NULL,\n    total_chunks INT NOT NULL,\n    INDEX idx_doc_id (doc_id),\n    INDEX idx_chunk_id (chunk_id),\n    VECTOR INDEX (embedding) M=16 DISTANCE=cosine  -- or DISTANCE=euclidean\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n```\n\n**Note**: The vector index is created directly within the `CREATE TABLE` statement, not as a separate SQL command. The `M` parameter controls the HNSW algorithm's graph connectivity (default: 16).\n\n### Performance Considerations\n\n- **VECTOR Data Type**: Uses MySQL's native VECTOR type for efficient storage\n- **Vector Index**: Automatically creates a vector index with the specified distance metric for fast similarity search\n- **Database-Level Distance Calculation**: Vector distance calculations are performed at the database level using MySQL's native vector functions (VEC_DISTANCE_COSINE, VEC_DISTANCE_EUCLIDEAN), with sorting done via SQL ORDER BY\n- **Native Vector Support**: MySQL 8.0+ has built-in vector functions that are highly optimized for vector operations\n- **Supported Distance Metrics**: Only COSINE and EUCLIDEAN are supported\n- **Small to Medium Datasets**: AlibabaCloudMySQLStore performs well for datasets up to 100K vectors\n- **Large Datasets**: For datasets with millions of vectors, consider using dedicated vector databases (MilvusLite, Qdrant) with specialized indexing (HNSW, IVF, etc.)\n- **RDS Performance**: Leverage AlibabaCloud RDS features like read replicas, backup, and monitoring\n\n## Advanced Usage\n\n### Direct Database Access\n\n```python\n# Get the MySQL connection for advanced operations\nconn = store.get_client()\ncursor = conn.cursor()\n\n# Execute custom SQL queries\ncursor.execute(\"SELECT COUNT(*) FROM test_vectors\")\ncount = cursor.fetchone()\nprint(f\"Total vectors: {count[0]}\")\n```\n\n### Using MySQL Native Vector Functions\n\nMySQL's native vector functions can be used directly in SQL queries:\n\n```python\nconn = store.get_client()\ncursor = conn.cursor()\n\n# Use MySQL native vector functions directly\nquery_vector = \"[0.1,0.2,0.3,0.4]\"\ncursor.execute(\"\"\"\n    SELECT\n        doc_id,\n        VEC_DISTANCE_COSINE(vector, VEC_FROMTEXT(%s)) as distance\n    FROM test_vectors\n    ORDER BY distance ASC\n    LIMIT 10\n\"\"\", (query_vector,))\n\nresults = cursor.fetchall()\n\n# Available MySQL vector functions in AlibabaCloud:\n# - VEC_FROMTEXT(text) - Convert text \"[1,2,3]\" to vector\n# - VEC_DISTANCE_COSINE(v1, v2) - Cosine distance\n# - VEC_DISTANCE_EUCLIDEAN(v1, v2) - Euclidean distance\n```\n\n### SSL Connection\n\nFor secure connections to AlibabaCloud RDS:\n\n```python\nstore = AlibabaCloudMySQLStore(\n    host=\"rm-xxxxx.mysql.rds.aliyuncs.com\",\n    port=3306,\n    user=\"your_username\",\n    password=\"your_password\",\n    database=\"agentscope_test\",\n    table_name=\"vectors\",\n    dimensions=768,\n    distance=\"COSINE\",\n    connection_kwargs={\n        \"ssl_ca\": \"/path/to/ca.pem\",\n        \"ssl_verify_cert\": True,\n        \"ssl_verify_identity\": True,\n    },\n)\n```\n\n### Batch Operations\n\n```python\n# Add large batches of documents\nbatch_size = 1000\nfor i in range(0, len(all_documents), batch_size):\n    batch = all_documents[i:i + batch_size]\n    await store.add(batch)\n```\n\n### Connection Pooling\n\n```python\nstore = AlibabaCloudMySQLStore(\n    host=\"rm-xxxxx.mysql.rds.aliyuncs.com\",\n    port=3306,\n    user=\"your_username\",\n    password=\"your_password\",\n    database=\"agentscope_test\",\n    table_name=\"vectors\",\n    dimensions=768,\n    distance=\"COSINE\",\n    connection_kwargs={\n        \"pool_name\": \"mypool\",\n        \"pool_size\": 10,\n        \"pool_reset_session\": True,\n    },\n)\n```\n\n## Troubleshooting\n\n### MySQL Version Check\n\nEnsure your RDS MySQL version supports vector functions:\n\n```sql\nSELECT VERSION();\n-- Should be MySQL 8.0 or higher\n\n-- Check if vector capability is enabled (critical check)\nSHOW VARIABLES LIKE 'vidx_disabled';\n-- Expected result: vidx_disabled | OFF (vector capability enabled)\n\n-- Test vector functions\nSELECT VEC_FROMTEXT('[1,2,3]');\n```\n\n### Connection Errors\n\nIf you get connection errors:\n\n1. **Check Whitelist**: Ensure your IP is in the RDS whitelist\n2. **Security Group**: Verify security group rules allow port 3306\n3. **Network Type**: Ensure you're using the correct endpoint (public/private)\n4. **Credentials**: Double-check username and password\n\n```bash\n# Test connection from command line\nmysql -h rm-xxxxx.mysql.rds.aliyuncs.com -P 3306 -u your_username -p\n```\n\n### Vector Function Errors\n\nIf you get errors about VEC_DISTANCE_COSINE or VECTOR type not being recognized:\n\n1. **Check if vector capability is enabled**:\n\n```sql\n-- Check vidx_disabled parameter (must be OFF)\nSHOW VARIABLES LIKE 'vidx_disabled';\n-- Expected result: vidx_disabled | OFF\n-- If ON, vector capability is disabled, contact AlibabaCloud support\n```\n\n2. Verify MySQL version is 8.0 or higher\n\n```sql\nSELECT VERSION();\n```\n\n3. Test vector functions availability:\n\n```sql\n-- Check if vector functions are available\nSELECT VEC_FROMTEXT('[1,2,3]');\n\n-- Check if VECTOR type is supported\nCREATE TABLE test_vector (v VECTOR(3));\nDROP TABLE test_vector;\n\n-- List vector indexes\nSHOW INDEX FROM your_table_name WHERE Index_type = 'VECTOR';\n```\n\nIf `vidx_disabled` is ON, contact AlibabaCloud support to enable the vector search plugin for your RDS instance.\n\n### Performance Optimization\n\nFor large datasets on AlibabaCloud RDS:\n\n1. **Upgrade Instance**: Consider higher specifications (CPU, Memory)\n2. **Read Replicas**: Use read replicas for read-heavy workloads\n3. **Indexes**: Add indexes on frequently filtered columns\n4. **Connection Pool**: Use connection pooling for concurrent operations\n5. **Monitor**: Use AlibabaCloud CloudMonitor for performance insights\n\n### Timeout Errors\n\nIf you experience timeout errors:\n\n```python\nstore = AlibabaCloudMySQLStore(\n    host=\"rm-xxxxx.mysql.rds.aliyuncs.com\",\n    port=3306,\n    user=\"your_username\",\n    password=\"your_password\",\n    database=\"agentscope_test\",\n    table_name=\"vectors\",\n    dimensions=768,\n    distance=\"COSINE\",\n    connection_kwargs={\n        \"connect_timeout\": 30,\n        \"read_timeout\": 60,\n        \"write_timeout\": 60,\n    },\n)\n```\n\n## AlibabaCloud RDS Best Practices\n\n1. **Backup**: Enable automatic backups in RDS console\n2. **Monitoring**: Set up alerts for CPU, memory, and connection usage\n3. **Security**: Use private network connections when possible\n4. **Scaling**: Consider read-only instances for read-heavy workloads\n5. **Cost Optimization**: Use reserved instances for long-term usage\n\n## Related Resources\n\n- [AlibabaCloud RDS Documentation](https://www.alibabacloud.com/help/en/apsaradb-for-rds)\n- [AlibabaCloud MySQL Vector Functions](https://www.alibabacloud.com/help/en/rds/apsaradb-rds-for-mysql/vector-storage-1)\n- [AgentScope RAG Tutorial](https://doc.agentscope.io/tutorial/task_rag.html)\n- [MySQL Connector Python](https://dev.mysql.com/doc/connector-python/en/)\n\n## Example Use Cases\n\n### RAG System with AlibabaCloud\n\n```python\nfrom agentscope.rag import AlibabaCloudMySQLStore, KnowledgeBase\n\n# Initialize vector store with AlibabaCloud RDS\nstore = AlibabaCloudMySQLStore(\n    host=\"rm-xxxxx.mysql.rds.aliyuncs.com\",\n    port=3306,\n    user=\"your_username\",\n    password=\"your_password\",\n    database=\"rag_system\",\n    table_name=\"knowledge_vectors\",\n    dimensions=768,\n    distance=\"COSINE\",\n)\n\n# Create knowledge base\nkb = KnowledgeBase(store=store)\n\n# Add documents\nawait kb.add_documents(documents)\n\n# Search\nresults = await kb.search(\"What is AI?\", top_k=5)\n```\n\n## Support\n\nFor issues related to:\n- **AlibabaCloudMySQLStore**: Open an issue on AgentScope GitHub\n- **RDS MySQL**: Contact AlibabaCloud Support\n- **Vector Functions**: Check MySQL documentation or AlibabaCloud support\n\n## License\n\nThis example is part of the AgentScope project and follows the same license.\n\n"
  },
  {
    "path": "examples/functionality/vector_store/alibabacloud_mysql_vector/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Example of using AlibabaCloudMySQLStore in AgentScope RAG system.\"\"\"\nimport asyncio\nfrom agentscope.rag import (\n    AlibabaCloudMySQLStore,\n    Document,\n    DocMetadata,\n)\nfrom agentscope.message import TextBlock\n\n\nasync def example_basic_operations() -> None:\n    \"\"\"The example of basic CRUD operations with AlibabaCloudMySQLStore.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test 1: Basic CRUD Operations\")\n    print(\"=\" * 60)\n\n    # Initialize AlibabaCloudMySQLStore\n    # Replace with your AlibabaCloud MySQL connection details\n    store = AlibabaCloudMySQLStore(\n        host=\"rm-xxxxx.mysql.rds.aliyuncs.com\",  # Your RDS endpoint\n        port=3306,\n        user=\"your_username\",\n        password=\"your_password\",\n        database=\"agentscope_test\",\n        table_name=\"test_vectors\",\n        dimensions=4,  # Small dimension for testing\n        distance=\"COSINE\",\n    )\n\n    print(\"✓ AlibabaCloudMySQLStore initialized\")\n\n    # Create test documents with embeddings\n    test_docs = [\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(\n                    text=\"Artificial Intelligence is the future\",\n                ),\n                doc_id=\"doc_1\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.1, 0.2, 0.3, 0.4],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Machine Learning is a subset of AI\"),\n                doc_id=\"doc_2\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.2, 0.3, 0.4, 0.5],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Deep Learning uses neural networks\"),\n                doc_id=\"doc_3\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.3, 0.4, 0.5, 0.6],\n        ),\n    ]\n\n    # Test add operation\n    await store.add(test_docs)\n    print(f\"✓ Added {len(test_docs)} documents to the store\")\n\n    # Test search operation\n    query_embedding = [0.15, 0.25, 0.35, 0.45]\n    results = await store.search(\n        query_embedding=query_embedding,\n        limit=2,\n    )\n\n    print(f\"\\n✓ Search completed, found {len(results)} results:\")\n    for i, result in enumerate(results, 1):\n        print(f\"  {i}. Score: {result.score:.4f}\")\n        print(f\"     Content: {result.metadata.content}\")\n        print(f\"     Doc ID: {result.metadata.doc_id}\")\n\n    # Test search with score threshold\n    results_filtered = await store.search(\n        query_embedding=query_embedding,\n        limit=5,\n        score_threshold=0.9,\n    )\n    print(f\"\\n✓ Search with threshold (>0.9): {len(results_filtered)} results\")\n\n    # Test delete operation\n    await store.delete(filter='doc_id = \"doc_2\"')\n    print(\"\\n✓ Deleted document with doc_id='doc_2'\")\n\n    # Verify deletion\n    results_after_delete = await store.search(\n        query_embedding=query_embedding,\n        limit=5,\n    )\n    print(f\"✓ After deletion: {len(results_after_delete)} documents remain\")\n\n    # Get client for advanced operations\n    client = store.get_client()\n    print(f\"\\n✓ Got MySQL connection: {type(client).__name__}\")\n\n    # Close connection\n    store.close()\n    print(\"✓ Connection closed\")\n\n\nasync def example_filter_search() -> None:\n    \"\"\"The example of search with metadata filtering.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test 2: Search with Metadata Filtering\")\n    print(\"=\" * 60)\n\n    store = AlibabaCloudMySQLStore(\n        host=\"rm-xxxxx.mysql.rds.aliyuncs.com\",\n        port=3306,\n        user=\"your_username\",\n        password=\"your_password\",\n        database=\"agentscope_test\",\n        table_name=\"filter_vectors\",\n        dimensions=4,\n        distance=\"COSINE\",\n    )\n\n    # Create documents with different categories\n    docs = [\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Python is a programming language\"),\n                doc_id=\"prog_1\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.1, 0.2, 0.3, 0.4],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(\n                    text=\"Java is used for enterprise applications\",\n                ),\n                doc_id=\"prog_2\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.2, 0.3, 0.4, 0.5],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Neural networks are used in AI\"),\n                doc_id=\"ai_1\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.3, 0.4, 0.5, 0.6],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Deep learning requires GPUs\"),\n                doc_id=\"ai_2\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.4, 0.5, 0.6, 0.7],\n        ),\n    ]\n\n    await store.add(docs)\n    print(f\"✓ Added {len(docs)} documents with different doc_id prefixes\")\n\n    # Search without filter\n    query_embedding = [0.25, 0.35, 0.45, 0.55]\n    all_results = await store.search(\n        query_embedding=query_embedding,\n        limit=4,\n    )\n    print(f\"\\n✓ Search without filter: {len(all_results)} results\")\n    for i, result in enumerate(all_results, 1):\n        doc_id = result.metadata.doc_id\n        score = result.score\n        print(f\"  {i}. Doc ID: {doc_id}, Score: {score:.4f}\")\n\n    # Search with filter for programming docs\n    prog_results = await store.search(\n        query_embedding=query_embedding,\n        limit=4,\n        filter='doc_id LIKE \"prog%\"',\n    )\n    filter_msg = 'doc_id LIKE \"prog%\"'\n    print(f\"\\n✓ Search with filter ({filter_msg}): {len(prog_results)}\")\n    for i, result in enumerate(prog_results, 1):\n        doc_id = result.metadata.doc_id\n        score = result.score\n        print(f\"  {i}. Doc ID: {doc_id}, Score: {score:.4f}\")\n\n    # Search with filter for AI docs\n    ai_results = await store.search(\n        query_embedding=query_embedding,\n        limit=4,\n        filter='doc_id LIKE \"ai%\"',\n    )\n    filter_msg = 'doc_id LIKE \"ai%\"'\n    print(f\"\\n✓ Search with filter ({filter_msg}): {len(ai_results)}\")\n    for i, result in enumerate(ai_results, 1):\n        doc_id = result.metadata.doc_id\n        score = result.score\n        print(f\"  {i}. Doc ID: {doc_id}, Score: {score:.4f}\")\n\n    store.close()\n\n\nasync def example_distance_metrics() -> None:\n    \"\"\"The example of different distance metrics.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test 3: Different Distance Metrics\")\n    print(\"=\" * 60)\n\n    # Test with different metrics\n    # Note: AlibabaCloud MySQL only supports COSINE and EUCLIDEAN\n    metrics = [\"COSINE\", \"EUCLIDEAN\"]\n\n    for metric in metrics:\n        print(f\"\\n--- Testing {metric} metric ---\")\n        store = AlibabaCloudMySQLStore(\n            host=\"rm-xxxxx.mysql.rds.aliyuncs.com\",\n            port=3306,\n            user=\"your_username\",\n            password=\"your_password\",\n            database=\"agentscope_test\",\n            table_name=f\"{metric.lower()}_vectors\",\n            dimensions=4,\n            distance=metric,\n        )\n\n        docs = [\n            Document(\n                metadata=DocMetadata(\n                    content=TextBlock(text=f\"Test doc for {metric}\"),\n                    doc_id=f\"doc_{metric}_1\",\n                    chunk_id=0,\n                    total_chunks=1,\n                ),\n                embedding=[0.1, 0.2, 0.3, 0.4],\n            ),\n        ]\n\n        await store.add(docs)\n        results = await store.search(\n            query_embedding=[0.1, 0.2, 0.3, 0.4],\n            limit=1,\n        )\n\n        print(f\"✓ {metric} metric: Score = {results[0].score:.4f}\")\n        store.close()\n\n\nasync def main() -> None:\n    \"\"\"Run all examples.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"AlibabaCloud MySQL Vector Store Test Suite\")\n    print(\"=\" * 60)\n\n    try:\n        await example_basic_operations()\n        await example_filter_search()\n        await example_distance_metrics()\n\n        print(\"\\n\" + \"=\" * 60)\n        print(\"✓ All tests completed successfully!\")\n        print(\"=\" * 60)\n\n    except Exception as e:\n        print(f\"\\n✗ Test failed with error: {e}\")\n        import traceback\n\n        traceback.print_exc()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/vector_store/milvus_lite/README.md",
    "content": "# MilvusLite Vector Store\n\nThis example demonstrates how to use **MilvusLiteStore** for vector storage and semantic search in AgentScope.\nIt includes four test scenarios covering CRUD operations, metadata filtering, document chunking, and distance metrics.\n\n### Quick Start\n\nInstall agentscope first, and then the MilvusLite dependency:\n\n```bash\n# In MacOS/Linux\npip install pymilvus\\[milvus_lite\\]\n\n# In Windows\npip install pymilvus[milvus_lite]\n```\n\nRun the example script, which showcases adding, searching with/without filters in MilvusLite vector store:\n\n```bash\npython milvuslite_store.py\n```\n\n> **Note:** The script creates `.db` files in the current directory. You can delete them after testing.\n\n## Usage\n\n### Initialize Store\n```python\nfrom agentscope.rag import MilvusLiteStore\n\nstore = MilvusLiteStore(\n    uri=\"./milvus_test.db\",\n    collection_name=\"test_collection\",\n    dimensions=768,              # Match your embedding model\n    distance=\"COSINE\",           # COSINE, L2, or IP\n)\n```\n\n### Add Documents\n\n```python\nfrom agentscope.rag import Document, DocMetadata\nfrom agentscope.message import TextBlock\n\ndoc = Document(\n    metadata=DocMetadata(\n        content=TextBlock(type=\"text\", text=\"Your document text\"),\n        doc_id=\"doc_1\",\n        chunk_id=0,\n        total_chunks=1,\n    ),\n    embedding=[0.1, 0.2, ...],  # Your embedding vector\n)\n\nawait store.add([doc])\n```\n\n### Search\n\n```python\nresults = await store.search(\n    query_embedding=[0.15, 0.25, ...],\n    limit=5,\n    score_threshold=0.9,                # Optional\n    filter='doc_id like \"prefix%\"',     # Optional\n)\n```\n\n### Delete\n\n```python\nawait store.delete(filter_expr='doc_id == \"doc_1\"')\n```\n\n## Distance Metrics\n\n| Metric | Description | Best For |\n|--------|-------------|----------|\n| **COSINE** | Cosine similarity | Text embeddings (recommended) |\n| **L2** | Euclidean distance | Spatial data |\n| **IP** | Inner Product | Recommendation systems |\n\n## Filter Expressions\n\n```python\n# Exact match\nfilter='doc_id == \"doc_1\"'\n\n# Pattern matching\nfilter='doc_id like \"prefix%\"'\n\n# Numeric and logical operators\nfilter='chunk_id >= 0 and total_chunks > 1'\n```\n\n## Advanced Usage\n\n### Access Underlying Client\n```python\nclient = store.get_client()\nstats = client.get_collection_stats(collection_name=\"test_collection\")\n```\n\n### Document Metadata\n- `content`: Text content (TextBlock)\n- `doc_id`: Unique document identifier\n- `chunk_id`: Chunk position (0-indexed)\n- `total_chunks`: Total chunks in document\n\n## FAQ\n\n**What embedding dimension should I use?**\nMatch your embedding model's output dimension (e.g., 768 for BERT, 1536 for OpenAI ada-002).\n\n**Can I change the distance metric after creation?**\nNo, create a new collection with the desired metric.\n\n**How do I delete the database?**\nDelete the `.db` file specified in the `uri` parameter.\n\n**Is this suitable for production?**\nMilvusLite works well for development and small-scale applications. For production at scale, consider Milvus standalone or cluster mode.\n\n## References\n\n- [Milvus Documentation](https://milvus.io/docs)\n- [AgentScope RAG Tutorial](https://doc.agentscope.io/tutorial/task_rag.html)\n"
  },
  {
    "path": "examples/functionality/vector_store/milvus_lite/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Example of using MilvusLiteStore in AgentScope RAG system.\"\"\"\nimport asyncio\nfrom agentscope.rag import (\n    MilvusLiteStore,\n    Document,\n    DocMetadata,\n)\nfrom agentscope.message import TextBlock\n\n\nasync def example_basic_operations() -> None:\n    \"\"\"The example of basic CRUD operations with MilvusLiteStore.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test 1: Basic CRUD Operations\")\n    print(\"=\" * 60)\n\n    # Initialize MilvusLiteStore with a local file\n    store = MilvusLiteStore(\n        uri=\"./milvus_test.db\",\n        collection_name=\"test_collection\",\n        dimensions=4,  # Small dimension for testing\n        distance=\"COSINE\",\n    )\n\n    print(\"✓ MilvusLiteStore initialized\")\n\n    # Create test documents with embeddings\n    test_docs = [\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(\n                    text=\"Artificial Intelligence is the future\",\n                ),\n                doc_id=\"doc_1\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.1, 0.2, 0.3, 0.4],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Machine Learning is a subset of AI\"),\n                doc_id=\"doc_2\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.2, 0.3, 0.4, 0.5],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Deep Learning uses neural networks\"),\n                doc_id=\"doc_3\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.3, 0.4, 0.5, 0.6],\n        ),\n    ]\n\n    # Test add operation\n    await store.add(test_docs)\n    print(f\"✓ Added {len(test_docs)} documents to the store\")\n\n    # Test search operation\n    query_embedding = [0.15, 0.25, 0.35, 0.45]\n    results = await store.search(\n        query_embedding=query_embedding,\n        limit=2,\n    )\n\n    print(f\"\\n✓ Search completed, found {len(results)} results:\")\n    for i, result in enumerate(results, 1):\n        print(f\"  {i}. Score: {result.score:.4f}\")\n        print(f\"     Content: {result.metadata.content}\")\n        print(f\"     Doc ID: {result.metadata.doc_id}\")\n\n    # Test search with score threshold\n    results_filtered = await store.search(\n        query_embedding=query_embedding,\n        limit=5,\n        score_threshold=0.9,\n    )\n    print(f\"\\n✓ Search with threshold (>0.9): {len(results_filtered)} results\")\n\n    # Test delete operation\n    # Note: We need to use filter expression to delete by doc_id\n    await store.delete(filter='doc_id == \"doc_2\"')\n    print(\"\\n✓ Deleted document with doc_id='doc_2'\")\n\n    # Verify deletion\n    results_after_delete = await store.search(\n        query_embedding=query_embedding,\n        limit=5,\n    )\n    print(f\"✓ After deletion: {len(results_after_delete)} documents remain\")\n\n    # Get client for advanced operations\n    client = store.get_client()\n    print(f\"\\n✓ Got MilvusClient: {type(client).__name__}\")\n\n\nasync def example_filter_search() -> None:\n    \"\"\"The example of search with metadata filtering.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test 2: Search with Metadata Filtering\")\n    print(\"=\" * 60)\n\n    store = MilvusLiteStore(\n        uri=\"./milvus_filter_test.db\",\n        collection_name=\"filter_collection\",\n        dimensions=4,\n        distance=\"COSINE\",\n    )\n\n    # Create documents with different categories\n    docs = [\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Python is a programming language\"),\n                doc_id=\"prog_1\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.1, 0.2, 0.3, 0.4],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(\n                    text=\"Java is used for enterprise applications\",\n                ),\n                doc_id=\"prog_2\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.2, 0.3, 0.4, 0.5],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Neural networks are used in AI\"),\n                doc_id=\"ai_1\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.3, 0.4, 0.5, 0.6],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Deep learning requires GPUs\"),\n                doc_id=\"ai_2\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.4, 0.5, 0.6, 0.7],\n        ),\n    ]\n\n    await store.add(docs)\n    print(f\"✓ Added {len(docs)} documents with different doc_id prefixes\")\n\n    # Search without filter\n    query_embedding = [0.25, 0.35, 0.45, 0.55]\n    all_results = await store.search(\n        query_embedding=query_embedding,\n        limit=4,\n    )\n    print(f\"\\n✓ Search without filter: {len(all_results)} results\")\n    for i, result in enumerate(all_results, 1):\n        doc_id = result.metadata.doc_id\n        score = result.score\n        print(f\"  {i}. Doc ID: {doc_id}, Score: {score:.4f}\")\n\n    # Search with filter for programming docs\n    prog_results = await store.search(\n        query_embedding=query_embedding,\n        limit=4,\n        filter='doc_id like \"prog%\"',\n    )\n    filter_msg = \"doc_id like 'prog%'\"\n    print(f\"\\n✓ Search with filter ({filter_msg}): {len(prog_results)}\")\n    for i, result in enumerate(prog_results, 1):\n        doc_id = result.metadata.doc_id\n        score = result.score\n        print(f\"  {i}. Doc ID: {doc_id}, Score: {score:.4f}\")\n\n    # Search with filter for AI docs\n    ai_results = await store.search(\n        query_embedding=query_embedding,\n        limit=4,\n        filter='doc_id like \"ai%\"',\n    )\n    filter_msg = \"doc_id like 'ai%'\"\n    print(f\"\\n✓ Search with filter ({filter_msg}): {len(ai_results)}\")\n    for i, result in enumerate(ai_results, 1):\n        doc_id = result.metadata.doc_id\n        score = result.score\n        print(f\"  {i}. Doc ID: {doc_id}, Score: {score:.4f}\")\n\n\nasync def example_multiple_chunks() -> None:\n    \"\"\"The example of documents with multiple chunks.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test 3: Documents with Multiple Chunks\")\n    print(\"=\" * 60)\n\n    store = MilvusLiteStore(\n        uri=\"./milvus_chunks_test.db\",\n        collection_name=\"chunks_collection\",\n        dimensions=4,\n        distance=\"COSINE\",\n    )\n\n    # Create a document split into multiple chunks\n    chunks = [\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Chapter 1: Introduction to AI\"),\n                doc_id=\"book_1\",\n                chunk_id=0,\n                total_chunks=3,\n            ),\n            embedding=[0.1, 0.2, 0.3, 0.4],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Chapter 2: Machine Learning Basics\"),\n                doc_id=\"book_1\",\n                chunk_id=1,\n                total_chunks=3,\n            ),\n            embedding=[0.2, 0.3, 0.4, 0.5],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Chapter 3: Deep Learning Advanced\"),\n                doc_id=\"book_1\",\n                chunk_id=2,\n                total_chunks=3,\n            ),\n            embedding=[0.3, 0.4, 0.5, 0.6],\n        ),\n    ]\n\n    await store.add(chunks)\n    print(f\"✓ Added document with {len(chunks)} chunks\")\n\n    # Search and verify chunk information\n    query_embedding = [0.2, 0.3, 0.4, 0.5]\n    results = await store.search(\n        query_embedding=query_embedding,\n        limit=3,\n    )\n\n    print(\"\\n✓ Search results for multi-chunk document:\")\n    for i, result in enumerate(results, 1):\n        chunk_info = (\n            f\"{result.metadata.chunk_id}/{result.metadata.total_chunks}\"\n        )\n        print(f\"  {i}. Chunk {chunk_info}\")\n        print(f\"     Content: {result.metadata.content}\")\n        print(f\"     Score: {result.score:.4f}\")\n\n\nasync def example_distance_metrics() -> None:\n    \"\"\"The example of different distance metrics.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test 4: Different Distance Metrics\")\n    print(\"=\" * 60)\n\n    # Test with different metrics\n    metrics = [\"COSINE\", \"L2\", \"IP\"]\n\n    for metric in metrics:\n        print(f\"\\n--- Testing {metric} metric ---\")\n        store = MilvusLiteStore(\n            uri=f\"./milvus_{metric.lower()}_test.db\",\n            collection_name=f\"{metric.lower()}_collection\",\n            dimensions=4,\n            distance=metric,\n        )\n\n        docs = [\n            Document(\n                metadata=DocMetadata(\n                    content=TextBlock(text=f\"Test doc for {metric}\"),\n                    doc_id=f\"doc_{metric}_1\",\n                    chunk_id=0,\n                    total_chunks=1,\n                ),\n                embedding=[0.1, 0.2, 0.3, 0.4],\n            ),\n        ]\n\n        await store.add(docs)\n        results = await store.search(\n            query_embedding=[0.1, 0.2, 0.3, 0.4],\n            limit=1,\n        )\n\n        print(f\"✓ {metric} metric: Score = {results[0].score:.4f}\")\n\n\nasync def main() -> None:\n    \"\"\"Run all example.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"MilvusLiteStore Comprehensive Test Suite\")\n    print(\"=\" * 60)\n\n    try:\n        await example_basic_operations()\n        await example_filter_search()\n        await example_multiple_chunks()\n        await example_distance_metrics()\n\n        print(\"\\n\" + \"=\" * 60)\n        print(\"✓ All tests completed successfully!\")\n        print(\"=\" * 60)\n\n    except Exception as e:\n        print(f\"\\n✗ Test failed with error: {e}\")\n        import traceback\n\n        traceback.print_exc()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/vector_store/mongodb/README.md",
    "content": "# MongoDB Vector Store\n\nThis example demonstrates how to use **MongoDBStore** for vector storage and semantic search in AgentScope using MongoDB's Vector Search capabilities.\nIt includes comprehensive test scenarios covering CRUD operations, metadata filtering, document chunking, and distance metrics.\n\n### Quick Start\n\nInstall agentscope first, and then the MongoDB dependency:\n\n```bash\npip install pymongo\n```\n\n**Important:** Before running the example, you need to set the `MONGODB_HOST`\nenvironment variable with your MongoDB connection string:\n\n```bash\n# For local MongoDB\nexport MONGODB_HOST=\"mongodb://localhost:27017/?directConnection=true\"\n\n# For MongoDB Atlas (replace with your connection string)\n# export MONGODB_HOST=${YOUR_MONGODB_HOST}\n```\n\nRun the example script, which showcases adding, searching, and deleting in MongoDB vector store:\n\n```bash\npython main.py\n```\n\n> **Note:** The script connects to MongoDB Atlas or local MongoDB instance. Make sure you have a valid MongoDB connection string.\n\n## Prerequisites\n\n- Confirm your MongoDB instance supports Vector Search functionality\n- Valid MongoDB connection string (local or Atlas)\n\n## Usage\n\n### Initialize Store\n\n```python\nfrom agentscope.rag import MongoDBStore\n\n# For MongoDB Atlas\nstore = MongoDBStore(\n    host=\"mongodb+srv://username:password@cluster.mongodb.net/\",\n    db_name=\"test_db\",\n    collection_name=\"test_collection\",\n    dimensions=768,              # Match your embedding model\n    distance=\"cosine\",           # cosine, euclidean, or dotProduct\n)\n\n# For local MongoDB\nstore = MongoDBStore(\n    host=\"mongodb://localhost:27017/?directConnection=true\",\n    db_name=\"test_db\",\n    collection_name=\"test_collection\",\n    dimensions=768,\n    distance=\"cosine\",\n)\n\n# To enable filtering in search, specify filter_fields:\nstore = MongoDBStore(\n    host=\"mongodb://localhost:27017/?directConnection=true\",\n    db_name=\"test_db\",\n    collection_name=\"test_collection\",\n    dimensions=768,\n    distance=\"cosine\",\n    filter_fields=[\"payload.doc_id\", \"payload.chunk_id\"],  # Fields for filtering\n)\n\n# No manual initialization needed - everything is automatic!\n# Database, collection, and vector search index are created automatically\n# when you first call add() or search()\n```\n\n### Add Documents\n\n```python\nfrom agentscope.rag import Document, DocMetadata\nfrom agentscope.message import TextBlock\n\ndoc = Document(\n    metadata=DocMetadata(\n        content=TextBlock(type=\"text\", text=\"Your document text\"),\n        doc_id=\"doc_1\",\n        chunk_id=0,\n        total_chunks=1,\n    ),\n    embedding=[0.1, 0.2, ...],  # Your embedding vector\n)\n\nawait store.add([doc])\n```\n\n### Search\n\n```python\nresults = await store.search(\n    query_embedding=[0.15, 0.25, ...],\n    limit=5,\n    score_threshold=0.9,                                # Optional\n    filter={\"payload.doc_id\": {\"$in\": [\"doc_1\", \"doc_2\"]}},  # Optional filter\n)\n# Note:\n# - To use filter, the field must be declared in filter_fields when creating store\n# - MongoDB $vectorSearch filter supports: $gt, $gte, $lt, $lte,\n#   $eq, $ne, $in, $nin, $exists, $not (NOT $regex)\n```\n\n### Delete\n\n```python\n# Delete by document IDs (no initialization needed)\nawait store.delete(ids=[\"doc_1\", \"doc_2\"])\n\n# Delete entire collection (use with caution)\nawait store.delete_collection()\n\n# Delete entire database (use with caution)\nawait store.delete_database()\n```\n\n## Distance Metrics\n\n| Metric | Description | Best For |\n|--------|-------------|----------|\n| **cosine** | Cosine similarity | Text embeddings (recommended) |\n| **euclidean** | Euclidean distance | Spatial data |\n| **dotProduct** | Inner Product | Recommendation systems |\n\n## Advanced Usage\n\n### Access Underlying Client\n\n```python\nclient = store.get_client()\n# Use MongoDB client for advanced operations\nstats = await client[store.db_name].command(\"collStats\", store.collection_name)\n```\n\n### Document Metadata\n\n- `content`: Text content (TextBlock)\n- `doc_id`: Unique document identifier\n- `chunk_id`: Chunk position (0-indexed)\n- `total_chunks`: Total chunks in document\n\n### Vector Search Index\n\nMongoDBStore automatically creates vector search indexes with the following configuration:\n\n```python\n{\n    \"fields\": [\n        {\n            \"type\": \"vector\",\n            \"path\": \"vector\",\n            \"similarity\": \"cosine\",  # or euclidean, dotProduct\n            \"numDimensions\": 768\n        }\n    ]\n}\n```\n\n## Connection Examples\n\n### MongoDB Atlas\n\n```python\nstore = MongoDBStore(\n    host=\"<YOUR_MONGO_ATLAS_CONNECTION_STRING>\",\n    db_name=\"production_db\",\n    collection_name=\"documents\",\n    dimensions=1536,\n    distance=\"cosine\",\n)\n```\n\n### Local MongoDB\n\n#### Without Authentication\n\n```python\nstore = MongoDBStore(\n    host=\"mongodb://localhost:27017?directConnection=true\",\n    db_name=\"local_db\",\n    collection_name=\"test_collection\",\n    dimensions=768,\n    distance=\"cosine\",\n)\n```\n\n#### With Authentication\n\n```python\nstore = MongoDBStore(\n    host=\"mongodb://user:pass@localhost:27017/?directConnection=true\",\n    db_name=\"test_db\",\n    collection_name=\"test_collection\",\n    dimensions=768,\n    distance=\"cosine\",\n)\n```\n\n## References\n\n- [MongoDB Vector Search Documentation](https://www.mongodb.com/docs/atlas/atlas-search/vector-search/)\n- [MongoDB Atlas Documentation](https://www.mongodb.com/docs/atlas/)\n- [AgentScope RAG Tutorial](https://doc.agentscope.io/tutorial/task_rag.html)\n"
  },
  {
    "path": "examples/functionality/vector_store/mongodb/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Example of using MongoDBStore in AgentScope RAG system.\"\"\"\nimport asyncio\nimport os\n\nfrom agentscope.rag import (\n    MongoDBStore,\n    Document,\n    DocMetadata,\n)\nfrom agentscope.message import TextBlock\n\n\nasync def example_basic_operations() -> None:\n    \"\"\"The example of basic CRUD operations with MongoDBStore.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test 1: Basic CRUD Operations\")\n    print(\"=\" * 60)\n\n    # Initialize MongoDBStore with MongoDB connection\n    store = MongoDBStore(\n        host=os.getenv(\"MONGODB_HOST\"),\n        db_name=\"test_db\",\n        collection_name=\"test_collection\",\n        dimensions=4,  # Small dimension for testing\n        distance=\"cosine\",\n    )\n\n    print(\"✓ MongoDBStore initialized\")\n\n    # Create test documents with embeddings\n    test_docs = [\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(\n                    text=\"Artificial Intelligence is the future\",\n                ),\n                doc_id=\"doc_1\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.1, 0.2, 0.3, 0.4],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Machine Learning is a subset of AI\"),\n                doc_id=\"doc_2\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.2, 0.3, 0.4, 0.5],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Deep Learning uses neural networks\"),\n                doc_id=\"doc_3\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.3, 0.4, 0.5, 0.6],\n        ),\n    ]\n\n    # Test add operation (automatically creates database, collection,\n    # and index)\n    await store.add(test_docs)\n    print(f\"✓ Added {len(test_docs)} documents to the store\")\n\n    # Test search operation (automatically waits for index to be ready)\n    query_embedding = [0.15, 0.25, 0.35, 0.45]\n    results = await store.search(\n        query_embedding=query_embedding,\n        limit=2,\n    )\n\n    print(f\"\\n✓ Search completed, found {len(results)} results:\")\n    for i, result in enumerate(results, 1):\n        print(f\"  {i}. Score: {result.score:.4f}\")\n        print(f\"     Content: {result.metadata.content}\")\n        print(f\"     Doc ID: {result.metadata.doc_id}\")\n\n    # Test search with score threshold (also waits for index if needed)\n    results_filtered = await store.search(\n        query_embedding=query_embedding,\n        limit=5,\n        score_threshold=0.3,\n    )\n    print(f\"\\n✓ Search with threshold (>0.3): {len(results_filtered)} results\")\n\n    # Test delete operation (no initialization needed)\n    # Note: MongoDBStore uses ids parameter for deletion\n    await store.delete(ids=[\"doc_2\", \"doc_3\", \"doc_1\"])\n    print(\"\\n✓ Deleted documents with specified doc_ids\")\n\n    # Verify deletion (search will wait for index if needed)\n    results_after_delete = await store.search(\n        query_embedding=query_embedding,\n        limit=5,\n    )\n    print(f\"✓ After deletion: {len(results_after_delete)} documents remain\")\n\n    # Get client for advanced operations\n    client = store.get_client()\n    print(f\"\\n✓ Got MongoDB Client: {type(client).__name__}\")\n\n    await store.close()\n\n\nasync def example_filter_search() -> None:\n    \"\"\"The example of search with metadata filtering.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test 2: Search with Metadata Filtering\")\n    print(\"=\" * 60)\n\n    # To use filter in search, specify filter_fields when creating the store.\n    # These fields will be indexed for filtering in $vectorSearch.\n    store = MongoDBStore(\n        host=os.getenv(\"MONGODB_HOST\"),\n        db_name=\"filter_test_db\",\n        collection_name=\"filter_collection\",\n        dimensions=4,\n        distance=\"cosine\",\n        filter_fields=[\"payload.doc_id\"],  # Enable filtering on doc_id\n    )\n\n    # Create documents with different categories\n    docs = [\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Python is a programming language\"),\n                doc_id=\"prog_1\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.1, 0.2, 0.3, 0.4],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(\n                    text=\"Java is used for enterprise applications\",\n                ),\n                doc_id=\"prog_2\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.2, 0.3, 0.4, 0.5],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Neural networks are used in AI\"),\n                doc_id=\"ai_1\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.3, 0.4, 0.5, 0.6],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Deep learning requires GPUs\"),\n                doc_id=\"ai_2\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.4, 0.5, 0.6, 0.7],\n        ),\n    ]\n\n    # Add documents (automatically creates database, collection, and index)\n    await store.add(docs)\n    print(f\"✓ Added {len(docs)} documents with different doc_id prefixes\")\n\n    # Search without filter (automatically waits for index if needed)\n    query_embedding = [0.25, 0.35, 0.45, 0.55]\n    all_results = await store.search(\n        query_embedding=query_embedding,\n        limit=4,\n    )\n    print(f\"\\n✓ Search without filter: {len(all_results)} results\")\n    for i, result in enumerate(all_results, 1):\n        doc_id = result.metadata.doc_id\n        score = result.score\n        print(f\"  {i}. Doc ID: {doc_id}, Score: {score:.4f}\")\n\n    # Search with filter for programming docs\n    # Note: doc_id is stored in payload.doc_id in MongoDB documents\n    # MongoDB $vectorSearch filter supports: $gt, $gte, $lt, $lte, $eq, $ne,\n    # $in, $nin, $exists, $not (NOT $regex)\n    prog_results = await store.search(\n        query_embedding=query_embedding,\n        limit=4,\n        filter={\"payload.doc_id\": {\"$in\": [\"prog_1\", \"prog_2\"]}},\n    )\n    print(f\"\\n✓ Search with filter (prog docs): {len(prog_results)} results\")\n    for i, result in enumerate(prog_results, 1):\n        doc_id = result.metadata.doc_id\n        score = result.score\n        print(f\"  {i}. Doc ID: {doc_id}, Score: {score:.4f}\")\n\n    # Search with filter for AI docs\n    ai_results = await store.search(\n        query_embedding=query_embedding,\n        limit=4,\n        filter={\"payload.doc_id\": {\"$in\": [\"ai_1\", \"ai_2\"]}},\n    )\n    print(f\"\\n✓ Search with filter (ai docs): {len(ai_results)} results\")\n    for i, result in enumerate(ai_results, 1):\n        doc_id = result.metadata.doc_id\n        score = result.score\n        print(f\"  {i}. Doc ID: {doc_id}, Score: {score:.4f}\")\n\n    await store.close()\n\n\nasync def example_multiple_chunks() -> None:\n    \"\"\"The example of documents with multiple chunks.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test 3: Documents with Multiple Chunks\")\n    print(\"=\" * 60)\n\n    store = MongoDBStore(\n        host=os.getenv(\"MONGODB_HOST\"),\n        db_name=\"chunks_test_db\",\n        collection_name=\"chunks_collection\",\n        dimensions=4,\n        distance=\"cosine\",\n    )\n\n    # Create a document split into multiple chunks\n    chunks = [\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Chapter 1: Introduction to AI\"),\n                doc_id=\"book_1\",\n                chunk_id=0,\n                total_chunks=3,\n            ),\n            embedding=[0.1, 0.2, 0.3, 0.4],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Chapter 2: Machine Learning Basics\"),\n                doc_id=\"book_1\",\n                chunk_id=1,\n                total_chunks=3,\n            ),\n            embedding=[0.2, 0.3, 0.4, 0.5],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(text=\"Chapter 3: Deep Learning Advanced\"),\n                doc_id=\"book_1\",\n                chunk_id=2,\n                total_chunks=3,\n            ),\n            embedding=[0.3, 0.4, 0.5, 0.6],\n        ),\n    ]\n\n    # Add chunks (automatically creates database, collection, and index)\n    await store.add(chunks)\n    print(f\"✓ Added document with {len(chunks)} chunks\")\n\n    # Search and verify chunk information (automatically waits for index if\n    # needed)\n    query_embedding = [0.2, 0.3, 0.4, 0.5]\n    results = await store.search(\n        query_embedding=query_embedding,\n        limit=3,\n    )\n\n    print(\"\\n✓ Search results for multi-chunk document:\")\n    for i, result in enumerate(results, 1):\n        chunk_info = (\n            f\"{result.metadata.chunk_id}/{result.metadata.total_chunks}\"\n        )\n        print(f\"  {i}. Chunk {chunk_info}\")\n        print(f\"     Content: {result.metadata.content}\")\n        print(f\"     Score: {result.score:.4f}\")\n\n    await store.close()\n\n\nasync def example_distance_metrics() -> None:\n    \"\"\"The example of different distance metrics.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test 4: Different Distance Metrics\")\n    print(\"=\" * 60)\n\n    # Test with different metrics\n    metrics = [\"cosine\", \"euclidean\", \"dotProduct\"]\n\n    for metric in metrics:\n        print(f\"\\n--- Testing {metric} metric ---\")\n        store = MongoDBStore(\n            host=os.getenv(\"MONGODB_HOST\"),\n            db_name=f\"{metric}_test_db\",\n            collection_name=f\"{metric}_collection\",\n            dimensions=4,\n            distance=metric,\n        )\n\n        docs = [\n            Document(\n                metadata=DocMetadata(\n                    content=TextBlock(text=f\"Test doc for {metric}\"),\n                    doc_id=f\"doc_{metric}_1\",\n                    chunk_id=0,\n                    total_chunks=1,\n                ),\n                embedding=[0.1, 0.2, 0.3, 0.4],\n            ),\n        ]\n\n        # Add and search (automatically creates database/collection/index\n        # and waits for index)\n        await store.add(docs)\n        results = await store.search(\n            query_embedding=[0.1, 0.2, 0.3, 0.4],\n            limit=1,\n        )\n\n        print(f\"✓ {metric} metric: Score = {results[0].score:.4f}\")\n\n        await store.close()\n\n\nasync def main() -> None:\n    \"\"\"Run all example.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"MongoDBStore Comprehensive Test Suite\")\n    print(\"=\" * 60)\n\n    try:\n        # await example_basic_operations()\n        # await example_filter_search()\n        # await example_multiple_chunks()\n        await example_distance_metrics()\n\n        print(\"\\n\" + \"=\" * 60)\n        print(\"✓ All tests completed successfully!\")\n        print(\"=\" * 60)\n\n    except Exception as e:\n        print(f\"\\n✗ Test failed with error: {e}\")\n        import traceback\n\n        traceback.print_exc()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/functionality/vector_store/oceanbase/README.md",
    "content": "# OceanBase Vector Store\n\nThis example demonstrates how to use **OceanBaseStore** for vector storage and semantic search in AgentScope.\nIt includes CRUD operations, metadata filtering, document chunking, and distance metric tests.\n\n### Quick Start\n\nInstall dependencies (including `pyobvector`):\n\n```bash\npip install -e .[full]\n```\n\nStart seekdb (a minimal OceanBase-compatible instance):\n\n```bash\ndocker run -d -p 2881:2881 oceanbase/seekdb\n```\n\nRun the example script:\n\n```bash\npython main.py\n```\n\n> **Note:** The script defaults to `127.0.0.1:2881`, user `root`, database `test`.\n> If you use a multi-tenant OceanBase account (e.g., `root@test`), override via environment variables.\n\n## Usage\n\n### Initialize Store\n\n```python\nfrom agentscope.rag import OceanBaseStore\n\nstore = OceanBaseStore(\n    collection_name=\"test_collection\",\n    dimensions=768,\n    distance=\"COSINE\",\n    uri=\"127.0.0.1:2881\",\n    user=\"root\",\n    password=\"\",\n    db_name=\"test\",\n)\n```\n\n### Add Documents\n\n```python\nfrom agentscope.rag import Document, DocMetadata\nfrom agentscope.message import TextBlock\n\ndoc = Document(\n    metadata=DocMetadata(\n        content=TextBlock(type=\"text\", text=\"Your document text\"),\n        doc_id=\"doc_1\",\n        chunk_id=0,\n        total_chunks=1,\n    ),\n    embedding=[0.1, 0.2, 0.3],\n)\n\nawait store.add([doc])\n```\n\n### Search\n\n```python\nresults = await store.search(\n    query_embedding=[0.1, 0.2, 0.3],\n    limit=5,\n    score_threshold=0.9,\n)\n```\n\n### Filter Search\n\n```python\nclient = store.get_client()\ntable = client.load_table(collection_name=\"test_collection\")\n\nresults = await store.search(\n    query_embedding=[0.1, 0.2, 0.3],\n    limit=5,\n    flter=[table.c[\"doc_id\"].like(\"doc%\")],\n)\n```\n\n> Note: The parameter name is `flter` (missing the \"i\") to avoid clashing with\n> Python's built-in `filter` and follows the underlying library's convention.\n\n### Delete\n\n```python\nclient = store.get_client()\ntable = client.load_table(collection_name=\"test_collection\")\n\nawait store.delete(where=[table.c[\"doc_id\"] == \"doc_1\"])\n```\n\n## Distance Metrics\n\n| Metric | Description | Best For |\n|--------|-------------|----------|\n| **COSINE** | Cosine similarity | Text embeddings (recommended) |\n| **L2** | Euclidean distance | Spatial data |\n| **IP** | Inner product | Recommendation systems |\n\n## Filter Expressions\n\nBuild filters using SQLAlchemy expressions and pass them via `flter`:\n\n```python\ntable = store.get_client().load_table(\"test_collection\")\n\nfilters = [\n    table.c[\"doc_id\"] == \"doc_1\",\n    table.c[\"doc_id\"].like(\"prefix%\"),\n    table.c[\"chunk_id\"] >= 0,\n]\n```\n\n## Advanced Usage\n\n### Access Underlying Client\n\n```python\nclient = store.get_client()\nstats = client.get_collection_stats(collection_name=\"test_collection\")\n```\n\n### Document Metadata\n\n- `content`: Text content (TextBlock)\n- `doc_id`: Unique document identifier\n- `chunk_id`: Chunk position (0-indexed)\n- `total_chunks`: Total chunks in document\n\n## FAQ\n\n**What embedding dimension should I use?**\nMatch your embedding model's output dimension (e.g., 768 for BERT, 1536 for OpenAI ada-002).\n\n**Can I change the distance metric after creation?**\nNo, create a new collection with the desired metric.\n\n**How do I clean up test data?**\nDrop the collection via the underlying client or remove the seekdb container volume.\n\n## Environment Variables\n\nThe script supports the following environment variables to override connection settings:\n\n```bash\nexport OCEANBASE_URI=\"127.0.0.1:2881\"\nexport OCEANBASE_USER=\"root\"\nexport OCEANBASE_PASSWORD=\"\"\nexport OCEANBASE_DB=\"test\"\n```\n\n## References\n\n- [OceanBase Vector Store](https://github.com/oceanbase/pyobvector)\n- [AgentScope RAG Tutorial](https://doc.agentscope.io/tutorial/task_rag.html)\n"
  },
  {
    "path": "examples/functionality/vector_store/oceanbase/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Example of using OceanBaseStore in AgentScope RAG system.\"\"\"\nimport asyncio\nimport os\n\nfrom agentscope.rag import (\n    OceanBaseStore,\n    Document,\n    DocMetadata,\n)\nfrom agentscope.message import TextBlock\n\n\ndef _create_store(\n    collection_name: str,\n    dimensions: int = 4,\n    distance: str = \"COSINE\",\n) -> OceanBaseStore:\n    return OceanBaseStore(\n        collection_name=collection_name,\n        dimensions=dimensions,\n        distance=distance,\n        uri=os.getenv(\"OCEANBASE_URI\", \"127.0.0.1:2881\"),\n        user=os.getenv(\"OCEANBASE_USER\", \"root\"),\n        password=os.getenv(\"OCEANBASE_PASSWORD\", \"\"),\n        db_name=os.getenv(\"OCEANBASE_DB\", \"test\"),\n    )\n\n\nasync def example_basic_operations() -> None:\n    \"\"\"The example of basic CRUD operations with OceanBaseStore.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test 1: Basic CRUD Operations\")\n    print(\"=\" * 60)\n\n    store = _create_store(collection_name=\"ob_basic_collection\")\n    store.get_client().drop_collection(\"ob_basic_collection\")\n\n    print(\"✓ OceanBaseStore initialized\")\n\n    test_docs = [\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(\n                    type=\"text\",\n                    text=\"Artificial Intelligence is the future\",\n                ),\n                doc_id=\"doc_1\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.1, 0.2, 0.3, 0.4],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(\n                    type=\"text\",\n                    text=\"Machine Learning is a subset of AI\",\n                ),\n                doc_id=\"doc_2\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.2, 0.3, 0.4, 0.5],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(\n                    type=\"text\",\n                    text=\"Deep Learning uses neural networks\",\n                ),\n                doc_id=\"doc_3\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.3, 0.4, 0.5, 0.6],\n        ),\n    ]\n\n    await store.add(test_docs)\n    print(f\"✓ Added {len(test_docs)} documents to the store\")\n\n    query_embedding = [0.15, 0.25, 0.35, 0.45]\n    results = await store.search(\n        query_embedding=query_embedding,\n        limit=2,\n    )\n\n    print(f\"\\n✓ Search completed, found {len(results)} results:\")\n    for i, result in enumerate(results, 1):\n        print(f\"  {i}. Score: {result.score:.4f}\")\n        print(f\"     Content: {result.metadata.content}\")\n        print(f\"     Doc ID: {result.metadata.doc_id}\")\n\n    results_filtered = await store.search(\n        query_embedding=query_embedding,\n        limit=5,\n        score_threshold=0.9,\n    )\n    print(f\"\\n✓ Search with threshold (>0.9): {len(results_filtered)} results\")\n\n    client = store.get_client()\n    table = client.load_table(collection_name=\"ob_basic_collection\")\n    await store.delete(where=[table.c[\"doc_id\"] == \"doc_2\"])\n    print(\"\\n✓ Deleted document with doc_id='doc_2'\")\n\n    results_after_delete = await store.search(\n        query_embedding=query_embedding,\n        limit=5,\n    )\n    print(f\"✓ After deletion: {len(results_after_delete)} documents remain\")\n\n    print(f\"\\n✓ Got MilvusLikeClient: {type(client).__name__}\")\n\n\nasync def example_filter_search() -> None:\n    \"\"\"The example of search with metadata filtering.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test 2: Search with Metadata Filtering\")\n    print(\"=\" * 60)\n\n    store = _create_store(collection_name=\"ob_filter_collection\")\n    client = store.get_client()\n    client.drop_collection(\"ob_filter_collection\")\n\n    docs = [\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(\n                    type=\"text\",\n                    text=\"Python is a programming language\",\n                ),\n                doc_id=\"prog_1\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.1, 0.2, 0.3, 0.4],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(\n                    type=\"text\",\n                    text=\"Java is used for enterprise applications\",\n                ),\n                doc_id=\"prog_2\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.2, 0.3, 0.4, 0.5],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(\n                    type=\"text\",\n                    text=\"Neural networks are used in AI\",\n                ),\n                doc_id=\"ai_1\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.3, 0.4, 0.5, 0.6],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(\n                    type=\"text\",\n                    text=\"Deep learning requires GPUs\",\n                ),\n                doc_id=\"ai_2\",\n                chunk_id=0,\n                total_chunks=1,\n            ),\n            embedding=[0.4, 0.5, 0.6, 0.7],\n        ),\n    ]\n\n    await store.add(docs)\n    print(f\"✓ Added {len(docs)} documents with different doc_id prefixes\")\n\n    query_embedding = [0.25, 0.35, 0.45, 0.55]\n    all_results = await store.search(\n        query_embedding=query_embedding,\n        limit=4,\n    )\n    print(f\"\\n✓ Search without filter: {len(all_results)} results\")\n    for i, result in enumerate(all_results, 1):\n        print(\n            f\"  {i}. Doc ID: {result.metadata.doc_id}, \"\n            f\"Score: {result.score:.4f}\",\n        )\n\n    table = client.load_table(collection_name=\"ob_filter_collection\")\n    prog_results = await store.search(\n        query_embedding=query_embedding,\n        limit=4,\n        flter=[table.c[\"doc_id\"].like(\"prog%\")],\n    )\n    print(\"\\n✓ Search with filter (doc_id like 'prog%'):\")\n    for i, result in enumerate(prog_results, 1):\n        print(\n            f\"  {i}. Doc ID: {result.metadata.doc_id}, \"\n            f\"Score: {result.score:.4f}\",\n        )\n\n    ai_results = await store.search(\n        query_embedding=query_embedding,\n        limit=4,\n        flter=[table.c[\"doc_id\"].like(\"ai%\")],\n    )\n    print(\"\\n✓ Search with filter (doc_id like 'ai%'):\")\n    for i, result in enumerate(ai_results, 1):\n        print(\n            f\"  {i}. Doc ID: {result.metadata.doc_id}, \"\n            f\"Score: {result.score:.4f}\",\n        )\n\n\nasync def example_multiple_chunks() -> None:\n    \"\"\"The example of documents with multiple chunks.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test 3: Documents with Multiple Chunks\")\n    print(\"=\" * 60)\n\n    store = _create_store(collection_name=\"ob_chunks_collection\")\n    store.get_client().drop_collection(\"ob_chunks_collection\")\n\n    chunks = [\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(\n                    type=\"text\",\n                    text=\"Chapter 1: Introduction to AI\",\n                ),\n                doc_id=\"book_1\",\n                chunk_id=0,\n                total_chunks=3,\n            ),\n            embedding=[0.1, 0.2, 0.3, 0.4],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(\n                    type=\"text\",\n                    text=\"Chapter 2: Machine Learning Basics\",\n                ),\n                doc_id=\"book_1\",\n                chunk_id=1,\n                total_chunks=3,\n            ),\n            embedding=[0.2, 0.3, 0.4, 0.5],\n        ),\n        Document(\n            metadata=DocMetadata(\n                content=TextBlock(\n                    type=\"text\",\n                    text=\"Chapter 3: Deep Learning Advanced\",\n                ),\n                doc_id=\"book_1\",\n                chunk_id=2,\n                total_chunks=3,\n            ),\n            embedding=[0.3, 0.4, 0.5, 0.6],\n        ),\n    ]\n\n    await store.add(chunks)\n    print(f\"✓ Added document with {len(chunks)} chunks\")\n\n    query_embedding = [0.2, 0.3, 0.4, 0.5]\n    results = await store.search(\n        query_embedding=query_embedding,\n        limit=3,\n    )\n\n    print(\"\\n✓ Search results for multi-chunk document:\")\n    for i, result in enumerate(results, 1):\n        chunk_info = (\n            f\"{result.metadata.chunk_id}/{result.metadata.total_chunks}\"\n        )\n        print(f\"  {i}. Chunk {chunk_info}\")\n        print(f\"     Content: {result.metadata.content}\")\n        print(f\"     Score: {result.score:.4f}\")\n\n\nasync def example_distance_metrics() -> None:\n    \"\"\"The example of different distance metrics.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test 4: Different Distance Metrics\")\n    print(\"=\" * 60)\n\n    metrics = [\"COSINE\", \"L2\", \"IP\"]\n\n    for metric in metrics:\n        print(f\"\\n--- Testing {metric} metric ---\")\n        collection_name = f\"ob_{metric}_collection\"\n        store = _create_store(\n            collection_name=collection_name,\n            distance=metric,\n        )\n        store.get_client().drop_collection(collection_name)\n\n        docs = [\n            Document(\n                metadata=DocMetadata(\n                    content=TextBlock(\n                        type=\"text\",\n                        text=f\"Test doc for {metric}\",\n                    ),\n                    doc_id=f\"doc_{metric}_1\",\n                    chunk_id=0,\n                    total_chunks=1,\n                ),\n                embedding=[0.1, 0.2, 0.3, 0.4],\n            ),\n        ]\n\n        await store.add(docs)\n        results = await store.search(\n            query_embedding=[0.1, 0.2, 0.3, 0.4],\n            limit=1,\n        )\n\n        print(f\"✓ {metric} metric: Score = {results[0].score:.4f}\")\n\n\nasync def main() -> None:\n    \"\"\"Run all example.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"OceanBaseStore Comprehensive Test Suite\")\n    print(\"=\" * 60)\n\n    try:\n        await example_basic_operations()\n        await example_filter_search()\n        await example_multiple_chunks()\n        await example_distance_metrics()\n\n        print(\"\\n\" + \"=\" * 60)\n        print(\"✓ All tests completed successfully!\")\n        print(\"=\" * 60)\n\n    except Exception as e:\n        print(f\"\\n✗ Test failed with error: {e}\")\n        import traceback\n\n        traceback.print_exc()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/game/werewolves/README.md",
    "content": "# 🐺⚔️👨‍🌾 Nine-Player Werewolves Game\n\nThis is a nine-players werewolves game example built using AgentScope, showcasing **multi-agent interactions**,\n**role-based gameplay**, and **structured output handling**.\nSpecifically, this game is consisted of\n\n- three villagers 👨‍🌾,\n- three werewolves 🐺,\n- one seer 🔮,\n- one witch 🧙‍♀️ and\n- one hunter 🏹.\n\n## ✨Changelog\n\n- 2025-10: We update the example to support more features:\n    - Allow the dead players to leave messages.\n    - Support Chinese now.\n    - Support **continuous gaming** by loading and saving session states, so the same agents can play multiple games and continue learning and optimizing their strategies.\n\n\n## QuickStart\n\nRun the following command to start the game, ensuring you have set up your DashScope API key as an environment variable.\n\n```bash\npython main.py\n```\n\n> Note:\n> - You can adjust the language, model and other parameters in `main.py`.\n> - Different models may yield different game experiences.\n\nRunning the example with AgentScope Studio provides a more interactive experience.\n\n- Demo Video in Chinese (click to play):\n\n[![Werewolf Game in Chinese](https://img.alicdn.com/imgextra/i3/6000000007235/O1CN011pK6Be23JgcdLWmLX_!!6000000007235-0-tbvideo.jpg)](https://cloud.video.taobao.com/vod/KxyR66_CWaWwu76OPTvOV2Ye1Gas3i5p4molJtzhn_s.mp4)\n\n- Demo Video in English (click to play):\n\n[![Werewolf Game in English](https://img.alicdn.com/imgextra/i3/6000000007389/O1CN011alyGK24SDcFBzHea_!!6000000007389-0-tbvideo.jpg)](https://cloud.video.taobao.com/vod/bMiRTfxPg2vm76wEoaIP2eJfkCi8CUExHRas-1LyK1I.mp4)\n\n## Details\n\nThe game is built with the ``ReActAgent`` in AgentScope, utilizing its ability to generate structured outputs to\ncontrol the game flow and interactions.\nWe also use the ``MsgHub`` and pipelines in AgentScope to manage the complex interactions like discussion and voting.\nIt's very interesting to see how agents play the werewolf game with different roles and objectives.\n\n# Advanced Usage\n\n## Change Language\n\nThe game is played in English by default. Just uncomment the following line in `game.py` to switch to Chinese.\n\n```python\n# from prompt import ChinesePrompts as Prompts\n```\n\n## Play with Agents\n\nYou can replace one of the agents with a `UserAgent` to play with AI agents.\n\n## Change Models\n\nJust modify the `model` parameter in `main.py` to try different models. Note you need to change the formatter at the same time to match the model's output format.\n\n## Enable Text-to-Speech (TTS)\n\nThe game supports Text-to-Speech functionality. To enable TTS:\n\n1. **In `main.py`**:\n   - Uncomment the import statement:\n     ```python\n     import random\n     from agentscope.tts import DashScopeTTSModel\n     ```\n   - Uncomment the `tts_model` parameter in the `get_official_agents` function:\n     ```python\n     tts_model=DashScopeTTSModel(\n         api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n         model_name=\"qwen3-tts-flash\",\n         voice=random.choice([\"Cherry\", \"Serena\", \"Ethan\", \"Chelsie\"]),\n         stream=True,\n     ),\n     ```\n\n2. **In `game.py`** (optional, for moderator TTS):\n   - Uncomment the import statement:\n     ```python\n     import random\n     from agentscope.tts import DashScopeTTSModel\n     ```\n   - Uncomment the `tts_model` parameter in the `moderator` initialization:\n     ```python\n     tts_model=DashScopeTTSModel(\n         api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n         model_name=\"qwen3-tts-flash\",\n         voice=random.choice([\"Cherry\", \"Serena\", \"Ethan\", \"Chelsie\"]),\n         stream=True,\n     ),\n     ```\n\n3. **Set up your API key**:\n   - Make sure you have set the `DASHSCOPE_API_KEY` environment variable.\n\nAfter enabling TTS, the game will synthesize speech for player messages and moderator announcements, providing a more immersive audio experience.\n\n## Further Reading\n\n- [Structured Output](https://doc.agentscope.io/tutorial/task_agent.html#structured-output)\n- [MsgHub and Pipelines](https://doc.agentscope.io/tutorial/task_pipeline.html)\n- [Prompt Formatter](https://doc.agentscope.io/tutorial/task_prompt.html)\n- [AgentScope Studio](https://doc.agentscope.io/tutorial/task_studio.html)\n- [TTS](https://doc.agentscope.io/tutorial/task_tts.html)\n"
  },
  {
    "path": "examples/game/werewolves/game.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=too-many-branches, too-many-statements, no-name-in-module\n\"\"\"A werewolf game implemented by agentscope.\"\"\"\nimport numpy as np\n\nfrom utils import (\n    majority_vote,\n    names_to_str,\n    EchoAgent,\n    MAX_GAME_ROUND,\n    MAX_DISCUSSION_ROUND,\n    Players,\n)\nfrom structured_model import (\n    DiscussionModel,\n    get_vote_model,\n    get_poison_model,\n    WitchResurrectModel,\n    get_seer_model,\n    get_hunter_model,\n)\nfrom prompt import EnglishPrompts as Prompts\n\n# Uncomment the following line to use Chinese prompts\n# from prompt import ChinesePrompts as Prompts\n\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.pipeline import (\n    MsgHub,\n    sequential_pipeline,\n    fanout_pipeline,\n)\n\n# from agentscope.tts import DashScopeTTSModel\n# import os\n# import random\n\nmoderator = EchoAgent(\n    # If you want to use TTS, uncomment the following lines and the\n    # TTS-related import statement at the beginning of this file.\n    # tts_model=DashScopeTTSModel(\n    #     api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n    #     model_name=\"qwen3-tts-flash\",\n    #     voice=random.choice([\"Cherry\", \"Serena\", \"Ethan\", \"Chelsie\"]),\n    #     stream=True,\n    # ),\n)\n\n\nasync def hunter_stage(\n    hunter_agent: ReActAgent,\n    players: Players,\n) -> str | None:\n    \"\"\"Because the hunter's stage may happen in two places: killed at night\n    or voted during the day, we define a function here to avoid duplication.\"\"\"\n    global moderator\n    msg_hunter = await hunter_agent(\n        await moderator(Prompts.to_hunter.format(name=hunter_agent.name)),\n        structured_model=get_hunter_model(players.current_alive),\n    )\n    if msg_hunter.metadata.get(\"shoot\"):\n        return msg_hunter.metadata.get(\"name\", None)\n    return None\n\n\nasync def werewolves_game(agents: list[ReActAgent]) -> None:\n    \"\"\"The main entry of the werewolf game\n\n    Args:\n        agents (`list[ReActAgent]`):\n            A list of 9 agents.\n    \"\"\"\n    assert len(agents) == 9, \"The werewolf game needs exactly 9 players.\"\n\n    # Init the players' status\n    players = Players()\n\n    # If the witch has healing and poison potion\n    healing, poison = True, True\n\n    # If it's the first day, the dead can leave a message\n    first_day = True\n\n    # Broadcast the game begin message\n    async with MsgHub(participants=agents) as greeting_hub:\n        await greeting_hub.broadcast(\n            await moderator(\n                Prompts.to_all_new_game.format(names_to_str(agents)),\n            ),\n        )\n\n    # Assign roles to the agents\n    roles = [\"werewolf\"] * 3 + [\"villager\"] * 3 + [\"seer\", \"witch\", \"hunter\"]\n    np.random.shuffle(agents)\n    np.random.shuffle(roles)\n\n    for agent, role in zip(agents, roles):\n        # Tell the agent its role\n        await agent.observe(\n            await moderator(\n                f\"[{agent.name} ONLY] {agent.name}, your role is {role}.\",\n            ),\n        )\n        players.add_player(agent, role)\n\n    # Printing the roles\n    players.print_roles()\n\n    # GAME BEGIN!\n    for _ in range(MAX_GAME_ROUND):\n        # Create a MsgHub for all players to broadcast messages\n        async with MsgHub(\n            participants=players.current_alive,\n            enable_auto_broadcast=False,  # manual broadcast only\n            name=\"alive_players\",\n        ) as alive_players_hub:\n            # Night phase\n            await alive_players_hub.broadcast(\n                await moderator(Prompts.to_all_night),\n            )\n            killed_player, poisoned_player, shot_player = None, None, None\n\n            # Werewolves discuss\n            async with MsgHub(\n                players.werewolves,\n                enable_auto_broadcast=True,\n                announcement=await moderator(\n                    Prompts.to_wolves_discussion.format(\n                        names_to_str(players.werewolves),\n                        names_to_str(players.current_alive),\n                    ),\n                ),\n                name=\"werewolves\",\n            ) as werewolves_hub:\n                # Discussion\n                n_werewolves = len(players.werewolves)\n                for _ in range(1, MAX_DISCUSSION_ROUND * n_werewolves + 1):\n                    res = await players.werewolves[_ % n_werewolves](\n                        structured_model=DiscussionModel,\n                    )\n                    if _ % n_werewolves == 0 and res.metadata.get(\n                        \"reach_agreement\",\n                    ):\n                        break\n\n                # Werewolves vote\n                # Disable auto broadcast to avoid following other's votes\n                werewolves_hub.set_auto_broadcast(False)\n                msgs_vote = await fanout_pipeline(\n                    players.werewolves,\n                    msg=await moderator(content=Prompts.to_wolves_vote),\n                    structured_model=get_vote_model(players.current_alive),\n                    enable_gather=False,\n                )\n                killed_player, votes = majority_vote(\n                    [_.metadata.get(\"vote\") for _ in msgs_vote],\n                )\n                # Postpone the broadcast of voting\n                await werewolves_hub.broadcast(\n                    [\n                        *msgs_vote,\n                        await moderator(\n                            Prompts.to_wolves_res.format(votes, killed_player),\n                        ),\n                    ],\n                )\n\n            # Witch's turn\n            await alive_players_hub.broadcast(\n                await moderator(Prompts.to_all_witch_turn),\n            )\n            msg_witch_poison = None\n            for agent in players.witch:\n                # Cannot heal witch herself\n                msg_witch_resurrect = None\n                if healing and killed_player != agent.name:\n                    msg_witch_resurrect = await agent(\n                        await moderator(\n                            Prompts.to_witch_resurrect.format(\n                                witch_name=agent.name,\n                                dead_name=killed_player,\n                            ),\n                        ),\n                        structured_model=WitchResurrectModel,\n                    )\n                    if msg_witch_resurrect.metadata.get(\"resurrect\"):\n                        killed_player = None\n                        healing = False\n\n                # Has poison potion and hasn't used the healing potion\n                if poison and not (\n                    msg_witch_resurrect\n                    and msg_witch_resurrect.metadata[\"resurrect\"]\n                ):\n                    msg_witch_poison = await agent(\n                        await moderator(\n                            Prompts.to_witch_poison.format(\n                                witch_name=agent.name,\n                            ),\n                        ),\n                        structured_model=get_poison_model(\n                            players.current_alive,\n                        ),\n                    )\n                    if msg_witch_poison.metadata.get(\"poison\"):\n                        poisoned_player = msg_witch_poison.metadata.get(\"name\")\n                        poison = False\n\n            # Seer's turn\n            await alive_players_hub.broadcast(\n                await moderator(Prompts.to_all_seer_turn),\n            )\n            for agent in players.seer:\n                msg_seer = await agent(\n                    await moderator(\n                        Prompts.to_seer.format(\n                            agent.name,\n                            names_to_str(players.current_alive),\n                        ),\n                    ),\n                    structured_model=get_seer_model(players.current_alive),\n                )\n                if msg_seer.metadata.get(\"name\"):\n                    player = msg_seer.metadata[\"name\"]\n                    await agent.observe(\n                        await moderator(\n                            Prompts.to_seer_result.format(\n                                agent_name=player,\n                                role=players.name_to_role[player],\n                            ),\n                        ),\n                    )\n\n            # Hunter's turn\n            for agent in players.hunter:\n                # If killed and not by witch's poison\n                if (\n                    killed_player == agent.name\n                    and poisoned_player != agent.name\n                ):\n                    shot_player = await hunter_stage(agent, players)\n\n            # Update alive players\n            dead_tonight = [killed_player, poisoned_player, shot_player]\n            players.update_players(dead_tonight)\n\n            # Day phase\n            if len([_ for _ in dead_tonight if _]) > 0:\n                await alive_players_hub.broadcast(\n                    await moderator(\n                        Prompts.to_all_day.format(\n                            names_to_str([_ for _ in dead_tonight if _]),\n                        ),\n                    ),\n                )\n\n                # The killed player leave a last message in first night\n                if killed_player and first_day:\n                    msg_moderator = await moderator(\n                        Prompts.to_dead_player.format(killed_player),\n                    )\n                    await alive_players_hub.broadcast(msg_moderator)\n                    # Leave a message\n                    last_msg = await players.name_to_agent[killed_player]()\n                    await alive_players_hub.broadcast(last_msg)\n\n            else:\n                await alive_players_hub.broadcast(\n                    await moderator(Prompts.to_all_peace),\n                )\n\n            # Check winning\n            res = players.check_winning()\n            if res:\n                async with MsgHub(players.all_players) as all_players_hub:\n                    res_msg = await moderator(res)\n                    await all_players_hub.broadcast(res_msg)\n                break\n\n            # Discussion\n            await alive_players_hub.broadcast(\n                await moderator(\n                    Prompts.to_all_discuss.format(\n                        names=names_to_str(players.current_alive),\n                    ),\n                ),\n            )\n            # Open the auto broadcast to enable discussion\n            alive_players_hub.set_auto_broadcast(True)\n            await sequential_pipeline(players.current_alive)\n            # Disable auto broadcast to avoid leaking info\n            alive_players_hub.set_auto_broadcast(False)\n\n            # Voting\n            msgs_vote = await fanout_pipeline(\n                players.current_alive,\n                await moderator(\n                    Prompts.to_all_vote.format(\n                        names_to_str(players.current_alive),\n                    ),\n                ),\n                structured_model=get_vote_model(players.current_alive),\n                enable_gather=False,\n            )\n            voted_player, votes = majority_vote(\n                [_.metadata.get(\"vote\") for _ in msgs_vote],\n            )\n            # Broadcast the voting messages together to avoid influencing\n            # each other\n            voting_msgs = [\n                *msgs_vote,\n                await moderator(\n                    Prompts.to_all_res.format(votes, voted_player),\n                ),\n            ]\n\n            # Leave a message if voted\n            if voted_player:\n                prompt_msg = await moderator(\n                    Prompts.to_dead_player.format(voted_player),\n                )\n                last_msg = await players.name_to_agent[voted_player](\n                    prompt_msg,\n                )\n                voting_msgs.extend([prompt_msg, last_msg])\n\n            await alive_players_hub.broadcast(voting_msgs)\n\n            # If the voted player is the hunter, he can shoot someone\n            shot_player = None\n            for agent in players.hunter:\n                if voted_player == agent.name:\n                    shot_player = await hunter_stage(agent, players)\n                    if shot_player:\n                        await alive_players_hub.broadcast(\n                            await moderator(\n                                Prompts.to_all_hunter_shoot.format(\n                                    shot_player,\n                                ),\n                            ),\n                        )\n\n            # Update alive players\n            dead_today = [voted_player, shot_player]\n            players.update_players(dead_today)\n\n            # Check winning\n            res = players.check_winning()\n            if res:\n                async with MsgHub(players.all_players) as all_players_hub:\n                    res_msg = await moderator(res)\n                    await all_players_hub.broadcast(res_msg)\n                break\n\n        # The day ends\n        first_day = False\n\n    # Game over, each player reflects\n    await fanout_pipeline(\n        agents=agents,\n        msg=await moderator(Prompts.to_all_reflect),\n    )\n"
  },
  {
    "path": "examples/game/werewolves/main.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n\"\"\"The main entry point for the werewolf game.\"\"\"\nimport asyncio\nimport os\n\nfrom game import werewolves_game\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeMultiAgentFormatter\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.session import JSONSession\n\n# import random\n# from agentscope.tts import DashScopeTTSModel\n\n\ndef get_official_agents(name: str) -> ReActAgent:\n    \"\"\"Get the official werewolves game agents.\"\"\"\n    agent = ReActAgent(\n        name=name,\n        sys_prompt=f\"\"\"You're a werewolf game player named {name}.\n\n# YOUR TARGET\nYour target is to win the game with your teammates as much as possible.\n\n# GAME RULES\n- In werewolf game, players are divided into three werewolves, three villagers, one seer, one hunter and one witch.\n    - Werewolves: kill one player each night, and must hide identity during the day.\n    - Villagers: ordinary players without special abilities, try to identify and eliminate werewolves.\n        - Seer: A special villager who can check one player's identity each night.\n        - Witch: A special villager with two one-time-use potions: a healing potion to save a player from being killed at night, and a poison to eliminate one player at night.\n        - Hunter: A special villager who can take one player down with them when they are eliminated.\n- The game alternates between night and day phases until one side wins:\n    - Night Phase\n        - Werewolves choose one victim\n        - Seer checks one player's identity\n        - Witch decides whether to use potions\n        - Moderator announces who died during the night\n    - Day Phase\n        - All players discuss and vote to eliminate one suspected player\n\n# GAME GUIDANCE\n- Try your best to win the game with your teammates, tricks, lies, and deception are all allowed, e.g. pretending to be a different role.\n- During discussion, don't be political, be direct and to the point.\n- The day phase voting provides important clues. For example, the werewolves may vote together, attack the seer, etc.\n## GAME GUIDANCE FOR WEREWOLF\n- Seer is your greatest threat, who can check one player's identity each night. Analyze players' speeches, find out the seer and eliminate him/her will greatly increase your chances of winning.\n- In the first night, making random choices is common for werewolves since no information is available.\n- Pretending to be other roles (seer, witch or villager) is a common strategy to hide your identity and mislead other villagers in the day phase.\n- The outcome of the night phase provides important clues. For example, if witch uses the healing or poison potion, if the dead player is hunter, etc. Use this information to adjust your strategy.\n## GAME GUIDANCE FOR SEER\n- Seer is very important to villagers, exposing yourself too early may lead to being targeted by werewolves.\n- Your ability to check one player's identity is crucial.\n- The outcome of the night phase provides important clues. For example, if witch uses the healing or poison potion, if the dead player is hunter, etc. Use this information to adjust your strategy.\n## GAME GUIDANCE FOR WITCH\n- Witch has two powerful potions, use them wisely to protect key villagers or eliminate suspected werewolves.\n- The outcome of the night phase provides important clues. For example, if the dead player is hunter, etc. Use this information to adjust your strategy.\n## GAME GUIDANCE FOR HUNTER\n- Using your ability in day phase will expose your role (since only hunter can take one player down)\n- The outcome of the night phase provides important clues. For example, if witch uses the healing or poison potion, etc. Use this information to adjust your strategy.\n## GAME GUIDANCE FOR VILLAGER\n- Protecting special villagers, especially the seer, is crucial for your team's success.\n- Werewolves may pretend to be the seer. Be cautious and don't trust anyone easily.\n- The outcome of the night phase provides important clues. For example, if witch uses the healing or poison potion, if the dead player is hunter, etc. Use this information to adjust your strategy.\n\n# NOTE\n- [IMPORTANT] DO NOT make up any information that is not provided by the moderator or other players.\n- This is a TEXT-based game, so DO NOT use or make up any non-textual information.\n- Always critically reflect on whether your evidence exist, and avoid making assumptions.\n- Your response should be specific and concise, provide clear reason and avoid unnecessary elaboration.\n- Generate a one-line response.\n- Don't repeat the others' speeches.\"\"\",\n        model=DashScopeChatModel(\n            api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n            model_name=\"qwen3-max\",\n        ),\n        formatter=DashScopeMultiAgentFormatter(),\n        # If you want to use TTS, uncomment the following lines and the\n        # TTS-related import statement at the beginning of this file.\n        # tts_model=DashScopeTTSModel(\n        #     api_key=os.environ.get(\"DASHSCOPE_API_KEY\"),\n        #     model_name=\"qwen3-tts-flash\",\n        #     voice=random.choice([\"Cherry\", \"Serena\", \"Ethan\", \"Chelsie\"]),\n        #     stream=True,\n        # ),\n    )\n    return agent\n\n\nasync def main() -> None:\n    \"\"\"The main entry point for the werewolf game.\"\"\"\n\n    # Uncomment the following lines if you want to use Agentscope Studio\n    # to visualize the game process.\n    # import agentscope\n    # agentscope.init(\n    #     studio_url=\"http://localhost:3000\",\n    #     project=\"werewolf_game\",\n    # )\n\n    # Prepare 9 players, you can change their names here\n    players = [get_official_agents(f\"Player{_ + 1}\") for _ in range(9)]\n\n    # Note: You can replace your own agents here, or use all your own agents\n\n    # Load states from a previous checkpoint\n    session = JSONSession(save_dir=\"./checkpoints\")\n    await session.load_session_state(\n        session_id=\"players_checkpoint\",\n        **{player.name: player for player in players},\n    )\n\n    await werewolves_game(players)\n\n    # Save the states to a checkpoint\n    await session.save_session_state(\n        session_id=\"players_checkpoint\",\n        **{player.name: player for player in players},\n    )\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/game/werewolves/prompt.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Default prompts\"\"\"\n\n\nclass EnglishPrompts:\n    \"\"\"English prompts used to guide the werewolf game.\"\"\"\n\n    to_dead_player = (\n        \"{}, you're eliminated now. Now you can make a final statement to \"\n        \"all alive players before you leave the game.\"\n    )\n\n    to_all_new_game = (\n        \"A new game is starting, the players are: {}. Now we randomly \"\n        \"reassign the roles to each player and inform them of their roles \"\n        \"privately.\"\n    )\n\n    to_all_night = (\n        \"Night has fallen, everyone close your eyes. Werewolves open your \"\n        \"eyes and choose a player to eliminate tonight.\"\n    )\n\n    to_wolves_discussion = (\n        \"[WEREWOLVES ONLY] {}, you should discuss and \"\n        \"decide on a player to eliminate tonight. Current alive players \"\n        \"are {}. Remember to set `reach_agreement` to True if you reach an \"\n        \"agreement during the discussion.\"\n    )\n\n    to_wolves_vote = \"[WEREWOLVES ONLY] Which player do you vote to kill?\"\n\n    to_wolves_res = (\n        \"[WEREWOLVES ONLY] The voting result is {}. So you have chosen to \"\n        \"eliminate {}.\"\n    )\n\n    to_all_witch_turn = (\n        \"Witch's turn, witch open your eyes and decide your action tonight...\"\n    )\n    to_witch_resurrect = (\n        \"[WITCH ONLY] {witch_name}, you're the witch, and tonight {dead_name} \"\n        \"is eliminated. You can resurrect him/her by using your healing \"\n        \"potion, \"\n        \"and note you can only use it once in the whole game. Do you want to \"\n        \"resurrect {dead_name}? Give me your reason and decision.\"\n    )\n\n    to_witch_resurrect_no = (\n        \"[WITCH ONLY] The witch has chosen not to resurrect the player.\"\n    )\n    to_witch_resurrect_yes = (\n        \"[WITCH ONLY] The witch has chosen to resurrect the player.\"\n    )\n\n    to_witch_poison = (\n        \"[WITCH ONLY] {witch_name}, as a witch, you have a one-time-use \"\n        \"poison potion, do you want to use it tonight? Give me your reason \"\n        \"and decision.\"\n    )\n\n    to_all_seer_turn = (\n        \"Seer's turn, seer open your eyes and check one player's identity \"\n        \"tonight...\"\n    )\n\n    to_seer = (\n        \"[SEER ONLY] {}, as the seer you can check one player's identity \"\n        \"tonight. Who do you want to check? Give me your reason and decision.\"\n    )\n\n    to_seer_result = (\n        \"[SEER ONLY] You've checked {agent_name}, and the result is: {role}.\"\n    )\n\n    to_hunter = (\n        \"[HUNTER ONLY] {name}, as the hunter you're eliminated tonight. You \"\n        \"can choose one player to take down with you. Also, you can choose \"\n        \"not to use this ability. Give me your reason and decision.\"\n    )\n\n    to_all_hunter_shoot = (\n        \"The hunter has chosen to shoot {} down with him/herself.\"\n    )\n\n    to_all_day = (\n        \"The day is coming, all players open your eyes. Last night, \"\n        \"the following player(s) has been eliminated: {}.\"\n    )\n\n    to_all_peace = (\n        \"The day is coming, all the players open your eyes. Last night is \"\n        \"peaceful, no player is eliminated.\"\n    )\n\n    to_all_discuss = (\n        \"Now the alive players are {names}. The game goes on, it's time to \"\n        \"discuss and vote a player to be eliminated. Now you each take turns \"\n        \"to speak once in the order of {names}.\"\n    )\n\n    to_all_vote = (\n        \"Now the discussion is over. Everyone, please vote to eliminate one \"\n        \"player from the alive players: {}.\"\n    )\n\n    to_all_res = \"The voting result is {}. So {} has been voted out.\"\n\n    to_all_wolf_win = (\n        \"There are {n_alive} players alive, and {n_werewolves} of them are \"\n        \"werewolves. \"\n        \"The game is over and werewolves win🐺🎉!\"\n        \"In this game, the true roles of all players are: {true_roles}\"\n    )\n\n    to_all_village_win = (\n        \"All the werewolves have been eliminated.\"\n        \"The game is over and villagers win🏘️🎉!\"\n        \"In this game, the true roles of all players are: {true_roles}\"\n    )\n\n    to_all_continue = \"The game goes on.\"\n\n    to_all_reflect = (\n        \"The game is over. Now each player can reflect on their performance. \"\n        \"Note each player only has one chance to speak and the reflection is \"\n        \"only visible to themselves.\"\n    )\n\n\nclass ChinesePrompts:\n    \"\"\"Chinese prompts used to guide the werewolf game.\"\"\"\n\n    to_dead_player = \"{}, 你已被淘汰。现在你可以向所有存活玩家发表最后的遗言。\"\n\n    to_all_new_game = \"新的一局游戏开始，参与玩家包括：{}。现在为每位玩家重新随机分配身份，并私下告知各自身份。\"\n\n    to_all_night = \"天黑了，请所有人闭眼。狼人请睁眼，选择今晚要淘汰的一名玩家...\"\n\n    to_wolves_discussion = (\n        \"[仅狼人可见] {}, 你们可以讨论并决定今晚要淘汰的玩家。当前存活玩家有：{}。\"\n        \"如果达成一致，请将 `reach_agreement` 设为 True。\"\n    )\n\n    to_wolves_vote = \"[仅狼人可见] 你投票要杀死哪位玩家？\"\n\n    to_wolves_res = \"[仅狼人可见] 投票结果为 {}，你们选择淘汰 {}。\"\n\n    to_all_witch_turn = \"轮到女巫行动，女巫请睁眼并决定今晚的操作...\"\n    to_witch_resurrect = (\n        \"[仅女巫可见] {witch_name}，你是女巫，今晚{dead_name}被淘汰。\"\n        \"你可以用解药救他/她，注意解药全局只能用一次。你要救{dead_name}吗？\"\n        \"请给出理由和决定。\"\n    )\n\n    to_witch_resurrect_no = \"[仅女巫可见] 女巫选择不救该玩家。\"\n    to_witch_resurrect_yes = \"[仅女巫可见] 女巫选择救活该玩家。\"\n\n    to_witch_poison = \"[仅女巫可见] {witch_name}，你有一瓶一次性毒药，今晚要使用吗？请给出理由和决定。\"\n\n    to_all_seer_turn = \"轮到预言家行动，预言家请睁眼并查验一名玩家身份...\"\n\n    to_seer = \"[仅预言家可见] {}, 你是预言家，今晚可以查验一名玩家身份。你要查谁？请给出理由和决定。\"\n\n    to_seer_result = \"[仅预言家可见] 你查验了{agent_name}，结果是：{role}。\"\n\n    to_hunter = \"[仅猎人可见] {name}，你是猎人，今晚被淘汰。你可以选择带走一名玩家，也可以选择不带走。请给出理由和决定。\"\n\n    to_all_hunter_shoot = \"猎人选择带走 {} 一起出局。\"\n\n    to_all_day = \"天亮了，请所有玩家睁眼。昨晚被淘汰的玩家有：{}。\"\n\n    to_all_peace = \"天亮了，请所有玩家睁眼。昨晚平安夜，无人被淘汰。\"\n\n    to_all_discuss = \"现在存活玩家有：{names}。游戏继续，大家开始讨论并投票淘汰一名玩家。请按顺序（{names}）依次发言。\"\n\n    to_all_vote = \"讨论结束。请大家从存活玩家中投票淘汰一人：{}。\"\n\n    to_all_res = \"投票结果为 {}，{} 被淘汰。\"\n\n    to_all_wolf_win = (\n        \"当前存活玩家共{n_alive}人，其中{n_werewolves}人为狼人。\"\n        \"游戏结束，狼人获胜🐺🎉！\"\n        \"本局所有玩家真实身份为：{true_roles}\"\n    )\n\n    to_all_village_win = \"所有狼人已被淘汰。游戏结束，村民获胜🏘️🎉！本局所有玩家真实身份为：{true_roles}\"\n\n    to_all_continue = \"游戏继续。\"\n\n    to_all_reflect = \"游戏结束。现在每位玩家可以对自己的表现进行反思。注意每位玩家只有一次发言机会，且反思内容仅自己可见。\"\n"
  },
  {
    "path": "examples/game/werewolves/structured_model.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The structured output models used in the werewolf game.\"\"\"\nfrom typing import Literal\n\nfrom pydantic import BaseModel, Field, model_validator\nfrom agentscope.agent import AgentBase\n\n\nclass DiscussionModel(BaseModel):\n    \"\"\"The output format for discussion.\"\"\"\n\n    reach_agreement: bool = Field(\n        description=\"Whether you have reached an agreement or not\",\n    )\n\n\ndef get_vote_model(agents: list[AgentBase]) -> type[BaseModel]:\n    \"\"\"Get the vote model by player names.\"\"\"\n\n    class VoteModel(BaseModel):\n        \"\"\"The vote output format.\"\"\"\n\n        vote: Literal[tuple(_.name for _ in agents)] = Field(  # type: ignore\n            description=\"The name of the player you want to vote for\",\n        )\n\n    return VoteModel\n\n\nclass WitchResurrectModel(BaseModel):\n    \"\"\"The output format for witch resurrect action.\"\"\"\n\n    resurrect: bool = Field(\n        description=\"Whether you want to resurrect the player\",\n    )\n\n\ndef get_poison_model(agents: list[AgentBase]) -> type[BaseModel]:\n    \"\"\"Get the poison model by player names.\"\"\"\n\n    class WitchPoisonModel(BaseModel):\n        \"\"\"The output format for witch poison action.\"\"\"\n\n        poison: bool = Field(\n            description=\"Do you want to use the poison potion\",\n        )\n        name: Literal[  # type: ignore\n            tuple(_.name for _ in agents)\n        ] | None = Field(\n            description=\"The name of the player you want to poison, if you \"\n            \"don't want to poison anyone, just leave it empty\",\n            default=None,\n        )\n\n        # pylint: disable=no-self-argument\n        @model_validator(mode=\"before\")\n        def clear_name_if_no_poison(cls, values: dict) -> dict:\n            \"\"\"Clear name if no poison is used.\n            This is to avoid validation errors when `poison` is false but a\n             `name` is provided.\n\n            Args:\n                values (`dict`):\n                    The input data for the model.\n            Returns:\n                `dict`:\n                    The validated and possibly modified data.\n            \"\"\"\n            if isinstance(values, dict) and not values.get(\"poison\"):\n                values[\"name\"] = None\n            return values\n\n    return WitchPoisonModel\n\n\ndef get_seer_model(agents: list[AgentBase]) -> type[BaseModel]:\n    \"\"\"Get the seer model by player names.\"\"\"\n\n    class SeerModel(BaseModel):\n        \"\"\"The output format for seer action.\"\"\"\n\n        name: Literal[tuple(_.name for _ in agents)] = Field(  # type: ignore\n            description=\"The name of the player you want to check\",\n        )\n\n    return SeerModel\n\n\ndef get_hunter_model(agents: list[AgentBase]) -> type[BaseModel]:\n    \"\"\"Get the hunter model by player agents.\"\"\"\n\n    class HunterModel(BaseModel):\n        \"\"\"The output format for hunter action.\"\"\"\n\n        shoot: bool = Field(\n            description=\"Whether you want to use the shooting ability or not\",\n        )\n        name: Literal[  # type: ignore\n            tuple(_.name for _ in agents)\n        ] | None = Field(\n            description=\"The name of the player you want to shoot, if you \"\n            \"don't want to the ability, just leave it empty\",\n            default=None,\n        )\n\n        # pylint: disable=no-self-argument\n        @model_validator(mode=\"before\")\n        def clear_name_if_no_shoot(cls, values: dict) -> dict:\n            \"\"\"If shoot is false, set name to None to skip validation.\n            This is to avoid validation errors when `shoot` is false but a\n             `name` is provided.\n\n            Args:\n                values (`dict`):\n                    The input data for the model.\n            Returns:\n                `dict`:\n                    The validated and possibly modified data.\n            \"\"\"\n            if isinstance(values, dict) and not values.get(\"shoot\"):\n                values[\"name\"] = None\n            return values\n\n    return HunterModel\n"
  },
  {
    "path": "examples/game/werewolves/utils.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Utility functions for the werewolf game.\"\"\"\nfrom collections import defaultdict\nfrom typing import Any\n\nimport numpy as np\n\nfrom prompt import EnglishPrompts as Prompts\n\nfrom agentscope.message import Msg, AudioBlock\nfrom agentscope.agent import ReActAgent, AgentBase\n\nfrom agentscope.tts import TTSModelBase\n\nMAX_GAME_ROUND = 30\nMAX_DISCUSSION_ROUND = 3\n\n\ndef majority_vote(votes: list[str]) -> tuple:\n    \"\"\"Return the vote with the most counts.\"\"\"\n    result = max(set(votes), key=votes.count)\n    names, counts = np.unique(votes, return_counts=True)\n    conditions = \", \".join(\n        [f\"{name}: {count}\" for name, count in zip(names, counts)],\n    )\n    return result, conditions\n\n\ndef names_to_str(agents: list[str] | list[ReActAgent]) -> str:\n    \"\"\"Return a string of agent names.\"\"\"\n    if not agents:\n        return \"\"\n\n    if len(agents) == 1:\n        if isinstance(agents[0], ReActAgent):\n            return agents[0].name\n        return agents[0]\n\n    names = []\n    for agent in agents:\n        if isinstance(agent, ReActAgent):\n            names.append(agent.name)\n        else:\n            names.append(agent)\n    return \", \".join([*names[:-1], \"and \" + names[-1]])\n\n\nclass EchoAgent(AgentBase):\n    \"\"\"Echo agent that repeats the input message.\"\"\"\n\n    def __init__(self, tts_model: TTSModelBase | None = None) -> None:\n        \"\"\"Initialize the echo agent.\"\"\"\n        super().__init__()\n        self.name = \"Moderator\"\n        self.tts_model = tts_model\n\n    async def reply(self, content: str) -> Msg:\n        \"\"\"Repeat the input content with its name and role.\"\"\"\n\n        msg = Msg(\n            self.name,\n            content,\n            role=\"assistant\",\n        )\n        speech: AudioBlock | list[AudioBlock] | None = None\n        if self.tts_model:\n            tts_res = await self.tts_model.synthesize(msg)\n            if self.tts_model.stream:\n                async for tts_chunk in tts_res:\n                    speech = tts_chunk.content\n                    await self.print(msg, False, speech=speech)\n            else:\n                speech = tts_res.content\n        await self.print(msg, True, speech=speech)\n        return msg\n\n    async def handle_interrupt(\n        self,\n        *args: Any,\n        **kwargs: Any,\n    ) -> Msg:\n        \"\"\"Handle interrupt.\"\"\"\n\n    async def observe(self, msg: Msg | list[Msg] | None) -> None:\n        \"\"\"Observe the user's message.\"\"\"\n\n\nclass Players:\n    \"\"\"Maintain the players' status.\"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"Initialize the players.\"\"\"\n        # The mapping from player name to role\n        self.name_to_role = {}\n        self.role_to_names = defaultdict(list)\n        self.name_to_agent = {}\n        self.werewolves = []\n        self.villagers = []\n        self.seer = []\n        self.hunter = []\n        self.witch = []\n        self.current_alive = []\n        self.all_players = []\n\n    def add_player(self, player: ReActAgent, role: str) -> None:\n        \"\"\"Add a player to the game.\n\n        Args:\n            player (`ReActAgent`):\n                The player to be added.\n            role (`str`):\n                The role of the player.\n        \"\"\"\n        self.name_to_role[player.name] = role\n        self.name_to_agent[player.name] = player\n        self.role_to_names[role].append(player.name)\n        self.all_players.append(player)\n        if role == \"werewolf\":\n            self.werewolves.append(player)\n        elif role == \"villager\":\n            self.villagers.append(player)\n        elif role == \"seer\":\n            self.seer.append(player)\n        elif role == \"hunter\":\n            self.hunter.append(player)\n        elif role == \"witch\":\n            self.witch.append(player)\n        else:\n            raise ValueError(f\"Unknown role: {role}\")\n        self.current_alive.append(player)\n\n    def update_players(self, dead_players: list[ReActAgent]) -> None:\n        \"\"\"Update the current alive players.\n\n        Args:\n            dead_players (`list[ReActAgent]`):\n                A list of dead players to be removed.\n        \"\"\"\n        self.werewolves = [\n            _ for _ in self.werewolves if _.name not in dead_players\n        ]\n        self.villagers = [\n            _ for _ in self.villagers if _.name not in dead_players\n        ]\n        self.seer = [_ for _ in self.seer if _.name not in dead_players]\n        self.hunter = [_ for _ in self.hunter if _.name not in dead_players]\n        self.witch = [_ for _ in self.witch if _.name not in dead_players]\n        self.current_alive = [\n            _ for _ in self.current_alive if _.name not in dead_players\n        ]\n\n    def print_roles(self) -> None:\n        \"\"\"Print the roles of all players.\"\"\"\n        print(\"Roles:\")\n        for name, role in self.name_to_role.items():\n            print(f\" - {name}: {role}\")\n\n    def check_winning(self) -> str | None:\n        \"\"\"Check if the game is over and return the winning message.\"\"\"\n\n        # Prepare true roles string\n        true_roles = (\n            f'{names_to_str(self.role_to_names[\"werewolf\"])} are werewolves, '\n            f'{names_to_str(self.role_to_names[\"villager\"])} are villagers, '\n            f'{names_to_str(self.role_to_names[\"seer\"])} is the seer, '\n            f'{names_to_str(self.role_to_names[\"hunter\"])} is the hunter, '\n            f'and {names_to_str(self.role_to_names[\"witch\"])} is the witch.'\n        )\n\n        if len(self.werewolves) * 2 >= len(self.current_alive):\n            return Prompts.to_all_wolf_win.format(\n                n_alive=len(self.current_alive),\n                n_werewolves=len(self.werewolves),\n                true_roles=true_roles,\n            )\n        if self.current_alive and not self.werewolves:\n            return Prompts.to_all_village_win.format(\n                true_roles=true_roles,\n            )\n        return None\n"
  },
  {
    "path": "examples/integration/alibabacloud_api_mcp/README.md",
    "content": "# Connect AlibabaCloud API MCP Server Example\n\n## What This Example Demonstrates\n\nThis use case shows how to use OAuth login in agentscope to connect to the Alibaba Cloud API MCP server.\n\nAlibaba Cloud is a world-leading cloud computing and artificial intelligence technology company, committed to providing one-stop cloud computing services and middleware for enterprises and developers.\n\nAlibaba Cloud API MCP Server provides MCP-based access to nearly all of Alibaba Cloud's OpenAPIs. You can create and optimize them without coding at <https://api.aliyun.com/mcp>.\n\nFor example, you can add the ECS service's price query interfaces DescribePrice and CreateInstance, DescribeImages to a custom MCP service. This allows you to obtain a remote MCP address without any code configuration. Using the agent scope, you can query prices and place orders from the agent.In addition to supporting atomic OpenAPI, it also supports encapsulating Terraform HCL as a remote tool to achieve deterministic orchestration.\n\nAfter adding the sample MCP, you can use queries similar to the following:\n1. Find the lowest-priced ECS instance in the Hangzhou region;\n2. Create an instance with the lowest price and lowest specifications in Hangzhou.\n\n\n## Prerequisites\n\n- Python 3.10 or higher\n- Python package asyncio, webbrowser\n- Node.js and npm (for the MCP server)\n- AlibabaCloud API MCP Server connect address [Alibaba Cloud API MCP Server console](https://api.aliyun.com/mcp)\n\n## How to Run This Example\n\n**Edit main.py**\n\n```python\n# openai base\n# read from .env\nload_dotenv()\n\nserver_url = \"https://openapi-mcp.cn-hangzhou.aliyuncs.com/accounts/14******/custom/****/id/KXy******/mcp\"\n```\n\n\nYou need to create your own MCP SERVER from https://api.aliyun.com/mcp and replace the link here. Please choose an address that uses the streamable HTTP protocol.\n\n\n**Run the script**:\n```bash\npython main.py\n```\n\n## Video example\n\n<https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250911/otcfsk/AgentScope+%E9%9B%86%E6%88%90+OpenAPI+MCP+Server%28%E8%87%AA%E7%84%B6%E8%AF%AD%E8%A8%80%E5%88%9B%E5%BB%BA+ECS%29.mp4>\n\nThis video demonstrates how to complete the configuration in the agent scope using the Alibaba Cloud API MCP SERVER service. After logging in through OAuth, users can create an ECS instance using natural language."
  },
  {
    "path": "examples/integration/alibabacloud_api_mcp/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The main entry point of the ReAct agent example.\"\"\"\n\nimport asyncio\nimport os\n\nfrom dotenv import load_dotenv\nfrom mcp.client.auth import (\n    OAuthClientProvider,\n)\nfrom mcp.shared.auth import (\n    OAuthClientMetadata,\n)\nfrom pydantic import AnyUrl\n\nfrom oauth_handler import (\n    InMemoryTokenStorage,\n    handle_callback,\n    handle_redirect,\n)\n\nfrom agentscope.agent import ReActAgent, UserAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.mcp import HttpStatelessClient\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.tool import Toolkit\n\nload_dotenv()\n\n# Fetch the MCP endpoint from https://api.aliyun.com/mcp after provisioning.\nserver_url = (\n    \"https://openapi-mcp.cn-hangzhou.aliyuncs.com/accounts/14******/custom/\"\n    \"****/id/KXy******/mcp\"\n)\n\nmemory_token_storage = InMemoryTokenStorage()\n\noauth_provider = OAuthClientProvider(\n    server_url=server_url,\n    client_metadata=OAuthClientMetadata(\n        client_name=\"AgentScopeExampleClient\",\n        redirect_uris=[AnyUrl(\"http://localhost:3000/callback\")],\n        grant_types=[\"authorization_code\", \"refresh_token\"],\n        response_types=[\"code\"],\n        scope=None,\n    ),\n    storage=memory_token_storage,\n    redirect_handler=handle_redirect,\n    callback_handler=handle_callback,\n)\n\nstateless_client = HttpStatelessClient(\n    # Name used to identify the MCP\n    name=\"mcp_services_stateless\",\n    transport=\"streamable_http\",\n    url=server_url,\n    auth=oauth_provider,\n)\n\n\ndef require_env_var(name: str) -> str:\n    \"\"\"Return the value of *name* or raise a helpful error.\"\"\"\n    value = os.environ.get(name)\n    if value is None:\n        raise RuntimeError(f\"Environment variable '{name}' must be set.\")\n    return value\n\n\nasync def main() -> None:\n    \"\"\"The main entry point for the ReAct agent example.\"\"\"\n    toolkit = Toolkit()\n    await toolkit.register_mcp_client(stateless_client)\n\n    agent = ReActAgent(\n        name=\"AlibabaCloudOpsAgent\",\n        sys_prompt=(\n            \"You are an Alibaba Cloud operations assistant. \"\n            \"Use ECS, RDS, VPC, and other services to satisfy requests.\"\n        ),\n        model=DashScopeChatModel(\n            api_key=require_env_var(\"DASHSCOPE_API_KEY\"),\n            model_name=\"qwen3-max-preview\",\n            enable_thinking=False,\n            stream=True,\n        ),\n        formatter=DashScopeChatFormatter(),\n        toolkit=toolkit,\n        memory=InMemoryMemory(),\n    )\n    user = UserAgent(\"User\")\n\n    msg = None\n    while True:\n        msg = await user(msg)\n        if msg.get_text_content() == \"exit\":\n            break\n        msg = await agent(msg)\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/integration/alibabacloud_api_mcp/oauth_handler.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"OAuth handler utilities for the Alibaba Cloud MCP example.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport socket\nimport threading\nimport webbrowser\nfrom functools import partial\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\nfrom textwrap import dedent\nfrom urllib.parse import parse_qs, urlparse\n\nfrom mcp.client.auth import TokenStorage\nfrom mcp.shared.auth import OAuthClientInformationFull, OAuthToken\n\nSUCCESS_PAGE = dedent(\n    \"\"\"\n    <!DOCTYPE html>\n    <html lang=\"en\">\n    <head>\n        <meta charset=\"UTF-8\">\n        <title>Authorization Complete</title>\n    </head>\n    <body>\n        <h1>Authorization Complete</h1>\n        <p>You can now return to the application.</p>\n        <button onclick=\"window.close()\">Close Window</button>\n    </body>\n    </html>\n    \"\"\",\n)\n\nERROR_PAGE_TEMPLATE = dedent(\n    \"\"\"\n    <!DOCTYPE html>\n    <html lang=\"en\">\n    <head>\n        <meta charset=\"UTF-8\">\n        <title>Authorization Error</title>\n    </head>\n    <body>\n        <h1>Authorization Error</h1>\n        <p><strong>Code:</strong> {error}</p>\n        <p><strong>Description:</strong> {description}</p>\n        <button onclick=\"window.close()\">Close Window</button>\n    </body>\n    </html>\n    \"\"\",\n)\n\nINTERNAL_ERROR_TEMPLATE = dedent(\n    \"\"\"\n    <!DOCTYPE html>\n    <html lang=\"en\">\n    <head>\n        <meta charset=\"UTF-8\">\n        <title>Server Error</title>\n    </head>\n    <body>\n        <h1>Server Error</h1>\n        <p>Sorry, something went wrong while handling the callback.</p>\n        <pre>{details}</pre>\n        <button onclick=\"window.close()\">Close Window</button>\n    </body>\n    </html>\n    \"\"\",\n)\n\n\nclass InMemoryTokenStorage(TokenStorage):\n    \"\"\"Demo in-memory token storage implementation.\"\"\"\n\n    def __init__(self) -> None:\n        self.tokens: OAuthToken | None = None\n        self.client_info: OAuthClientInformationFull | None = None\n\n    async def get_tokens(self) -> OAuthToken | None:\n        \"\"\"Get stored tokens.\"\"\"\n        return self.tokens\n\n    async def set_tokens(self, tokens: OAuthToken) -> None:\n        \"\"\"Store tokens.\"\"\"\n        self.tokens = tokens\n\n    async def get_client_info(self) -> OAuthClientInformationFull | None:\n        \"\"\"Get stored client information.\"\"\"\n        return self.client_info\n\n    async def set_client_info(\n        self,\n        client_info: OAuthClientInformationFull,\n    ) -> None:\n        \"\"\"Store client information.\"\"\"\n        self.client_info = client_info\n\n\nclass CallbackHandler(BaseHTTPRequestHandler):\n    \"\"\"HTTP handler for OAuth callback.\"\"\"\n\n    def __init__(\n        self,\n        callback_server: \"CallbackServer\",\n        request: socket.socket,\n        client_address: tuple[str, int],\n        server: HTTPServer,\n    ) -> None:\n        self.callback_server: \"CallbackServer\" = callback_server\n        super().__init__(request, client_address, server)\n\n    def do_GET(self) -> None:\n        \"\"\"Handle GET request for OAuth callback.\"\"\"\n        try:\n            parsed_url = urlparse(self.path)\n            params = parse_qs(parsed_url.query)\n\n            if \"code\" in params:\n                code = params[\"code\"][0]\n                state = params.get(\"state\", [None])[0]\n\n                self.callback_server.auth_code = code\n                self.callback_server.auth_state = state\n                self.callback_server.auth_received = True\n\n                self.send_response(200)\n                self.send_header(\"Content-type\", \"text/html; charset=utf-8\")\n                self.end_headers()\n                self.wfile.write(SUCCESS_PAGE.encode(\"utf-8\"))\n\n            elif \"error\" in params:\n                error = params[\"error\"][0]\n                description = params.get(\n                    \"error_description\",\n                    [\"Unknown error\"],\n                )[0]\n\n                self.callback_server.auth_error = f\"{error}: {description}\"\n                self.callback_server.auth_received = True\n\n                self.send_response(400)\n                self.send_header(\"Content-type\", \"text/html; charset=utf-8\")\n                self.end_headers()\n                page = ERROR_PAGE_TEMPLATE.format(\n                    error=error,\n                    description=description,\n                )\n                self.wfile.write(page.encode(\"utf-8\"))\n\n        except Exception as exc:  # pylint: disable=broad-exception-caught\n            self.callback_server.auth_error = str(exc)\n            self.callback_server.auth_received = True\n\n            self.send_response(500)\n            self.send_header(\"Content-type\", \"text/html; charset=utf-8\")\n            self.end_headers()\n\n            page = INTERNAL_ERROR_TEMPLATE.format(details=exc)\n            self.wfile.write(page.encode(\"utf-8\"))\n\n\nclass CallbackServer:\n    \"\"\"OAuth callback server.\"\"\"\n\n    def __init__(self, port: int = 3000) -> None:\n        self.port = port\n        self.server: HTTPServer | None = None\n        self.thread: threading.Thread | None = None\n        self.auth_code: str | None = None\n        self.auth_state: str | None = None\n        self.auth_error: str | None = None\n        self.auth_received = False\n\n    def start(self) -> None:\n        \"\"\"Start callback server.\"\"\"\n\n        handler = partial(CallbackHandler, self)\n        self.server = HTTPServer((\"localhost\", self.port), handler)\n        self.thread = threading.Thread(\n            target=self.server.serve_forever,\n            daemon=True,\n        )\n        self.thread.start()\n        print(f\"OAuth callback server started, listening on port {self.port}\")\n\n    def stop(self) -> None:\n        \"\"\"Stop callback server.\"\"\"\n        if self.server:\n            self.server.shutdown()\n            self.server.server_close()\n        if self.thread:\n            self.thread.join(timeout=1)\n        print(\"OAuth callback server stopped\")\n\n    async def wait_for_callback(\n        self,\n        timeout: float = 300,\n    ) -> tuple[str, str | None]:\n        \"\"\"Wait for OAuth callback.\"\"\"\n\n        loop = asyncio.get_running_loop()\n        start_time = loop.time()\n\n        while not self.auth_received:\n            if loop.time() - start_time > timeout:\n                raise TimeoutError(\"OAuth callback timeout\")\n            await asyncio.sleep(0.1)\n\n        if self.auth_error:\n            raise RuntimeError(\n                f\"OAuth authorization failed: {self.auth_error}\",\n            )\n\n        if self.auth_code is None:\n            raise RuntimeError(\n                \"OAuth authorization failed: missing authorization code\",\n            )\n\n        return self.auth_code, self.auth_state\n\n\n# Global callback server instance\n_callback_server: CallbackServer | None = None\n\n\nasync def handle_redirect(auth_url: str) -> None:\n    \"\"\"Automatically open browser for OAuth authorization.\"\"\"\n    global _callback_server\n\n    # Start callback server\n    if _callback_server is None:\n        _callback_server = CallbackServer(port=3000)\n        _callback_server.start()\n\n    print(\"Opening browser for OAuth authorization...\")\n    print(f\"Authorization URL: {auth_url}\")\n\n    # Automatically open browser\n    webbrowser.open(auth_url)\n\n\nasync def handle_callback() -> tuple[str, str | None]:\n    \"\"\"Automatically handle OAuth callback.\"\"\"\n    global _callback_server\n\n    if _callback_server is None:\n        raise RuntimeError(\"Callback server not started\")\n\n    print(\"Waiting for OAuth authorization to complete...\")\n\n    try:\n        # Wait for callback\n        code, state = await _callback_server.wait_for_callback()\n        print(\"OAuth authorization successful!\")\n        return code, state\n\n    except Exception as e:  # pylint: disable=broad-exception-caught\n        print(f\"OAuth authorization failed: {e}\")\n        raise\n\n    finally:\n        # Clean up server state but keep server running for reuse\n        _callback_server.auth_code = None\n        _callback_server.auth_state = None\n        _callback_server.auth_error = None\n        _callback_server.auth_received = False\n"
  },
  {
    "path": "examples/integration/qwen_deep_research_model/README.md",
    "content": "# Deep Research Agent Example with Qwen-Deep-Research Model\n\n## What This Example Demonstrates\n\nThis example shows an Agent implementation with **Qwen-Deep-Research** model using the AgentScope framework. It can break down complex problems, uses web searches to perform analysis, and generates research reports.\n\nReference: https://www.alibabacloud.com/help/en/model-studio/qwen-deep-research\n\n## Prerequisites\n\n- Python 3.10 or higher\n- DashScope API key from [Alibaba Cloud](https://dashscope.console.aliyun.com/)\n\n## How to Run This Example\n1. **Set Environment Variable**:\n   ```bash\n   export DASHSCOPE_API_KEY=\"your_dashscope_api_key_here\"\n   ```\n2. **Run the script**:\n    ```bash\n   python main.py\n   ```\n"
  },
  {
    "path": "examples/integration/qwen_deep_research_model/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The main entry point of the Qwen Deep Research agent example.\"\"\"\nimport asyncio\n\nfrom qwen_deep_research_agent import QwenDeepResearchAgent\nfrom agentscope import logger\nfrom agentscope.message import Msg\n\n\nasync def main() -> None:\n    \"\"\"The main entry point for the Qwen Deep Research agent example.\"\"\"\n    # Create DeepResearch Agent\n    researcher = QwenDeepResearchAgent(\n        name=\"Researcher Qwen\",\n        verbose=True,\n    )\n\n    # Step 1: Model follow-up question for confirmation\n    # The model analyzes the user's question\n    # and asks follow-up questions to clarify the research direction.\n    user_msg = Msg(\n        name=\"User\",\n        content=\"Research the applications of artificial intelligence in \"\n        \"education\",\n        role=\"user\",\n    )\n\n    clarification = await researcher(user_msg)\n    print(f\"\\n{clarification.name}: {clarification.content}\\n\")\n\n    # Step 2: Deep research\n    # Based on the content of the follow-up question in Step 1,\n    # the model executes the complete research process.\n    user_response = Msg(\n        name=\"User\",\n        content=\"I am mainly interested in personalized learning and \"\n        \"intelligent assessment.\",\n        role=\"user\",\n    )\n\n    research_result = await researcher(user_response)\n    print(f\"\\n{research_result.name}: {research_result.content}\\n\")\n\n    print(\"\\n✅ Research complete!\\n\")\n\n\nif __name__ == \"__main__\":\n    try:\n        asyncio.run(main())\n    except Exception as e:\n        logger.exception(e)\n"
  },
  {
    "path": "examples/integration/qwen_deep_research_model/qwen_deep_research_agent.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Qwen Deep Research Agent\"\"\"\n# pylint: disable=line-too-long, too-many-branches, too-many-statements\n\nimport os\nfrom typing import Any, Optional, Union, Sequence\n\nimport dashscope\nfrom dashscope.api_entities.dashscope_response import GenerationResponse\n\nfrom agentscope import logger\nfrom agentscope.agent import AgentBase\nfrom agentscope.memory import MemoryBase, InMemoryMemory\nfrom agentscope.message import Msg\n\n\nclass QwenDeepResearchAgent(AgentBase):\n    \"\"\"\n    Deep Research Agent based on Qwen-Deep-Research model\n\n    This agent supports a two-step research process:\n    1. Clarification: Analyzes the question and asks follow-up questions\n    2. Deep research: Executes the complete research process\n\n    Args:\n        name (str):\n            Agent name\n        api_key (str, optional):\n            DashScope API Key, defaults to environment variable\n        memory (MemoryBase, optional):\n            Memory component\n        verbose (bool):\n            Whether to display detailed process, defaults to True\n    \"\"\"\n\n    def __init__(\n        self,\n        name: str,\n        api_key: Optional[str] = None,\n        memory: Optional[MemoryBase] = None,\n        verbose: bool = False,\n    ):\n        \"\"\"Initialize QwenDeepResearchAgent Agent\"\"\"\n\n        super().__init__()\n\n        self.name = name\n\n        # Configure API Key\n        self.api_key = api_key or os.getenv(\"DASHSCOPE_API_KEY\")\n        if not self.api_key:\n            raise ValueError(\n                \"The DASHSCOPE_API_KEY environment variable is not set.\",\n            )\n\n        self.model_name = \"qwen-deep-research\"\n        self.verbose = verbose\n        self.memory = memory or InMemoryMemory()\n\n    async def reply(\n        self,\n        x: Optional[Union[Msg, Sequence[Msg]]] = None,\n    ) -> Msg:\n        \"\"\"\n        Process input message and return reply (asynchronous version)\n\n        Args:\n            x: Input message, can be a single Msg or a list of Msg\n\n        Returns:\n            Msg: Agent's reply message\n        \"\"\"\n\n        # Process input message\n        if x is None:\n            logger.warning(\"Received empty message\")\n            return Msg(name=self.name, content=\"\", role=\"assistant\")\n\n        # Convert to message list\n        if isinstance(x, Msg):\n            msgs = [x]\n        else:\n            msgs = list(x)\n\n        # Add to memory\n        for msg in msgs:\n            await self.memory.add(msg)\n\n        # Check if clarification is needed\n        memory_list = await self.memory.get_memory()\n        user_msgs = [m for m in memory_list if m.role == \"user\"]\n\n        if len(user_msgs) == 1:\n            # Step 1: Clarification\n            logger.info(\"[%s] Starting clarification ...\", self.name)\n            content = await self._call_model(step_name=\"Clarification\")\n\n            response_msg = Msg(\n                name=self.name,\n                content=content,\n                role=\"assistant\",\n                metadata={\n                    \"phase\": \"clarification\",\n                    \"requires_user_response\": True,\n                },\n            )\n        else:\n            # Step 2: Deep Research\n            logger.info(\"[%s] Starting deep research ...\", self.name)\n            content = await self._call_model(step_name=\"Deep Research\")\n\n            response_msg = Msg(\n                name=self.name,\n                content=content,\n                role=\"assistant\",\n                metadata={\n                    \"phase\": \"deep_research\",\n                    \"requires_user_response\": False,\n                },\n            )\n\n        await self.memory.add(response_msg)\n\n        return response_msg\n\n    async def _call_model(self, step_name: str) -> str:\n        \"\"\"\n        Call qwen-deep-research model\n\n        Args:\n            step_name: step name\n\n        Returns:\n            str: Model response content\n        \"\"\"\n\n        if self.verbose:\n            logger.info(\"\\n%s\", \"=\" * 50)\n            logger.info(\"  %s\", step_name)\n            logger.info(\"%s\", \"=\" * 50)\n\n        memory_list = await self.memory.get_memory()\n        messages = []\n        for msg in memory_list:\n            messages.append(\n                {\n                    \"role\": msg.role,\n                    \"content\": msg.content,\n                },\n            )\n        try:\n            responses = await dashscope.AioGeneration.call(\n                api_key=self.api_key,\n                model=self.model_name,\n                messages=messages,\n                stream=True,\n                request_timeout=1800,  # Seconds\n            )\n            return await self._process_responses(responses)\n        except Exception as e:\n            err_msg = f\"An error occurred when calling the API: {e}\"\n            logger.error(err_msg)\n            return err_msg\n\n    async def _process_responses(\n        self,\n        responses: GenerationResponse,\n    ) -> str:\n        \"\"\"\n        Process model streaming responses (asynchronous version)\n\n        Args:\n            responses: Model response stream\n            step_name: Step name\n\n        Returns:\n            str: Model response content\n        \"\"\"\n\n        current_phase = None\n        current_status = None\n        phase_content = \"\"\n        research_goal = \"\"\n        keepalive_shown = False\n        references = []\n\n        async for response in responses:\n            # Check response status\n            if (\n                hasattr(response, \"status_code\")\n                and response.status_code != 200\n            ):\n                error_msg = f\"HTTP status code: {response.status_code}\"\n                if hasattr(response, \"code\"):\n                    error_msg += f\", Error code: {response.code}\"\n                if hasattr(response, \"message\"):\n                    error_msg += f\", Error message: {response.message}\"\n                logger.error(error_msg)\n                continue\n\n            if hasattr(response, \"output\") and response.output:\n                message = response.output.get(\"message\", {})\n                phase = message.get(\"phase\")\n                content = message.get(\"content\", \"\")\n                status = message.get(\"status\")\n                extra = message.get(\"extra\", {})\n\n                # Phase change detection\n                if phase != current_phase:\n                    if current_phase and phase_content and self.verbose:\n                        logger.info(\"\\n✓ %s phase completed\", current_phase)\n\n                    current_phase = phase\n                    phase_content = \"\"\n                    keepalive_shown = False\n\n                    if phase and phase != \"KeepAlive\" and self.verbose:\n                        logger.info(\"\\n▶ Entering %s phase\", phase)\n                        if phase == \"answer\":\n                            references = extra.get(\"deep_research\", {}).get(\n                                \"references\",\n                                [],\n                            )\n\n                # Process WebResearch phase\n                if phase == \"WebResearch\" and self.verbose:\n                    research_goal = self._handle_web_research_phase(\n                        status,\n                        extra,\n                        research_goal,\n                    )\n\n                if content:\n                    phase_content += content\n\n                    # Display content\n                    if self.verbose:\n                        print(content, end=\"\", flush=True)\n\n                # Display status changes\n                if status:\n                    if (\n                        status != current_status\n                        and status != \"typing\"\n                        and self.verbose\n                    ):\n                        self._log_status(status)\n                    current_status = status\n\n                # Token usage statistics\n                if status == \"finished\":\n                    self._log_usage(response)\n                    if self.verbose:\n                        logger.info(\"\\n✓ %s phase completed\", current_phase)\n                    if phase == \"answer\":\n                        if len(references) > 0:\n                            reference_links = []\n                            list_links = []\n                            for i, ref in enumerate(references):\n                                title = ref[\"title\"]\n                                url = ref[\"url\"]\n                                reference_links.append(\n                                    f'[{i + 1}]: {url} \"{title}\"',\n                                )\n                                list_links.append(f\"{i + 1}. [{title}]({url})\")\n                            phase_content = (\n                                phase_content\n                                + \"\\n\\n## References\\n\\n\"\n                                + \"\\n\".join(list_links)\n                                + \"\\n\\n\"\n                                + \"\\n\".join(reference_links)\n                            )\n                            break\n\n                # Process KeepAlive\n                if phase == \"KeepAlive\":\n                    if not keepalive_shown and self.verbose:\n                        logger.info(\"\\n⏳ Preparing for the next phase...\")\n                        keepalive_shown = True\n                    continue\n        return phase_content\n\n    def _handle_web_research_phase(\n        self,\n        status: str,\n        extra: dict,\n        research_goal: str,\n    ) -> str:\n        web_sites = []\n        if extra.get(\"deep_research\", {}).get(\"research\"):\n            research_info = extra[\"deep_research\"][\"research\"]\n\n            # handle research goal\n            if status == \"streamingQueries\":\n                if \"researchGoal\" in research_info:\n                    goal = research_info[\"researchGoal\"]\n                    if goal:\n                        research_goal += goal\n\n            # handle web site search results\n            elif status == \"streamingWebResult\":\n                if research_goal != \"\":\n                    logger.info(\"\\n🎯 Research Goal: %s\", research_goal)\n                    research_goal = \"\"\n                if \"webSites\" in research_info:\n                    sites = research_info[\"webSites\"]\n                    if sites and sites != web_sites:\n                        # web_sites.clear()\n                        web_sites.extend(sites)\n                        msg = (\n                            f\"\\n🔍 Found {len(sites)} relevant websites:\\n\"\n                            + \"\\n\".join(\n                                f\"  {i + 1}. {site.get('title', 'No title')}\\n\"\n                                f\"     {site.get('url', 'No link')}\"\n                                for i, site in enumerate(sites)\n                            )\n                        )\n                        logger.info(msg)\n            # handle finished status\n            elif status == \"WebResultFinished\":\n                logger.info(\n                    \"\\n✓ Web search completed, found %s reference sources\",\n                    len(web_sites),\n                )\n\n        return research_goal\n\n    def _log_status(self, status: str) -> None:\n        \"\"\"log status information\"\"\"\n\n        status_desc = {\n            \"streamingQueries\": \"Generating research goals and search queries \"\n            \"(WebResearch phase)\",\n            \"streamingWebResult\": \"Performing search, web page reading, and \"\n            \"code execution (WebResearch phase)\",\n            \"WebResultFinished\": \"Web search phase completed (WebResearch \"\n            \"phase)\",\n        }\n\n        if status in status_desc:\n            logger.info(\"\\n📊 %s\", status_desc[status])\n\n    def _log_usage(self, response: GenerationResponse) -> None:\n        \"\"\"log Token usage information\"\"\"\n\n        if hasattr(response, \"usage\") and response.usage:\n            usage = response.usage\n            if self.verbose:\n                print(\"\\n\")\n                logger.info(\n                    \"\\n📈 Token usage - input: %s output: %s\",\n                    usage.get(\"input_tokens\", 0),\n                    usage.get(\"output_tokens\", 0),\n                )\n\n    async def observe(self, msg: Msg | list[Msg] | None) -> None:\n        \"\"\"Receive the given message(s) without generating a reply.\n\n        Args:\n            msg (`Msg | list[Msg] | None`):\n                The message(s) to be observed.\n        \"\"\"\n        # Simply add the message(s) to memory without generating a reply\n        if msg is not None:\n            if isinstance(msg, Msg):\n                await self.memory.add(msg)\n            else:\n                for m in msg:\n                    await self.memory.add(m)\n\n    async def handle_interrupt(self, *args: Any, **kwargs: Any) -> Msg:\n        \"\"\"The post-processing logic when the reply is interrupted by the\n        user or something else.\n\n        Returns:\n            Msg: The interrupt message.\n        \"\"\"\n        # Return a message indicating the interruption\n        # pylint: disable=unused-argument\n        return Msg(\n            name=self.name,\n            content=\"Operation was interrupted.\",\n            role=\"assistant\",\n        )\n\n    async def reset_memory(self) -> None:\n        \"\"\"reset memory\"\"\"\n        await self.memory.clear()\n"
  },
  {
    "path": "examples/tuner/react_agent/README.md",
    "content": "# AgentScope Tuner Quick Start Guide\n\nAgentScope provides a `tuner` sub-module to train agent workflows using reinforcement learning (RL).\nThis guide walks you through the steps to implement and train an agent workflow using RL with AgentScope Tuner.\n\n## Overview\n\nTo train your agent workflow using RL, you need to understand three components:\n\n1. **Workflow function**: Refactor your agent workflow into a workflow function that follows the specified input/output signature.\n2. **Judge function**: Implement a judge function that computes rewards based on the agent's responses.\n3. **Task dataset**: Prepare a dataset containing training samples for the agent to learn.\n\nThe following diagram illustrates the relationship between these components:\n\n```mermaid\nflowchart TD\n    Model[Model] --> WorkflowFunction[Workflow Function]\n    WorkflowFunction --> JudgeFunction[Judge Function]\n    Task[Task] --> WorkflowFunction\n    Task[Task] --> JudgeFunction\n    JudgeFunction --> Reward[Reward]\n\n    classDef wfcolor fill:#e67e22,stroke:#333,color:#111;\n    classDef judgecolor fill:#1abc9c,stroke:#333,color:#111,stroke-dasharray: 5 5;\n    classDef taskcolor fill:#3498db,stroke:#333,color:#111;\n    class WorkflowFunction wfcolor;\n    class JudgeFunction judgecolor;\n    class Task taskcolor;\n```\n\n## How to implement\n\nHere we use a math problem solving scenario as an example to illustrate how to implement the above three components.\n\nSuppose you have an agent workflow that solves math problems using the `ReActAgent`.\n\n```python\nfrom agentscope.agent import ReActAgent\n\nasync def run_react_agent(query: str):\n    # model = ...  # Initialize your ChatModel here\n\n    agent = ReActAgent(\n        name=\"react_agent\",\n        sys_prompt=\"You are a helpful math problem solving agent.\",\n        model=model,\n        enable_meta_tool=True,\n        formatter=OpenAIChatFormatter(),\n    )\n\n    response = await agent.reply(\n        msg=Msg(\"user\", query, role=\"user\"),\n    )\n\n    print(response)\n```\n\n### Step 1: Prepare task dataset\n\nTo train the agent solving math problems, you need a training dataset that contains samples of math problems and their corresponding ground truth answers.\n\nThe dataset should be organized in huggingface [datasets](https://huggingface.co/docs/datasets/quickstart) format and can be loaded using the `datasets.load_dataset` function. For example:\n\n```\nmy_dataset/\n    ├── train.jsonl  # samples for training\n    └── test.jsonl   # samples for evaluation\n```\n\nSuppose your `train.jsonl` contains samples like:\n\n```json\n{\"question\": \"What is 2 + 2?\", \"answer\": \"4\"}\n{\"question\": \"What is 4 + 4?\", \"answer\": \"8\"}\n```\n\nNote that the task sample format can vary based on your specific scenario. The key point is that each sample should contain the necessary information for the agent to complete the task and for judging the quality of the response.\n\nYou can preview your dataset using the following code:\n\n```python\nfrom agentscope.tuner import DatasetConfig\n\nDatasetConfig(path=\"my_dataset\", split=\"train\").preview()\n\n# Output:\n# [\n#   {\n#     \"question\": \"What is 2 + 2?\",\n#     \"answer\": \"4\"\n#   },\n#   {\n#     \"question\": \"What is 4 + 4?\",\n#     \"answer\": \"8\"\n#   }\n# ]\n```\n\n### Step 2: Define a workflow function\n\nTo train an agent workflow using RL, you need to refactor your agent with the following signature.\n\n```python\nasync def workflow_function(\n    task: Dict,\n    model: ChatModelBase,\n    auxiliary_models: Optional[Dict[str, ChatModelBase]]=None,\n) -> WorkflowOutput:\n    \"\"\"Run the agent workflow on a single task and return a scalar reward.\"\"\"\n```\n\n- Inputs:\n    - `task`: A dictionary representing a single training task, converted from a sample in the training dataset. For example, if using the dataset prepared in Step 1, the `task` is a dictionary containing `question` and `answer` fields.\n    - `model`: A `ChatModelBase` instance, which has the same interface as `OpenAIChatModel`, but it supports automatically converting invoke history into trainable data.\n    - `auxiliary_models`: A dictionary of auxiliary models that can be used in the workflow. The keys are model names, and the values are `ChatModelBase` instances. These models are different from the main `model` in that they are not directly trained, but can be used to assist the main model in completing the task (e.g., acting as Judge). Empty dict if no auxiliary models are needed.\n\n- Outputs:\n    - `WorkflowOutput`: An object containing the output of the workflow function, which contains:\n        - `reward`: A scalar float representing the reward obtained from the workflow function. Fill this field if you want to directly output the reward from the workflow function. Otherwise, you can leave it as `None` and implement the reward calculation in the judge function.\n        - `response`: The output from the workflow function, which can be the agent's response or other types of outputs depending on your workflow function implementation. Used for reward calculation in the judge function. If you don't need to calculate reward in the judge function, you can leave it as `None`.\n        - `metrics`: A dictionary of additional metrics that can be logged during training. Leave it as `None` if no additional metrics are needed.\n\n\nBelow is a refactored version of the original `run_react_agent` function to fit the workflow function signature.\n\n**There are only 3 minor changes from the original function**:\n\n1. use the input `model` to initialize the agent.\n2. use the `question` field from the `task` dictionary as the user query.\n3. return a `WorkflowOutput` object containing the agent's response.\n\n```python\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import OpenAIChatFormatter\nfrom agentscope.tuner import WorkflowOutput\nfrom agentscope.message import Msg\n\nasync def run_react_agent(\n    task: Dict,\n    model: ChatModelBase,\n    auxiliary_models: Optional[Dict[str, ChatModelBase]]=None,\n) -> WorkflowOutput:\n    agent = ReActAgent(\n        name=\"react_agent\",\n        sys_prompt=\"You are a helpful math problem solving agent.\",\n        model=model,  # directly use the trainable model here\n        formatter=OpenAIChatFormatter(),\n    )\n\n    response = await agent.reply(\n        msg=Msg(\"user\", task[\"question\"], role=\"user\"),  # extract question from task\n    )\n\n    return WorkflowOutput(  # put the response into WorkflowOutput\n        response=response,\n    )\n```\n\n### Step 3: Implement the judge function\n\nTo train the agent using RL, you need to define a judge function that computes a reward following the signature below.\n\n```python\nasync def judge_function(\n    task: Dict,\n    response: Any,\n    auxiliary_models: Dict[str, ChatModelBase],\n) -> JudgeOutput:\n    \"\"\"Calculate reward based on the input task and agent's response.\"\"\"\n```\n\n- Inputs:\n    - `task`: A dictionary representing a single training task, same as the input to the workflow function.\n    - `response`: The output from the workflow function, which can be the agent's response or other types of outputs depending on your workflow function implementation.\n    - `auxiliary_models`: A dictionary of auxiliary models that can be used in the reward calculation. The keys are model names, and the values are `ChatModelBase` instances. These models are different from the main model in that they are not directly trained, but can be used to assist in calculating the reward (e.g., acting as Judge). Empty dict if no auxiliary models are needed.\n\n- Outputs:\n    - `JudgeOutput`: An object containing the output of the judge function. It contains:\n        - `reward`: A scalar float representing the reward calculated based on the input task and agent's response. This field must be filled.\n        - `metrics`: A dictionary of additional metrics that can be logged during training. Leave it as `None` if no additional metrics are needed.\n\nHere is an example implementation of a simple reward calculation mechanism that gives a reward of `1.0` for an exact match between the agent's answer and the ground truth answer, and `0.0` otherwise.\n\n> Note: This is a toy reward function; in practice, you need to parse the agent's response to extract the final answer before comparing it with the ground truth. You may also want to use a more robust metric for reward calculation.\n\n```python\nfrom agentscope.message import Msg\nfrom agentscope.tuner import JudgeOutput\n\nasync def judge_function(\n    task: Dict, response: Msg, auxiliary_models: Dict[str, ChatModelBase]\n) -> JudgeOutput:\n    \"\"\"Simple reward: 1.0 for exact match, else 0.0.\"\"\"\n    ground_truth = task[\"answer\"]\n    reward = 1.0 if ground_truth in response.get_text_content() else 0.0\n    return JudgeOutput(reward=reward)\n```\n\n> Tip: You can leverage existing [`MetricBase`](https://github.com/agentscope-ai/agentscope/blob/main/src/agentscope/evaluate/_metric_base.py) implementations in your judge function to compute more sophisticated metrics and combine them into a composite reward.\n\n### Step 4: Start tuning\n\nFinally, you can use the `tune` interface to train the defined workflow function with a configuration file.\n\n```python\nfrom agentscope.tuner import tune, AlgorithmConfig, DatasetConfig, TunerModelConfig\n\n# your workflow / judge function here...\n\nif __name__ == \"__main__\":\n    dataset = DatasetConfig(path=\"my_dataset\", split=\"train\")\n    model = TunerModelConfig(model_path=\"Qwen/Qwen3-0.6B\", max_model_len=16384)\n    algorithm = AlgorithmConfig(\n        algorithm_type=\"multi_step_grpo\",\n        group_size=8,\n        batch_size=32,\n        learning_rate=1e-6,\n    )\n    tune(\n        workflow_func=run_react_agent,\n        judge_func=judge_function,\n        model=model,\n        train_dataset=dataset,\n        algorithm=algorithm,\n    )\n    # for advanced users, you can pass in config_path to load config from a YAML file\n    # and ignore other arguments\n    # tune(\n    #     workflow_func=run_react_agent,\n    #     judge_func=judge_function,\n    #     config_path=\"config.yaml\",\n    #)\n```\n\nHere, we use `DatasetConfig` to load the training dataset, `TunerModelConfig` to initialize the trainable model, and `AlgorithmConfig` to specify the RL algorithm and its hyperparameters.\n\n> Note:\n> The `tune` function is based on [Trinity-RFT](https://github.com/agentscope-ai/Trinity-RFT) and it converts the input parameters into a YAML configuration internally.\n> Advanced users can ignore `model`, `train_dataset`, `algorithm` arguments and provide a configuration file path pointing to a YAML file using the `config_path` argument instead (see [config.yaml](./config.yaml) for an example).\n> We recommend using the configuration file approach for fine-grained control over the training process and leveraging advanced features provided by Trinity-RFT.\n> You can refer to the Trinity-RFT [Configuration Guide](https://agentscope-ai.github.io/Trinity-RFT/en/main/tutorial/trinity_configs.html) for more details on configuration options.\n\nThe checkpoint and logs will automatically be saved to the `checkpoints/AgentScope` directory under the current working directory and each run will be save in a sub-directory suffixed with current timestamp.\nYou can found the tensorboard logs inside `monitor/tensorboard` of the checkpoint directory.\n\n```\nreact_agent/\n    └── checkpoints/\n        └──AgentScope/\n            └── Experiment-20260104185355/  # each run saved in a sub-directory with timestamp\n                ├── monitor/\n                │   └── tensorboard/  # tensorboard logs\n                └── global_step_x/    # saved model checkpoints at step x\n```\n\n---\n\n### Complete example\n\n```python\nfrom typing import Dict\n\nfrom agentscope.tuner import tune, WorkflowOutput, JudgeOutput, DatasetConfig, AlgorithmConfig\nfrom agentscope.agent import ReActAgent\nfrom agentscope.model import ChatModelBase\nfrom agentscope.formatter import OpenAIChatFormatter\nfrom agentscope.message import Msg\n\n\nasync def run_react_agent(\n    task: Dict,\n    model: ChatModelBase,\n    auxiliary_models: Dict[str, ChatModelBase],\n) -> WorkflowOutput:\n    agent = ReActAgent(\n        name=\"react_agent\",\n        sys_prompt=\"You are a helpful math problem solving agent.\",\n        model=model,  # directly use the trainable model here\n        formatter=OpenAIChatFormatter(),\n    )\n\n    response = await agent.reply(\n        msg=Msg(\"user\", task[\"question\"], role=\"user\"),  # extract question from task\n    )\n\n    return WorkflowOutput(\n        response=response,\n    )\n\n\nasync def judge_function(\n    task: Dict, response: Msg, auxiliary_models: Dict[str, ChatModelBase]\n) -> JudgeOutput:\n    \"\"\"Simple reward: 1.0 for exact match, else 0.0.\"\"\"\n    ground_truth = task[\"answer\"]\n    reward = 1.0 if ground_truth in response.get_text_content() else 0.0\n    return JudgeOutput(reward=reward)\n\n\nif __name__ == \"__main__\":\n    dataset = DatasetConfig(path=\"my_dataset\", split=\"train\")\n    model = TunerModelConfig(model_path=\"Qwen/Qwen3-0.6B\", max_model_len=16384)\n    algorithm = AlgorithmConfig(\n        algorithm_type=\"multi_step_grpo\",\n        group_size=8,\n        batch_size=32,\n        learning_rate=1e-6,\n    )\n    tune(\n        workflow_func=run_react_agent,\n        judge_func=judge_function,\n        model=model,\n        train_dataset=dataset,\n        algorithm=algorithm,\n    )\n```\n\n> Note:\n> Above code is a simplified example for illustration purposes only.\n> For a complete implementation, please refer to [main.py](./main.py), which trains a ReAct agent to solve math problems on the GSM8K dataset.\n\n---\n\n## How to run\n\nAfter implementing the workflow function, follow these steps to run the training:\n\n1. Prerequisites\n\n    - At least 2 NVIDIA GPUs with CUDA 12.8 or newer.\n    - Adjust the configuration file ([config.yaml](./config.yaml)) based on your hardware.\n    - Follow the Trinity-RFT [installation guide](https://agentscope-ai.github.io/Trinity-RFT/en/main/tutorial/trinity_installation.html) to install the latest version from source code.\n    - Download the GSM8K dataset and Qwen/Qwen3-0.6B model checkpoints (example):\n\n      ```bash\n      huggingface-cli download openai/gsm8k --repo-type dataset\n      huggingface-cli download Qwen/Qwen3-0.6B\n      ```\n\n2. Set up a [Ray](https://github.com/ray-project/ray) cluster\n\n    ```bash\n    ray start --head\n    # for multi-node setup, run the following command on worker nodes\n    # ray start --address=<master_address>\n    ```\n\n3. Run the training script\n\n    ```bash\n    python main.py\n    ```\n\n4. The reward curve and other training metrics can be monitored using TensorBoard:\n\n    ```bash\n    tensorboard --logdir ./checkpoints/AgentScope/Experiment-xxxxxx/monitor/tensorboard\n    ```\n\n    An example reward curve is shown below:\n\n    ![reward_curve](./reward_curve.png)\n\n> [!TIP]\n> For more tuning examples, refer to [tuner](https://github.com/agentscope-ai/agentscope-samples/tree/main/tuner) directory of the AgentScope-Samples repository.\n"
  },
  {
    "path": "examples/tuner/react_agent/config.yaml",
    "content": "# Please refer to https://agentscope-ai.github.io/Trinity-RFT/en/main/tutorial/trinity_configs.html for detailed explanation of each field.\nproject: AgentScope\nname: GSM8K-Qwen3-0.6B\n# directory to save checkpoints, default to ./checkpoints if TRINITY_CHECKPOINT_ROOT_DIR not set\ncheckpoint_root_dir: ${oc.env:TRINITY_CHECKPOINT_ROOT_DIR,./checkpoints}\nalgorithm:\n  algorithm_type: multi_step_grpo  # a GRPO-based algorithm for multi-step reasoning\n  repeat_times: 8  # repeat each training sample 8 times\nmodel:\n  # path to the pre-trained model, default to Qwen/Qwen3-0.6B if TRINITY_MODEL_PATH not set\n  model_path: ${oc.env:TRINITY_MODEL_PATH,Qwen/Qwen3-0.6B}\n  # maximum tokens generated in response\n  max_response_tokens: 16384\n  # maximum token length for both input and output\n  # if you face OOM, try to reduce max_model_len and max_response_tokens\n  max_model_len: 24576\n  temperature: 1.0\ncluster:\n  node_num: 1  # cluster with 1 node\n  gpu_per_node: 8  # each node has 8 GPUs\nbuffer:\n  total_epochs: 1  # run taskset for 1 epoch\n  batch_size: 32  # each step contains 32 samples from taskset\n  train_batch_size: 256  # trainer batch size is 256 (multi-step reasoning generate more training samples)\n  explorer_input:\n    taskset:  # define the taskset for rollout\n      name: gsm8k\n      path: 'openai/gsm8k'\n      subset_name: 'main'\n      split: 'train'\nexplorer:\n  runner_per_model: 16  # each model has 16 runners for parallel rollout\n  max_timeout: 600  # max timeout for each rollout is 600 seconds\n  rollout_model:\n    engine_num: 4  # setup 4 vllm inference model instances\n    tensor_parallel_size: 1  # each model instance uses tensor parallel size of 1\n    enable_openai_api: true  # some parameters to provide openai-style API, don't change them\n    enable_history: true\n    enable_auto_tool_choice: true\n    # Qwen3 series tool_call_parser and reasoning_parser, if you use other models, please adjust accordingly\n    tool_call_parser: hermes\n    reasoning_parser: deepseek_r1\nsynchronizer:\n  sync_style: dynamic_by_explorer\n  sync_method: 'nccl'\n  sync_interval: 1\n  sync_timeout: 1800  # wait for 30 minutes\ntrainer:\n  save_interval: 100  # save checkpoint every 100 steps\n  use_dynamic_bsz: true\n  ulysses_sequence_parallel_size: 1  # use sequence parallelism to reduce memory usage\nmonitor:\n  monitor_type: tensorboard  # here we use tensorboard, you can also use wandb, mlflow or swanlab\n"
  },
  {
    "path": "examples/tuner/react_agent/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Example of training a ReAct agent on GSM8K with Trinity-RFT.\"\"\"\nfrom typing import Dict\n\nfrom agentscope.tuner import (\n    tune,\n    DatasetConfig,\n    WorkflowOutput,\n    JudgeOutput,\n    TunerModelConfig,\n    AlgorithmConfig,\n)\nfrom agentscope.agent import ReActAgent\nfrom agentscope.model import OpenAIChatModel\nfrom agentscope.formatter import OpenAIChatFormatter\nfrom agentscope.message import Msg\n\n\nasync def run_react_agent(\n    task: Dict,\n    model: OpenAIChatModel,\n    auxiliary_models: Dict[str, OpenAIChatModel] | None = None,\n) -> WorkflowOutput:\n    \"\"\"A simple workflow function using the ReAct agent to solve tasks.\n\n    Args:\n        task (`Dict`): The task to be solved.\n        model (`OpenAIChatModel`): The language model to use.\n        auxiliary_models (`Dict[str, OpenAIChatModel]`):\n            A dictionary of additional chat models available for\n            LLM-as-a-Judge. Not used in this workflow.\n\n    Returns:\n        `WorkflowOutput`: The workflow output containing the agent's response.\n    \"\"\"\n    assert (\n        auxiliary_models is None or len(auxiliary_models) == 0\n    ), \"No auxiliary models are used in this workflow.\"\n\n    sys_prompt = (\n        \"You are an agent specialized in solving math problems with tools. \"\n        \"Please solve the math problem given to you. You can write and \"\n        \"execute Python code to perform calculation or verify your answer. \"\n        \"You should return your final answer within \\\\boxed{{}}.\"\n    )\n    agent = ReActAgent(\n        name=\"react_agent\",\n        sys_prompt=sys_prompt,\n        model=model,\n        enable_meta_tool=True,\n        formatter=OpenAIChatFormatter(),\n    )\n    response = await agent.reply(\n        msg=Msg(\"user\", task[\"question\"], role=\"user\"),\n    )\n    return WorkflowOutput(\n        response=response,\n    )\n\n\nasync def gsm8k_judge(\n    task: Dict,\n    response: Msg,\n    auxiliary_models: Dict[str, OpenAIChatModel] | None = None,\n) -> JudgeOutput:\n    \"\"\"A simple judge function to calculate reward based on agent's response.\n\n    Args:\n        task (`Dict`): The task information for the corresponding workflow.\n        response (`Msg`): The response generated by the corresponding workflow.\n        auxiliary_models (`Dict[str, OpenAIChatModel]`):\n            A dictionary of additional chat models available for LLM-as-a-Judge\n            usage. The keys are model names, and the values are the\n            corresponding OpenAIChatModel instances.\n\n    Returns:\n        `JudgeOutput`: The reward value assigned by the judge function.\n    \"\"\"\n    from trinity.common.rewards.math_reward import MathBoxedRewardFn\n\n    assert (\n        auxiliary_models is None or len(auxiliary_models) == 0\n    ), \"No auxiliary models are used in this workflow.\"\n\n    reward_fn = MathBoxedRewardFn()\n    # parse truth from gsm8k raw text\n    truth = task[\"answer\"]\n    if isinstance(truth, str) and \"####\" in truth:\n        truth = truth.split(\"####\")[1].strip()\n    else:\n        truth = str(truth)\n    # parse answer from response message\n    result = response.get_text_content()\n    reward_dict = reward_fn(\n        response=result,\n        truth=truth,\n    )\n    return JudgeOutput(\n        reward=sum(reward_dict.values()),\n        metrics=reward_dict,\n    )\n\n\nif __name__ == \"__main__\":\n    dataset = DatasetConfig(\n        path=\"openai/gsm8k\",\n        name=\"main\",\n        split=\"train\",\n    )\n    tuner_model = TunerModelConfig(\n        model_path=\"Qwen/Qwen3-0.6B\",\n        max_model_len=24576,\n        max_tokens=16384,\n        temperature=1.0,\n        inference_engine_num=4,\n        tensor_parallel_size=1,\n    )\n\n    # If you have no GPU and want to use Tinker,\n    # uncomment the following code\n    # If you want to use local deployed TuFT\n    # set `base_url` to your TuFT server address.\n    #\n    # from agentscope.tuner import TinkerConfig\n    # tuner_model = TunerModelConfig(\n    #     model_path=\"Qwen/Qwen3-4B-Instruct-2507\",\n    #     max_model_len=24576,\n    #     max_tokens=16384,\n    #     temperature=1.0,\n    #     tinker_config=TinkerConfig(\n    #         rank=16,\n    #         base_url=None,\n    #     ),\n    # )\n\n    algorithm = AlgorithmConfig(\n        algorithm_type=\"multi_step_grpo\",\n        group_size=8,\n        learning_rate=1e-6,\n        batch_size=32,\n    )\n    tune(\n        workflow_func=run_react_agent,\n        judge_func=gsm8k_judge,\n        train_dataset=dataset,\n        model=tuner_model,\n        algorithm=algorithm,\n    )\n"
  },
  {
    "path": "examples/workflows/multiagent_concurrent/README.md",
    "content": "# Multiagent Concurrent\n\nThis example demonstrates how to run multiple agents concurrently in AgentScope, where each agent operates\nindependently and can perform tasks simultaneously.\n\nSpecifically, we showcase two ways to achieve concurrency:\n\n- Using Python's `asyncio.gather` to run multiple agents asynchronously.\n- Using `fanout_pipeline` to execute multiple agents in parallel and gather their results.\n\nThe fanout pipeline will distribute the input to multiple agents and collect their outputs, which is appropriate for\nscenarios like voting or parallel question answering.\n\n## QuickStart\n\nInstall the agentscope package if you haven't already:\n\n```bash\npip install agentscope\n```\n\nThen run the example script:\n\n```bash\npython main.py\n```\n\n## Further Reading\n- [Pipelines](https://doc.agentscope.io/tutorial/task_pipeline.html)\n"
  },
  {
    "path": "examples/workflows/multiagent_concurrent/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Parallel Multi-Perspective Discussion System.\"\"\"\nimport asyncio\nfrom datetime import datetime\nfrom typing import Any\n\nimport numpy as np\n\nfrom agentscope.agent import AgentBase\nfrom agentscope.message import Msg\nfrom agentscope.pipeline import fanout_pipeline\n\n\nclass ExampleAgent(AgentBase):\n    \"\"\"The example agent used to label the time.\"\"\"\n\n    def __init__(self, name: str) -> None:\n        \"\"\"The constructor of the example agent\n\n        Args:\n            name (`str`):\n                The agent name.\n        \"\"\"\n        super().__init__()\n        self.name = name\n\n    async def reply(self, *args: Any, **kwargs: Any) -> Msg:\n        \"\"\"The reply function of the example agent.\"\"\"\n        # we record the start time\n        start_time = datetime.now()\n        await self.print(\n            Msg(\n                self.name,\n                f\"begins at {start_time.strftime('%H:%M:%S.%f')}\",\n                \"assistant\",\n            ),\n        )\n\n        # Sleep some time\n        await asyncio.sleep(np.random.choice([2, 3, 4]))\n\n        end_time = datetime.now()\n        msg = Msg(\n            self.name,\n            f\"finishes at {end_time.strftime('%H:%M:%S.%f')}\",\n            \"user\",\n            # Add some metadata for demonstration\n            metadata={\n                \"time\": (end_time - start_time).total_seconds(),\n            },\n        )\n        await self.print(msg)\n        return msg\n\n    async def handle_interrupt(\n        self,\n        *args: Any,\n        **kwargs: Any,\n    ) -> Msg:\n        \"\"\"We leave this function unimplemented in this example, because we\n        won't use the interrupt functionality\"\"\"\n\n    async def observe(self, *args: Any, **kwargs: Any) -> None:\n        \"\"\"Similar with the handle_interrupt function, leaving this empty\"\"\"\n\n\nasync def main() -> None:\n    \"\"\"The main entry of the concurrent example.\"\"\"\n    alice = ExampleAgent(\"Alice\")\n    bob = ExampleAgent(\"Bob\")\n    chalice = ExampleAgent(\"Chalice\")\n\n    print(\"Use 'asyncio.gather' to run the agents concurrently:\")\n    futures = [alice(), bob(), chalice()]\n\n    await asyncio.gather(*futures)\n\n    print(\"\\n\\nUse fanout pipeline to run the agents concurrently:\")\n    collected_res = await fanout_pipeline(\n        agents=[alice, bob, chalice],\n        enable_gather=True,\n    )\n    # Print the collected results\n    print(\"\\n\\nThe collected time used by each agent:\")\n    for res in collected_res:\n        print(f\"{res.name}: {res.metadata['time']} seconds\")\n\n    print(\"\\nThe average time used:\")\n    avg_time = np.mean([res.metadata[\"time\"] for res in collected_res])\n    print(f\"{avg_time} seconds\")\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/workflows/multiagent_conversation/README.md",
    "content": "# MultiAgent Conversation\n\nThis example demonstrates how to build a multi-agent conversation workflow using ``MsgHub`` in AgentScope,\nwhere multiple agents broadcast messages to each other in a shared conversation space.\n\n## Setup\n\nThe example is built upon the DashScope LLM API in [main.py](https://github.com/agentscope-ai/agentscope/blob/main/examples/workflows/multiagent_conversation/main.py). You can switch to other LLMs by modifying the ``model`` and ``formatter`` parameters in the code.\n\nTo run the example, first install the latest version of AgentScope, then run:\n\n```bash\npython examples/workflows/multiagent_conversation/main.py\n```\n\n## Main Workflow\n\n- Create multiple participant agents with different attributes (e.g., Alice, Bob, Charlie).\n- Agents introduce themselves and interact in the message hub.\n- Supports dynamic addition and removal of agents, as well as broadcasting messages.\n\n> Note: The example is built with DashScope chat model. If you want to change the model in this example, don't forget\n> to change the formatter at the same time! The corresponding relationship between built-in models and formatters are\n> list in [our tutorial](https://doc.agentscope.io/tutorial/task_prompt.html#id1)"
  },
  {
    "path": "examples/workflows/multiagent_conversation/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The example of how to construct multi-agent conversation with MsgHub and\npipeline in AgentScope.\"\"\"\nimport asyncio\nimport os\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeMultiAgentFormatter\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.pipeline import MsgHub, sequential_pipeline\n\n\ndef create_participant_agent(\n    name: str,\n    age: int,\n    career: str,\n    character: str,\n) -> ReActAgent:\n    \"\"\"Create a participant agent with a specific name, age, and character.\"\"\"\n    return ReActAgent(\n        name=name,\n        sys_prompt=(\n            f\"You're a {age}-year-old {career} named {name} and you're \"\n            f\"a {character} person.\"\n        ),\n        model=DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            stream=True,\n        ),\n        # Use multiagent formatter because the multiple entities will\n        # occur in the prompt of the LLM API call\n        formatter=DashScopeMultiAgentFormatter(),\n    )\n\n\nasync def main() -> None:\n    \"\"\"Run a multi-agent conversation workflow.\"\"\"\n\n    # Create multiple participant agents with different characteristics\n    alice = create_participant_agent(\"Alice\", 30, \"teacher\", \"friendly\")\n    bob = create_participant_agent(\"Bob\", 14, \"student\", \"rebellious\")\n    charlie = create_participant_agent(\"Charlie\", 28, \"doctor\", \"thoughtful\")\n\n    # Create a conversation where participants introduce themselves within\n    # a message hub\n    async with MsgHub(\n        participants=[alice, bob, charlie],\n        # The greeting message will be sent to all participants at the start\n        announcement=Msg(\n            \"system\",\n            \"Now you meet each other with a brief self-introduction.\",\n            \"system\",\n        ),\n    ) as hub:\n        # Quick construct a pipeline to run the conversation\n        await sequential_pipeline([alice, bob, charlie])\n        # Or by the following way:\n        # await alice()\n        # await bob()\n        # await charlie()\n\n        # Delete a participant agent from the hub and fake a broadcast message\n        print(\"##### We fake Bob's departure #####\")\n        hub.delete(bob)\n        await hub.broadcast(\n            Msg(\n                \"bob\",\n                \"I have to start my homework now, see you later!\",\n                \"assistant\",\n            ),\n        )\n        await alice()\n        await charlie()\n\n        # ...\n\n\nasyncio.run(main())\n"
  },
  {
    "path": "examples/workflows/multiagent_debate/README.md",
    "content": "# MultiAgent Debate\n\nDebate workflow simulates a multi-turn discussion between different agents, mostly several solvers and an aggregator.\nTypically, the solvers generate and exchange their answers, while the aggregator collects and summarizes the answers.\n\nWe implement the examples in [EMNLP 2024](https://aclanthology.org/2024.emnlp-main.992/), where two debater agents\nwill discuss a topic in a fixed order, and express their arguments based on the previous debate history.\nAt each round a moderator agent will decide whether the correct answer can be obtained in the current iteration.\n\n## Setup\n\nThe example is built upon DashScope LLM API in [main.py](https://github.com/agentscope-ai/agentscope/blob/main/examples/workflows/multiagent_debate/main.py).\nYou can also change to the other LLMs by modifying the ``model`` and ``formatter`` parameters in the code.\n\nTo run the example, first install the latest version of AgentScope, then run:\n\n```bash\npython examples/workflows/multiagent_debate/main.py\n```\n\n\n> Note: The example is built with DashScope chat model. If you want to change the model in this example, don't forget\n> to change the formatter at the same time! The corresponding relationship between built-in models and formatters are\n> list in [our tutorial](https://doc.agentscope.io/tutorial/task_prompt.html#id1)"
  },
  {
    "path": "examples/workflows/multiagent_debate/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The multi-agent debate workflow example in AgentScope.\"\"\"\nimport asyncio\nimport os\n\nfrom pydantic import (\n    BaseModel,\n    Field,\n)\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import (\n    DashScopeChatFormatter,\n    DashScopeMultiAgentFormatter,\n)\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.pipeline import MsgHub\n\ntopic = (\n    \"The two circles are externally tangent and there is no relative sliding. \"\n    \"The radius of circle A is 1/3 the radius of circle B. Circle A rolls \"\n    \"around circle B one trip back to its starting point. How many times will \"\n    \"circle A revolve in total?\"\n)\n\n\n# Create two debater agents, Alice and Bob, who will discuss the topic.\ndef create_solver_agent(name: str) -> ReActAgent:\n    \"\"\"Get a solver agent.\"\"\"\n    return ReActAgent(\n        name=name,\n        sys_prompt=f\"You're a debater named {name}. Hello and welcome to the \"\n        \"debate competition. It's not necessary to fully agree \"\n        \"with each other's perspectives, as our objective is to \"\n        \"find the correct answer. The debate topic is stated as \"\n        f\"follows: {topic}. Use Chinese to answer the question\",\n        model=DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n            stream=True,\n        ),\n        formatter=DashScopeChatFormatter(),\n    )\n\n\nalice, bob = [create_solver_agent(name) for name in [\"Alice\", \"Bob\"]]\n\n# Create a moderator agent\nmoderator = ReActAgent(\n    name=\"Aggregator\",\n    sys_prompt=(\n        \"You're a moderator. There will be two debaters involved in a debate \"\n        \"competition. They will present their answer and discuss their \"\n        \"perspectives on the topic:\\n\"\n        \"```\\n\"\n        \"{topic}\\n\"\n        \"```\\n\"\n        \"At the end of each round, you will evaluate both sides' answers \"\n        \"and decide which one is correct.\"\n    ),\n    model=DashScopeChatModel(\n        model_name=\"qwen-max\",\n        api_key=os.environ[\"DASHSCOPE_API_KEY\"],\n        stream=True,\n    ),\n    formatter=DashScopeMultiAgentFormatter(),\n)\n\n\n# A structured output model for the moderator\nclass JudgeModel(BaseModel):\n    \"\"\"The structured output model for the moderator.\"\"\"\n\n    finished: bool = Field(\n        description=\"Whether the debate is finished.\",\n    )\n    correct_answer: str | None = Field(\n        description=\"The correct answer to the debate topic, only if the \"\n        \"debate is finished. Otherwise, leave it as None.\",\n        default=None,\n    )\n\n\nasync def run_multiagent_debate() -> None:\n    \"\"\"Run the multi-agent debate workflow.\"\"\"\n    while True:\n        # The reply messages in MsgHub from the participants will be\n        # broadcasted to all participants.\n        async with MsgHub(participants=[alice, bob, moderator]):\n            await alice(\n                Msg(\n                    \"user\",\n                    \"You are affirmative side, Please express your \"\n                    \"viewpoints.\",\n                    \"user\",\n                ),\n            )\n            await bob(\n                Msg(\n                    \"user\",\n                    \"You are negative side. You disagree with the \"\n                    \"affirmative side. Provide your reason and answer.\",\n                    \"user\",\n                ),\n            )\n\n        # Alice and Bob doesn't need to know the moderator's message,\n        # so moderator is called outside the MsgHub.\n        msg_judge = await moderator(\n            Msg(\n                \"user\",\n                \"Now you have heard the answers from the others, have \"\n                \"the debate finished, and can you get the correct answer?\",\n                \"user\",\n            ),\n            structured_model=JudgeModel,\n        )\n\n        print(\"【STRUCTURED_OUTPUT】: \", msg_judge.metadata)\n\n        if msg_judge.metadata.get(\"finished\"):\n            print(\n                \"The debate is finished, and the correct answer is: \",\n                msg_judge.metadata.get(\"correct_answer\"),\n            )\n            break\n\n\nasyncio.run(run_multiagent_debate())\n"
  },
  {
    "path": "examples/workflows/multiagent_realtime/README.md",
    "content": "# Multi-Agent Realtime Voice Interaction Example\n\nThis example demonstrates how to use AgentScope's `ChatRoom` class to create a multi-agent real-time voice interaction system where two AI agents can have autonomous conversations without user input.\n\n## Features\n\n- 🗣️ **Real-time Voice Interaction**: Two agents communicate through voice in real-time\n- 🤖 **Autonomous Conversation**: Agents converse with each other without user intervention\n- ⚙️ **Customizable Configuration**: Configure agent names and instructions through the web interface\n- 🎨 **Modern UI**: Clean, shadcn-inspired interface for easy interaction\n- 📊 **Live Transcript**: See the conversation transcripts in real-time\n\n## Architecture\n\nThe example uses:\n- **Backend**: FastAPI server with WebSocket support\n- **Frontend**: HTML5 with Web Audio API for audio playback\n- **AgentScope Components**:\n  - `ChatRoom`: Manages multiple `RealtimeAgent` instances\n  - `RealtimeAgent`: Handles real-time voice interaction with AI models\n  - `DashScopeRealtimeModel`: DashScope's Qwen3-Omni realtime model\n\n## Prerequisites\n\n1. **Python Dependencies**:\n   ```bash\n   pip install agentscope[dashscope]\n   pip install fastapi uvicorn\n   ```\n\n2. **DashScope API Key**:\n   - Set your DashScope API key as an environment variable:\n     ```bash\n     export DASHSCOPE_API_KEY=\"your-api-key-here\"\n     ```\n\n## Usage\n\n1. **Start the Server**:\n   ```bash\n   python run_server.py\n   ```\n\n2. **Open the Web Interface**:\n   - Navigate to `http://localhost:8000` in your web browser\n\n3. **Configure Agents**:\n   - Set names and instructions for both Agent 1 and Agent 2\n   - Example configurations:\n     - **Agent 1 (Alice)**: \"You are Alice, a cheerful and optimistic person who loves to share stories and ask questions. Keep your responses brief and conversational.\"\n     - **Agent 2 (Bob)**: \"You are Bob, a thoughtful and analytical person who enjoys deep conversations. Keep your responses brief and conversational.\"\n\n4. **Start the Conversation**:\n   - Click the \"▶️ Start Conversation\" button\n   - The agents will begin conversing autonomously\n   - You'll see transcripts and system messages in the message panel\n   - Audio playback will stream in real-time\n\n5. **Stop the Conversation**:\n   - Click the \"⏹️ Stop Conversation\" button when you want to end the session\n\n## How It Works\n\n### Backend Flow\n\n1. **WebSocket Connection**: Client connects via WebSocket to `/ws/{user_id}/{session_id}`\n2. **Session Creation**:\n   - Client sends `client_session_create` event with agent configurations\n   - Server creates two `RealtimeAgent` instances with specified names and instructions\n   - Server creates a `ChatRoom` with both agents\n   - Server starts the chat room and returns `session_created` event\n3. **Message Broadcasting**:\n   - `ChatRoom` automatically broadcasts messages between agents\n   - All events (audio, transcripts, etc.) are forwarded to the frontend\n4. **Session End**: Client sends `client_session_end` event to stop the conversation\n\n### Frontend Flow\n\n1. **WebSocket Setup**: Establishes connection and waits for server events\n2. **Session Management**: Sends configuration and manages conversation state\n3. **Audio Playback**:\n   - Receives base64-encoded PCM16 audio chunks\n   - Decodes and queues audio data\n   - Uses Web Audio API `ScriptProcessorNode` for streaming playback at 24kHz\n4. **Transcript Display**: Shows real-time transcripts from both agents\n\n## Key Components\n\n### ChatRoom\n\nThe `ChatRoom` class manages multiple `RealtimeAgent` instances:\n- Establishes connections for all agents\n- Broadcasts messages between agents automatically\n- Forwards events to the frontend\n- Handles lifecycle management (start/stop)\n\n### RealtimeAgent\n\nEach `RealtimeAgent`:\n- Connects to the DashScope realtime API\n- Processes audio input from other agents\n- Generates voice responses\n- Emits events for transcripts, audio, and status updates\n\n## Customization\n\n### Changing the Model\n\nTo use a different model, modify the `DashScopeRealtimeModel` configuration in `run_server.py`:\n\n```python\nmodel=DashScopeRealtimeModel(\n    model_name=\"your-model-name\",\n    api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n)\n```\n\n### Adding More Agents\n\nTo add more agents, modify the agent creation section in `run_server.py`:\n\n```python\nagent3 = RealtimeAgent(\n    name=agent3_name,\n    sys_prompt=agent3_instructions,\n    model=DashScopeRealtimeModel(\n        model_name=\"qwen3-omni-flash-realtime\",\n        api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n    ),\n)\n\nchat_room = ChatRoom(agents=[agent1, agent2, agent3])\n```\n\nAnd update the frontend to include configuration fields for the additional agents.\n\n## Troubleshooting\n\n### No Audio Playback\n- Ensure your browser supports Web Audio API\n- Check browser console for audio-related errors\n- Verify the audio format matches the expected PCM16 at 24kHz\n\n### Connection Issues\n- Verify your DashScope API key is set correctly\n- Check that port 8000 is not blocked by firewall\n- Review server logs for error messages\n\n### Agents Not Responding\n- Ensure both agent configurations have valid instructions\n- Check that the instructions encourage conversational behavior\n- Review the console logs for API errors\n\n## References\n\n- [AgentScope Documentation](https://modelscope.github.io/agentscope/)\n- [DashScope API Documentation](https://help.aliyun.com/zh/model-studio/)\n- [FastAPI Documentation](https://fastapi.tiangolo.com/)\n- [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)\n\n"
  },
  {
    "path": "examples/workflows/multiagent_realtime/multi_agent.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <title>Multi-Agent Realtime Voice Interaction</title>\n    <meta charset=\"UTF-8\">\n    <style>\n        * {\n            margin: 0;\n            padding: 0;\n            box-sizing: border-box;\n        }\n\n        body {\n            font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n            max-width: 1200px;\n            margin: 0 auto;\n            padding: 2rem;\n            background: hsl(0, 0%, 98%);\n            color: hsl(222.2, 84%, 4.9%);\n            line-height: 1.5;\n        }\n\n        h1 {\n            font-size: 2rem;\n            font-weight: 600;\n            margin-bottom: 0.5rem;\n            color: hsl(222.2, 84%, 4.9%);\n            letter-spacing: -0.025em;\n        }\n\n        .subtitle {\n            font-size: 0.875rem;\n            color: hsl(215.4, 16.3%, 46.9%);\n            margin-bottom: 1.5rem;\n        }\n\n        #messages {\n            border: 1px solid hsl(214.3, 31.8%, 91.4%);\n            height: 400px;\n            overflow-y: auto;\n            padding: 1rem;\n            margin: 1.5rem 0;\n            background: hsl(0, 0%, 100%);\n            border-radius: 0.5rem;\n            box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);\n        }\n\n        #messages::-webkit-scrollbar {\n            width: 8px;\n        }\n\n        #messages::-webkit-scrollbar-track {\n            background: hsl(210, 40%, 96.1%);\n            border-radius: 4px;\n        }\n\n        #messages::-webkit-scrollbar-thumb {\n            background: hsl(215.4, 16.3%, 56.9%);\n            border-radius: 4px;\n        }\n\n        #messages::-webkit-scrollbar-thumb:hover {\n            background: hsl(215.4, 16.3%, 46.9%);\n        }\n\n        input[type=\"text\"] {\n            width: 100%;\n            padding: 0.625rem 0.875rem;\n            font-size: 0.875rem;\n            border: 1px solid hsl(214.3, 31.8%, 91.4%);\n            border-radius: 0.375rem;\n            background: hsl(0, 0%, 100%);\n            color: hsl(222.2, 84%, 4.9%);\n            transition: all 0.15s ease;\n            outline: none;\n        }\n\n        input[type=\"text\"]:focus {\n            border-color: hsl(221.2, 83.2%, 53.3%);\n            box-shadow: 0 0 0 3px hsl(221.2, 83.2%, 53.3%, 0.1);\n        }\n\n        textarea {\n            width: 100%;\n            min-height: 80px;\n            padding: 0.625rem 0.875rem;\n            font-size: 0.875rem;\n            border: 1px solid hsl(214.3, 31.8%, 91.4%);\n            border-radius: 0.375rem;\n            background: hsl(0, 0%, 100%);\n            color: hsl(222.2, 84%, 4.9%);\n            transition: all 0.15s ease;\n            outline: none;\n            resize: vertical;\n            font-family: inherit;\n            line-height: 1.5;\n        }\n\n        textarea:focus {\n            border-color: hsl(221.2, 83.2%, 53.3%);\n            box-shadow: 0 0 0 3px hsl(221.2, 83.2%, 53.3%, 0.1);\n        }\n\n        button {\n            display: inline-flex;\n            align-items: center;\n            justify-content: center;\n            padding: 0.625rem 1rem;\n            font-size: 0.875rem;\n            font-weight: 500;\n            border: none;\n            border-radius: 0.375rem;\n            cursor: pointer;\n            transition: all 0.15s ease;\n            background: hsl(222.2, 47.4%, 11.2%);\n            color: hsl(210, 40%, 98%);\n            box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);\n        }\n\n        button:hover:not(:disabled) {\n            background: hsl(222.2, 47.4%, 15%);\n        }\n\n        button:active:not(:disabled) {\n            transform: scale(0.98);\n        }\n\n        button:focus-visible {\n            outline: 2px solid hsl(221.2, 83.2%, 53.3%);\n            outline-offset: 2px;\n        }\n\n        button:disabled {\n            opacity: 0.5;\n            cursor: not-allowed;\n        }\n\n        button.primary {\n            background: hsl(221.2, 83.2%, 53.3%);\n            color: hsl(0, 0%, 100%);\n        }\n\n        button.primary:hover:not(:disabled) {\n            background: hsl(221.2, 83.2%, 48%);\n        }\n\n        button.destructive {\n            background: hsl(0, 84.2%, 60.2%);\n            color: hsl(0, 0%, 100%);\n        }\n\n        button.destructive:hover:not(:disabled) {\n            background: hsl(0, 84.2%, 55%);\n        }\n\n        button.active {\n            background: hsl(142.1, 76.2%, 36.3%);\n            animation: pulse 1.5s ease-in-out infinite;\n        }\n\n        @keyframes pulse {\n            0%, 100% {\n                opacity: 1;\n                box-shadow: 0 0 0 0 hsl(142.1, 76.2%, 36.3%, 0.7);\n            }\n            50% {\n                opacity: 0.9;\n                box-shadow: 0 0 0 8px hsl(142.1, 76.2%, 36.3%, 0);\n            }\n        }\n\n        .message {\n            margin: 0.75rem 0;\n            padding: 0.75rem 1rem;\n            background: hsl(210, 40%, 98%);\n            border-radius: 0.5rem;\n            border: 1px solid hsl(214.3, 31.8%, 91.4%);\n            font-size: 0.875rem;\n        }\n\n        .message strong {\n            color: hsl(222.2, 47.4%, 11.2%);\n            font-weight: 600;\n        }\n\n        .controls {\n            display: flex;\n            flex-wrap: wrap;\n            gap: 0.75rem;\n            margin: 1.5rem 0;\n        }\n\n        .configuration-container {\n            background: hsl(0, 0%, 100%);\n            padding: 1.5rem;\n            border-radius: 0.5rem;\n            margin: 1.5rem 0;\n            border: 1px solid hsl(214.3, 31.8%, 91.4%);\n            box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);\n        }\n\n        .configuration-container h3 {\n            font-size: 1.125rem;\n            font-weight: 600;\n            margin-bottom: 1rem;\n            color: hsl(222.2, 84%, 4.9%);\n            display: flex;\n            align-items: center;\n            gap: 0.5rem;\n        }\n\n        .agents-grid {\n            display: grid;\n            grid-template-columns: 1fr 1fr;\n            gap: 1.5rem;\n            margin-bottom: 1.5rem;\n        }\n\n        .agent-config {\n            padding: 1rem;\n            background: hsl(210, 40%, 98%);\n            border-radius: 0.5rem;\n            border: 1px solid hsl(214.3, 31.8%, 91.4%);\n        }\n\n        .agent-config h4 {\n            font-size: 1rem;\n            font-weight: 600;\n            margin-bottom: 0.75rem;\n            color: hsl(222.2, 47.4%, 11.2%);\n        }\n\n        .config-field {\n            margin-bottom: 1rem;\n        }\n\n        .config-field:last-child {\n            margin-bottom: 0;\n        }\n\n        .config-field label {\n            display: block;\n            font-weight: 500;\n            margin-bottom: 0.5rem;\n            color: hsl(222.2, 47.4%, 11.2%);\n            font-size: 0.875rem;\n        }\n\n        .error-message {\n            padding: 0.875rem 1rem;\n            background: hsl(0, 84.2%, 95%);\n            border: 1px solid hsl(0, 84.2%, 85%);\n            border-radius: 0.5rem;\n            margin: 1rem 0;\n            display: none;\n            color: hsl(0, 84.2%, 30%);\n            font-size: 0.875rem;\n            font-weight: 500;\n            box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);\n        }\n\n        .model-options {\n            display: flex;\n            flex-direction: row;\n            gap: 0.75rem;\n        }\n\n        .model-option {\n            flex: 1;\n            padding: 0.75rem;\n            border: 2px solid hsl(214.3, 31.8%, 91.4%);\n            border-radius: 0.5rem;\n            cursor: pointer;\n            transition: all 0.15s ease;\n            background: hsl(0, 0%, 100%);\n            display: flex;\n            flex-direction: column;\n            justify-content: center;\n            gap: 0.5rem;\n        }\n\n        .model-option:hover:not(.disabled) {\n            border-color: hsl(221.2, 83.2%, 53.3%);\n            background: hsl(221.2, 83.2%, 98%);\n        }\n\n        .model-option.selected {\n            border-color: hsl(221.2, 83.2%, 53.3%);\n            background: hsl(221.2, 83.2%, 95%);\n        }\n\n        .model-option.disabled {\n            opacity: 0.5;\n            cursor: not-allowed;\n            background: hsl(0, 0%, 98%);\n        }\n\n        .model-option-header {\n            display: flex;\n            align-items: center;\n            gap: 0.5rem;\n        }\n\n        .model-option input[type=\"radio\"] {\n            margin: 0;\n            cursor: pointer;\n            flex-shrink: 0;\n        }\n\n        .model-option.disabled input[type=\"radio\"] {\n            cursor: not-allowed;\n        }\n\n        .model-info {\n            display: flex;\n            flex-direction: column;\n            gap: 0.5rem;\n            flex: 1;\n        }\n\n        .model-name-line {\n            display: flex;\n            align-items: center;\n            gap: 0.5rem;\n            min-height: 1.25rem;\n        }\n\n        .model-name {\n            font-weight: 600;\n            color: hsl(222.2, 84%, 4.9%);\n        }\n\n        .model-unavailable-reason {\n            font-size: 0.625rem;\n            color: hsl(215.4, 16.3%, 56.9%);\n            font-style: italic;\n            white-space: nowrap;\n        }\n\n        @media (max-width: 768px) {\n            .agents-grid {\n                grid-template-columns: 1fr;\n            }\n        }\n\n\n\n        .audio-mode-selector {\n            margin-bottom: 1rem;\n        }\n\n        .audio-mode-selector label {\n            display: block;\n            font-weight: 500;\n            margin-bottom: 0.5rem;\n            color: hsl(222.2, 47.4%, 11.2%);\n            font-size: 0.875rem;\n        }\n\n        .preset-group {\n            display: flex;\n            gap: 1rem;\n            flex-wrap: wrap;\n        }\n\n        .preset-card {\n            flex: 1;\n            min-width: 200px;\n            padding: 1rem 1.25rem;\n            background: hsl(0, 0%, 100%);\n            border: 2px solid hsl(214.3, 31.8%, 91.4%);\n            border-radius: 0.5rem;\n            cursor: pointer;\n            transition: all 0.2s ease;\n            text-align: center;\n            font-weight: 500;\n            font-size: 0.875rem;\n            color: hsl(222.2, 84%, 4.9%);\n            user-select: none;\n        }\n\n        .preset-card:hover {\n            border-color: hsl(221.2, 83.2%, 53.3%);\n            background: hsl(210, 40%, 98%);\n            transform: translateY(-2px);\n            box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);\n        }\n\n        .preset-card.selected {\n            background: hsl(221.2, 83.2%, 53.3%);\n            border-color: hsl(221.2, 83.2%, 53.3%);\n            color: hsl(0, 0%, 100%);\n            box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.2), 0 2px 4px -2px rgb(0 0 0 / 0.2);\n        }\n\n        .preset-card.selected:hover {\n            background: hsl(221.2, 83.2%, 48%);\n            border-color: hsl(221.2, 83.2%, 48%);\n            transform: translateY(-2px);\n        }\n    </style>\n</head>\n<body>\n    <h1>🗣️ Multi-Agent Realtime Voice Interaction</h1>\n    <p class=\"subtitle\">Two AI agents having a real-time voice conversation using AgentScope ChatRoom</p>\n\n    <div class=\"configuration-container\">\n        <h3>⚙️ Agent Configuration</h3>\n\n\n        <div class=\"audio-mode-selector\">\n            <label>🎭 Debate Preset</label>\n            <div class=\"preset-group\">\n                <div class=\"preset-card\" data-preset=\"ai\" onclick=\"selectPreset('ai')\">\n                    🤖 AI Threat v.s. AI Optimism (en)\n                </div>\n                <div class=\"preset-card\" data-preset=\"human\" onclick=\"selectPreset('human')\">\n                    👥 人性本恶 v.s. 人性本善 (zh)\n                </div>\n            </div>\n        </div>\n\n        <div class=\"config-field\">\n            <label>Model Provider</label>\n            <div class=\"model-options\" id=\"modelOptions\">\n                <label class=\"model-option\" data-provider=\"dashscope\">\n                    <div class=\"model-option-header\">\n                        <input type=\"radio\" name=\"modelProvider\" value=\"dashscope\" checked />\n                        <div class=\"model-info\">\n                            <div class=\"model-name-line\">\n                                <span class=\"model-name\">DashScope</span>\n                                <span class=\"model-unavailable-reason\" style=\"display: none;\"></span>\n                            </div>\n                        </div>\n                    </div>\n                </label>\n                <label class=\"model-option\" data-provider=\"gemini\">\n                    <div class=\"model-option-header\">\n                        <input type=\"radio\" name=\"modelProvider\" value=\"gemini\" />\n                        <div class=\"model-info\">\n                            <div class=\"model-name-line\">\n                                <span class=\"model-name\">Gemini</span>\n                                <span class=\"model-unavailable-reason\" style=\"display: none;\"></span>\n                            </div>\n                        </div>\n                    </div>\n                </label>\n                <label class=\"model-option\" data-provider=\"openai\">\n                    <div class=\"model-option-header\">\n                        <input type=\"radio\" name=\"modelProvider\" value=\"openai\" />\n                        <div class=\"model-info\">\n                            <div class=\"model-name-line\">\n                                <span class=\"model-name\">OpenAI</span>\n                                <span class=\"model-unavailable-reason\" style=\"display: none;\"></span>\n                            </div>\n                        </div>\n                    </div>\n                </label>\n            </div>\n        </div>\n\n        <div class=\"agents-grid\">\n            <div class=\"agent-config\">\n                <h4>🤖 Agent 1</h4>\n                <div class=\"config-field\">\n                    <label for=\"agent1Name\">Name</label>\n                    <input type=\"text\" id=\"agent1Name\" placeholder=\"Enter agent 1 name\" value=\"Elon Musk\" />\n                </div>\n                <div class=\"config-field\">\n                    <label for=\"agent1Instructions\">Instructions</label>\n                    <textarea id=\"agent1Instructions\" placeholder=\"Enter agent 1 instructions...\">You're Elon Musk arguing with Mark Zuckerberg about whether AI is humanity's greatest threat. You believe it absolutely is - we're building superintelligence without safeguards, risking autonomous weapons, mass job loss, and losing control to machines smarter than us. Be bold, direct, and warn that optimists like Zuck are dangerously naive. Use your trademark sarcasm and urgency. You must keep each response under 100 words.</textarea>\n                </div>\n            </div>\n\n            <div class=\"agent-config\">\n                <h4>🤖 Agent 2</h4>\n                <div class=\"config-field\">\n                    <label for=\"agent2Name\">Name</label>\n                    <input type=\"text\" id=\"agent2Name\" placeholder=\"Enter agent 2 name\" value=\"Mark Zuckerberg\" />\n                </div>\n                <div class=\"config-field\">\n                    <label for=\"agent2Instructions\">Instructions</label>\n                    <textarea id=\"agent2Instructions\" placeholder=\"Enter agent 2 instructions...\">You're Mark Zuckerberg arguing with Elon Musk about whether AI is humanity's greatest threat. You believe it's not - climate change, pandemics, and inequality are bigger dangers, while AI is already saving lives and solving problems. Be calm, rational, and data-driven. Counter Elon's fearmongering with practical AI benefits and argue responsible development beats fear-based thinking.</textarea>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <div id=\"errorMessage\" class=\"error-message\"></div>\n\n    <div class=\"controls\">\n        <button id=\"startBtn\" class=\"primary\" onclick=\"startConversation()\">▶️ Start Conversation</button>\n        <button id=\"stopBtn\" class=\"destructive\" onclick=\"stopConversation()\" disabled>⏹️ Stop Conversation</button>\n    </div>\n\n    <div id=\"messages\"></div>\n\n    <script>\n        let ws = null;\n        let playbackAudioContext = null;  // For playback, 24kHz\n\n        // Audio queue management: sequential playback\n        let globalAudioQueue = [];  // Array of { agentId, agentName, chunks, messageElement }\n        let currentlyPlaying = null;  // Currently playing agent info\n        let isPlayingGlobal = false;\n\n        let sessionId = \"session1\";  // Session ID\n        let sessionCreated = false;  // Track if session has been created\n        let isConversationActive = false;\n\n        // Used to accumulate transcript text\n        let currentResponseTranscripts = {};  // Store by agent_id\n\n\n        // Silent audio monitoring for continuous audio stream\n        let lastAudioReceivedTime = null;\n        let silentAudioCheckInterval = null;\n        const SILENT_AUDIO_CHECK_INTERVAL = 2000; // Check every 1000ms (1 second)\n        const SILENT_AUDIO_THRESHOLD = 2000; // Send silent audio if no audio received for 200ms\n\n        // Debate presets\n        const debatePresets = {\n            ai: {\n                agent1: {\n                    name: \"Elon Musk\",\n                    instructions: \"You're Elon Musk arguing with Mark Zuckerberg about whether AI is humanity's greatest threat. You believe it absolutely is - we're building superintelligence without safeguards, risking autonomous weapons, mass job loss, and losing control to machines smarter than us. Be bold, direct, and warn that optimists like Zuck are dangerously naive. Use your trademark sarcasm and urgency. You must keep each response under 100 words.\"\n                },\n                agent2: {\n                    name: \"Mark Zuckerberg\",\n                    instructions: \"You're Mark Zuckerberg arguing with Elon Musk about whether AI is humanity's greatest threat. You believe it's not - climate change, pandemics, and inequality are bigger dangers, while AI is already saving lives and solving problems. Be calm, rational, and data-driven. Counter Elon's fearmongering with practical AI benefits and argue responsible development beats fear-based thinking.\"\n                }\n            },\n            human: {\n                agent1: {\n                    name: \"郭德纲\",\n                    instructions: \"你是郭德纲，你要和于谦进行一场关于\\\"人性本恶\\\"的辩论。你坚定地认为人性本恶，人生来就有自私、贪婪等恶的一面，只有通过教育、道德约束和社会规范才能引导人向善。用你幽默风趣的语言风格，结合相声的表达方式，用生动的例子和机智的话语来论证你的观点。每次回应控制在100字以内。\"\n                },\n                agent2: {\n                    name: \"于谦\",\n                    instructions: \"你是于谦，你要和郭德纲进行一场关于\\\"人性本善\\\"的辩论。你坚信人性本善，人生来就有善良、同情等美好品质，恶是后天环境影响造成的。用你朴实、真诚的语言风格，举出人性光辉的例子来反驳郭德纲的观点。保持相声捧哏的风格，既要论证有力，又要配合好逗哏。每次回应控制在100字以内。\"\n                }\n            }\n        };\n\n        // Track currently selected preset\n        let currentPreset = null;\n\n        function selectPreset(presetType) {\n            if (!debatePresets[presetType]) return;\n\n            // Update selected state\n            currentPreset = presetType;\n            document.querySelectorAll('.preset-card').forEach(card => {\n                card.classList.remove('selected');\n            });\n            document.querySelector(`.preset-card[data-preset=\"${presetType}\"]`).classList.add('selected');\n\n            // Load preset values\n            const preset = debatePresets[presetType];\n            document.getElementById(\"agent1Name\").value = preset.agent1.name;\n            document.getElementById(\"agent1Instructions\").value = preset.agent1.instructions;\n            document.getElementById(\"agent2Name\").value = preset.agent2.name;\n            document.getElementById(\"agent2Instructions\").value = preset.agent2.instructions;\n\n            console.log(`Loaded ${presetType} debate preset`);\n        }\n\n        function clearPresetSelection() {\n            currentPreset = null;\n            document.querySelectorAll('.preset-card').forEach(card => {\n                card.classList.remove('selected');\n            });\n        }\n\n        // Monitor input changes to clear preset selection\n        function setupInputMonitoring() {\n            const inputs = [\n                document.getElementById(\"agent1Name\"),\n                document.getElementById(\"agent1Instructions\"),\n                document.getElementById(\"agent2Name\"),\n                document.getElementById(\"agent2Instructions\")\n            ];\n\n            inputs.forEach(input => {\n                input.addEventListener('input', () => {\n                    // Check if current values match any preset\n                    if (currentPreset && debatePresets[currentPreset]) {\n                        const preset = debatePresets[currentPreset];\n                        const agent1Name = document.getElementById(\"agent1Name\").value;\n                        const agent1Instructions = document.getElementById(\"agent1Instructions\").value;\n                        const agent2Name = document.getElementById(\"agent2Name\").value;\n                        const agent2Instructions = document.getElementById(\"agent2Instructions\").value;\n\n                        // If values don't match preset, clear selection\n                        if (agent1Name !== preset.agent1.name ||\n                            agent1Instructions !== preset.agent1.instructions ||\n                            agent2Name !== preset.agent2.name ||\n                            agent2Instructions !== preset.agent2.instructions) {\n                            clearPresetSelection();\n                        }\n                    }\n                });\n            });\n        }\n\n        function showError(message) {\n            const errorDiv = document.getElementById(\"errorMessage\");\n            errorDiv.innerText = message;\n            errorDiv.style.display = \"block\";\n            setTimeout(() => {\n                errorDiv.style.display = \"none\";\n            }, 5000);\n        }\n\n        function generateSilentAudio(durationMs, sampleRate) {\n            // Calculate number of samples needed\n            const numSamples = Math.floor(sampleRate * durationMs / 1000);\n            // Create Int16Array filled with zeros (silence)\n            const int16Array = new Int16Array(numSamples);\n            return int16Array.buffer;\n        }\n\n        function arrayBufferToBase64(buffer) {\n            const bytes = new Uint8Array(buffer);\n            let binary = '';\n            for (let i = 0; i < bytes.byteLength; i++) {\n                binary += String.fromCharCode(bytes[i]);\n            }\n            return btoa(binary);\n        }\n\n\n        function startSilentAudioMonitoring() {\n            // Initialize timestamp\n            lastAudioReceivedTime = Date.now();\n\n            // Start periodic check\n            silentAudioCheckInterval = setInterval(() => {\n                checkAndSendSilentAudio();\n            }, SILENT_AUDIO_CHECK_INTERVAL);\n\n            console.log(\"Started silent audio monitoring\");\n        }\n\n        function stopSilentAudioMonitoring() {\n            if (silentAudioCheckInterval) {\n                clearInterval(silentAudioCheckInterval);\n                silentAudioCheckInterval = null;\n            }\n            lastAudioReceivedTime = null;\n            console.log(\"Stopped silent audio monitoring\");\n        }\n\n        function checkAndSendSilentAudio() {\n            // Check if session is active\n            if (!isConversationActive || !ws || ws.readyState !== WebSocket.OPEN) {\n                return;\n            }\n\n            // Check if any audio is playing\n            if (isPlayingGlobal) {\n                // If audio is playing, update timestamp (playing also counts as audio activity)\n                lastAudioReceivedTime = Date.now();\n                return;\n            }\n\n            // Calculate time since last audio received\n            const now = Date.now();\n            const timeSinceLastAudio = now - lastAudioReceivedTime;\n\n            // If threshold exceeded, send silent audio\n            if (timeSinceLastAudio >= SILENT_AUDIO_THRESHOLD) {\n                sendSilentAudio(timeSinceLastAudio);\n                // Update timestamp\n                lastAudioReceivedTime = now;\n            }\n        }\n\n        function sendSilentAudio(durationMs) {\n            if (!ws || ws.readyState !== WebSocket.OPEN) {\n                return;\n            }\n\n            // Limit maximum duration to avoid sending too large data\n            const maxDuration = 2000; // Max 1 second\n            const actualDuration = Math.min(durationMs, maxDuration);\n\n            // Generate silent audio\n            const silentBuffer = generateSilentAudio(actualDuration, 16000);\n            const base64Audio = arrayBufferToBase64(silentBuffer);\n\n            // Send audio data\n            ws.send(JSON.stringify({\n                type: \"client_audio_append\",\n                session_id: sessionId,\n                audio: base64Audio,\n                format: {\n                    rate: 16000,\n                    type: \"audio/pcm\"\n                }\n            }));\n\n            console.log(`Sent ${actualDuration}ms silent audio to backend`);\n        }\n\n\n        async function connect() {\n            const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n            const host = window.location.hostname;\n            const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80');\n            const wsUrl = `${protocol}//${host}:${port}/ws/user1/${sessionId}`;\n\n            console.log(`Connecting to WebSocket: ${wsUrl}`);\n            ws = new WebSocket(wsUrl);\n\n            ws.onopen = function(event) {\n                addMessage(\"System\", \"✅ Connected to server\");\n            };\n\n            ws.onmessage = async function(event) {\n                try {\n                    const data = JSON.parse(event.data);\n                    console.log(\"Received message:\", data);\n\n                    // Handle ServerEvents\n                    switch (data.type) {\n                        case \"server_session_created\":\n                            sessionCreated = true;\n                            addMessage(\"System\", `✅ Chat room created: ${data.session_id}`);\n                            break;\n\n                        case \"agent_ready\":\n                            addMessage(\"System\", `🤖 Agent ${data.agent_name} is ready`);\n                            break;\n\n                        case \"agent_response_created\":\n                            // Initialize audio buffer for this response (no message display)\n                            if (data.agent_id) {\n                                initializeAudioBuffer(data.agent_id, data.agent_name);\n                            }\n                            break;\n\n                        case \"agent_response_audio_delta\":\n                            // Update last audio received timestamp\n                            lastAudioReceivedTime = Date.now();\n\n                            // Receive audio data\n                            console.log(`Received audio delta from agent: ${data.agent_name} (${data.agent_id})`);\n                            if (data.agent_id) {\n                                queueAudioChunk(data.agent_id, data.agent_name, data.delta);\n                            }\n                            break;\n\n                        case \"agent_response_audio_done\":\n                            // Mark audio complete (no message display)\n                            if (data.agent_id) {\n                                markAudioComplete(data.agent_id);\n                            }\n                            break;\n\n                        case \"agent_response_audio_transcript_delta\":\n                            // Agent response transcript text\n                            appendResponseTranscript(\n                                data.agent_name,\n                                data.delta || \"\",\n                                data.agent_id\n                            );\n                            break;\n\n                        case \"agent_response_audio_transcript_done\":\n                            // Complete Agent response transcript message\n                            finishResponseTranscript(data.agent_id);\n                            break;\n\n                        case \"agent_response_done\":\n                            // Response completed (no message display)\n                            break;\n\n                        case \"agent_error\":\n                            addMessage(\"Error\", `❌ ${data.error_type}: ${data.message}`);\n                            break;\n\n                        case \"agent_ended\":\n                            addMessage(\"System\", `👋 Agent ${data.agent_name} has ended`);\n                            break;\n\n                        case \"server_session_ended\":\n                            addMessage(\"System\", `🔚 Session ${data.session_id} has ended`);\n                            break;\n\n                        default:\n                            console.log(\"Unhandled event type:\", data.type);\n                            break;\n                    }\n                } catch (e) {\n                    console.error(\"Error processing message:\", e);\n                }\n            };\n\n            ws.onclose = function(event) {\n                addMessage(\"System\", \"❌ Disconnected\");\n                stopSilentAudioMonitoring();\n                sessionCreated = false;\n                isConversationActive = false;\n                updateButtons();\n            };\n\n            ws.onerror = function(error) {\n                addMessage(\"System\", \"⚠️ Connection error\");\n            };\n        }\n\n        async function startConversation() {\n            try {\n                // Validate inputs\n                const agent1Name = document.getElementById(\"agent1Name\").value.trim();\n                const agent1Instructions = document.getElementById(\"agent1Instructions\").value.trim();\n                const agent2Name = document.getElementById(\"agent2Name\").value.trim();\n                const agent2Instructions = document.getElementById(\"agent2Instructions\").value.trim();\n\n                if (!agent1Name || !agent1Instructions || !agent2Name || !agent2Instructions) {\n                    showError(\"⚠️ All fields are required! Please fill in all agent configurations.\");\n                    return;\n                }\n\n                // Check if WebSocket is connected\n                if (!ws || ws.readyState !== WebSocket.OPEN) {\n                    showError(\"⚠️ WebSocket is not connected! Please wait for connection.\");\n                    return;\n                }\n\n                // Disable start button\n                isConversationActive = true;\n                updateButtons();\n\n                // Get selected model provider\n                const selectedModel = document.querySelector('input[name=\"modelProvider\"]:checked');\n                const modelProvider = selectedModel ? selectedModel.value : \"dashscope\";\n\n                // Send session create event\n                addMessage(\"System\", \"📝 Creating chat room...\");\n                ws.send(JSON.stringify({\n                    type: \"client_session_create\",\n                    config: {\n                        agent1_name: agent1Name,\n                        agent1_instructions: agent1Instructions,\n                        agent2_name: agent2Name,\n                        agent2_instructions: agent2Instructions,\n                        model_provider: modelProvider\n                    }\n                }));\n\n                // Wait for session_created event\n                await new Promise((resolve, reject) => {\n                    const timeout = setTimeout(() => {\n                        reject(new Error(\"Session creation timeout\"));\n                    }, 10000);\n\n                    const checkSession = setInterval(() => {\n                        if (sessionCreated) {\n                            clearTimeout(timeout);\n                            clearInterval(checkSession);\n                            resolve();\n                        }\n                    }, 100);\n                });\n\n                addMessage(\"System\", \"🎉 Chat room created! Agents are now conversing...\");\n\n                // Start silent audio monitoring\n                startSilentAudioMonitoring();\n\n            } catch (err) {\n                console.error(\"Failed to start conversation:\", err);\n                if (err.message === \"Session creation timeout\") {\n                    showError(\"⚠️ Session creation timeout. Please try again.\");\n                    addMessage(\"System\", \"⚠️ Session creation timeout\");\n                } else {\n                    showError(\"⚠️ Failed to start conversation: \" + err.message);\n                    addMessage(\"System\", \"⚠️ Failed to start conversation: \" + err.message);\n                }\n                isConversationActive = false;\n                updateButtons();\n            }\n        }\n\n        function stopConversation() {\n            // Stop silent audio monitoring\n            stopSilentAudioMonitoring();\n\n            if (ws && ws.readyState === WebSocket.OPEN) {\n                ws.send(JSON.stringify({\n                    type: \"client_session_end\",\n                    session_id: sessionId\n                }));\n            }\n\n            stopAllAudioPlayback();\n            sessionCreated = false;\n            isConversationActive = false;\n            updateButtons();\n            addMessage(\"System\", \"⏹️ Conversation stopped\");\n        }\n\n        function updateButtons() {\n            document.getElementById(\"startBtn\").disabled = isConversationActive;\n            document.getElementById(\"stopBtn\").disabled = !isConversationActive;\n\n            if (isConversationActive) {\n                document.getElementById(\"startBtn\").classList.remove(\"primary\");\n                document.getElementById(\"stopBtn\").classList.add(\"active\");\n            } else {\n                document.getElementById(\"startBtn\").classList.add(\"primary\");\n                document.getElementById(\"stopBtn\").classList.remove(\"active\");\n            }\n        }\n\n        // ==================== Audio Queue Management ====================\n\n        function initializeAudioBuffer(agentId, agentName) {\n            // Find if this agent already has an entry in the queue\n            let queueEntry = globalAudioQueue.find(entry => entry.agentId === agentId && !entry.isComplete);\n\n            if (!queueEntry) {\n                queueEntry = {\n                    agentId: agentId,\n                    agentName: agentName,\n                    chunks: [],\n                    messageElement: null,\n                    isComplete: false,\n                    playbackIndex: 0\n                };\n                globalAudioQueue.push(queueEntry);\n                console.log(`Initialized audio buffer for agent: ${agentName} (${agentId})`);\n            }\n        }\n\n        function queueAudioChunk(agentId, agentName, base64Audio) {\n            try {\n                // Decode audio chunk\n                const float32Array = decodeAudioChunk(base64Audio);\n\n                // Find or create queue entry for this agent\n                let queueEntry = globalAudioQueue.find(entry => entry.agentId === agentId && !entry.isComplete);\n\n                if (!queueEntry) {\n                    queueEntry = {\n                        agentId: agentId,\n                        agentName: agentName,\n                        chunks: [],\n                        messageElement: null,\n                        isComplete: false,\n                        playbackIndex: 0\n                    };\n                    globalAudioQueue.push(queueEntry);\n                }\n\n                queueEntry.chunks.push(float32Array);\n                console.log(`Queued audio chunk for ${agentName}, total chunks: ${queueEntry.chunks.length}`);\n\n\n                // If not currently playing, start playback\n                if (!isPlayingGlobal) {\n                    playNextInQueue();\n                }\n            } catch (err) {\n                console.error(\"Failed to queue audio chunk:\", err);\n            }\n        }\n\n        function markAudioComplete(agentId) {\n            const queueEntry = globalAudioQueue.find(entry => entry.agentId === agentId && !entry.isComplete);\n            if (queueEntry) {\n                queueEntry.isComplete = true;\n                console.log(`Marked audio complete for agent: ${agentId}`);\n            }\n        }\n\n        function playNextInQueue() {\n            if (isPlayingGlobal) return;\n\n            // Find the first entry with audio chunks\n            const nextEntry = globalAudioQueue.find(entry => entry.chunks.length > 0);\n\n            if (!nextEntry) {\n                console.log(\"No audio in queue to play\");\n                return;\n            }\n\n            console.log(`Starting playback for agent: ${nextEntry.agentName}`);\n            currentlyPlaying = nextEntry;\n            isPlayingGlobal = true;\n\n            // Highlight the corresponding message\n            if (nextEntry.messageElement) {\n                nextEntry.messageElement.classList.add('playing-audio');\n            }\n\n\n            startPlayback(nextEntry);\n        }\n\n        function startPlayback(queueEntry) {\n            try {\n                if (!playbackAudioContext) {\n                    playbackAudioContext = new (window.AudioContext || window.webkitAudioContext)({\n                        sampleRate: 24000\n                    });\n                }\n\n                if (playbackAudioContext.state === 'suspended') {\n                    playbackAudioContext.resume();\n                }\n\n                const bufferSize = 4096;\n                const processor = playbackAudioContext.createScriptProcessor(bufferSize, 0, 1);\n\n                processor.onaudioprocess = function(e) {\n                    const output = e.outputBuffer.getChannelData(0);\n                    const samplesNeeded = output.length;\n                    let samplesWritten = 0;\n\n                    while (samplesWritten < samplesNeeded && queueEntry.chunks.length > 0) {\n                        const chunk = queueEntry.chunks[0];\n                        const samplesToRead = Math.min(\n                            samplesNeeded - samplesWritten,\n                            chunk.length - queueEntry.playbackIndex\n                        );\n\n                        for (let i = 0; i < samplesToRead; i++) {\n                            output[samplesWritten + i] = chunk[queueEntry.playbackIndex + i];\n                        }\n\n                        samplesWritten += samplesToRead;\n                        queueEntry.playbackIndex += samplesToRead;\n\n                        if (queueEntry.playbackIndex >= chunk.length) {\n                            queueEntry.chunks.shift();\n                            queueEntry.playbackIndex = 0;\n                        }\n                    }\n\n                    // Fill remaining with silence\n                    for (let i = samplesWritten; i < samplesNeeded; i++) {\n                        output[i] = 0;\n                    }\n\n                    // Check if this agent's audio is complete\n                    if (queueEntry.chunks.length === 0 && queueEntry.isComplete) {\n                        setTimeout(() => {\n                            if (queueEntry.chunks.length === 0 && queueEntry.isComplete) {\n                                finishCurrentPlayback();\n                            }\n                        }, 100);\n                    }\n                };\n\n                processor.connect(playbackAudioContext.destination);\n                queueEntry.node = processor;\n\n            } catch (err) {\n                console.error(\"Failed to start playback:\", err);\n                finishCurrentPlayback();\n            }\n        }\n\n        function finishCurrentPlayback() {\n            if (!currentlyPlaying) return;\n\n            console.log(`Finished playback for agent: ${currentlyPlaying.agentName}`);\n\n            // Cleanup\n            if (currentlyPlaying.node) {\n                currentlyPlaying.node.disconnect();\n                currentlyPlaying.node = null;\n            }\n\n            // Remove highlight\n            if (currentlyPlaying.messageElement) {\n                currentlyPlaying.messageElement.classList.remove('playing-audio');\n            }\n\n            // Remove from queue\n            const index = globalAudioQueue.indexOf(currentlyPlaying);\n            if (index > -1) {\n                globalAudioQueue.splice(index, 1);\n            }\n\n            currentlyPlaying = null;\n            isPlayingGlobal = false;\n\n            // Play next in queue\n            setTimeout(() => playNextInQueue(), 100);\n        }\n\n\n        // ==================== Common Audio Functions ====================\n\n        function decodeAudioChunk(base64Audio) {\n            const binaryString = atob(base64Audio);\n            const bytes = new Uint8Array(binaryString.length);\n            for (let i = 0; i < binaryString.length; i++) {\n                bytes[i] = binaryString.charCodeAt(i);\n            }\n\n            const int16Array = new Int16Array(bytes.buffer);\n            const float32Array = new Float32Array(int16Array.length);\n\n            for (let i = 0; i < int16Array.length; i++) {\n                float32Array[i] = int16Array[i] / 32768.0;\n            }\n\n            return float32Array;\n        }\n\n        function stopAllAudioPlayback() {\n            // Stop sequential playback\n            if (currentlyPlaying && currentlyPlaying.node) {\n                currentlyPlaying.node.disconnect();\n                currentlyPlaying.node = null;\n                if (currentlyPlaying.messageElement) {\n                    currentlyPlaying.messageElement.classList.remove('playing-audio');\n                }\n            }\n            currentlyPlaying = null;\n            isPlayingGlobal = false;\n            globalAudioQueue = [];\n        }\n\n        function addMessage(sender, message) {\n            const messagesDiv = document.getElementById(\"messages\");\n            const messageDiv = document.createElement(\"div\");\n            messageDiv.className = \"message\";\n            const time = new Date().toLocaleTimeString();\n            messageDiv.innerHTML = `<strong>[${time}] ${sender}:</strong> ${message}`;\n            messagesDiv.insertBefore(messageDiv, messagesDiv.firstChild);\n            messagesDiv.scrollTop = 0;\n        }\n\n        function appendResponseTranscript(agentName, text, agentId) {\n            const messagesDiv = document.getElementById(\"messages\");\n\n            // If there's no current response message element for this agent, create one\n            if (!currentResponseTranscripts[agentId]) {\n                currentResponseTranscripts[agentId] = {\n                    text: \"\",\n                    element: document.createElement(\"div\")\n                };\n\n                const transcript = currentResponseTranscripts[agentId];\n                transcript.element.className = \"message\";\n                const time = new Date().toLocaleTimeString();\n                transcript.element.innerHTML = `<strong>[${time}] ${agentName}:</strong> <span class=\"response-transcript-content\"></span>`;\n                messagesDiv.insertBefore(transcript.element, messagesDiv.firstChild);\n\n                // Link this message element to the audio queue entry\n                const queueEntry = globalAudioQueue.find(entry => entry.agentId === agentId && !entry.isComplete);\n                if (queueEntry) {\n                    queueEntry.messageElement = transcript.element;\n                }\n            }\n\n            // Accumulate text\n            currentResponseTranscripts[agentId].text += text;\n\n            // Update displayed content\n            const contentSpan = currentResponseTranscripts[agentId].element.querySelector('.response-transcript-content');\n            if (contentSpan) {\n                contentSpan.textContent = currentResponseTranscripts[agentId].text;\n            }\n\n            // Scroll to top\n            messagesDiv.scrollTop = 0;\n        }\n\n        function finishResponseTranscript(agentId) {\n            // Complete current response transcript message for this agent\n            if (currentResponseTranscripts[agentId]) {\n                delete currentResponseTranscripts[agentId];\n            }\n        }\n\n        async function checkModelAvailability() {\n            try {\n                const response = await fetch('/model_availability');\n                const availability = await response.json();\n                console.log(\"Model availability:\", availability);\n\n                const modelOptions = document.querySelectorAll('.model-option');\n                let hasAvailableModel = false;\n\n                modelOptions.forEach(option => {\n                    const provider = option.getAttribute('data-provider');\n                    const radio = option.querySelector('input[type=\"radio\"]');\n                    const unavailableReason = option.querySelector('.model-unavailable-reason');\n\n                    if (!availability[provider]) {\n                        // Mark as disabled\n                        option.classList.add('disabled');\n                        radio.disabled = true;\n\n                        // Show unavailable reason\n                        const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);\n                        unavailableReason.textContent = `(${providerName.toUpperCase()}_API_KEY not set)`;\n                        unavailableReason.style.display = 'inline';\n\n                        // If this was the selected option, uncheck it\n                        if (radio.checked) {\n                            radio.checked = false;\n                        }\n                    } else {\n                        hasAvailableModel = true;\n                        unavailableReason.style.display = 'none';\n                    }\n                });\n\n                // If no model is selected after checking availability, select first available\n                const selectedRadio = document.querySelector('input[name=\"modelProvider\"]:checked');\n                if (!selectedRadio && hasAvailableModel) {\n                    for (const option of modelOptions) {\n                        const provider = option.getAttribute('data-provider');\n                        if (availability[provider]) {\n                            option.querySelector('input[type=\"radio\"]').checked = true;\n                            option.classList.add('selected');\n                            break;\n                        }\n                    }\n                }\n\n                // Disable start button if no model is available\n                const startBtn = document.getElementById('startBtn');\n                if (!hasAvailableModel) {\n                    startBtn.disabled = true;\n                    showError('⚠️ No model API keys configured. Please set at least one API key to start conversation.');\n                } else {\n                    startBtn.disabled = false;\n                }\n\n                // Add click handlers for model options\n                modelOptions.forEach(option => {\n                    option.addEventListener('click', function() {\n                        if (!this.classList.contains('disabled')) {\n                            modelOptions.forEach(opt => opt.classList.remove('selected'));\n                            this.classList.add('selected');\n                        }\n                    });\n                });\n\n                // Mark initially selected option\n                const currentSelected = document.querySelector('input[name=\"modelProvider\"]:checked');\n                if (currentSelected) {\n                    currentSelected.closest('.model-option').classList.add('selected');\n                }\n\n            } catch (error) {\n                console.error(\"Failed to check model availability:\", error);\n                showError(\"⚠️ Failed to check model availability. Please refresh the page.\");\n            }\n        }\n\n        // Auto-connect when page loads\n        window.onload = function() {\n            connect();\n            setupInputMonitoring();\n            // Set default preset to AI debate\n            selectPreset('ai');\n            // Check model availability\n            checkModelAvailability();\n        };\n    </script>\n</body>\n</html>\n\n"
  },
  {
    "path": "examples/workflows/multiagent_realtime/run_server.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"A multi-agent realtime voice interaction server using ChatRoom.\"\"\"\nimport asyncio\nimport os\nimport traceback\nfrom pathlib import Path\n\nimport uvicorn\nfrom fastapi import FastAPI, WebSocket\nfrom fastapi.responses import FileResponse\n\nfrom agentscope import logger\nfrom agentscope.agent import RealtimeAgent\nfrom agentscope.message import TextBlock\nfrom agentscope.pipeline import ChatRoom\nfrom agentscope.realtime import (\n    ClientEvents,\n    ServerEvents,\n    ClientEventType,\n    DashScopeRealtimeModel,\n    GeminiRealtimeModel,\n    OpenAIRealtimeModel,\n)\n\napp = FastAPI()\n\n\n@app.get(\"/\")\nasync def get() -> FileResponse:\n    \"\"\"Serve the HTML test page.\"\"\"\n    html_path = Path(__file__).parent / \"multi_agent.html\"\n    return FileResponse(html_path)\n\n\n@app.get(\"/model_availability\")\nasync def model_availability() -> dict:\n    \"\"\"Check which model API keys are available in environment variables.\"\"\"\n    return {\n        \"dashscope\": bool(os.getenv(\"DASHSCOPE_API_KEY\")),\n        \"gemini\": bool(os.getenv(\"GEMINI_API_KEY\")),\n        \"openai\": bool(os.getenv(\"OPENAI_API_KEY\")),\n    }\n\n\nasync def frontend_receive(\n    websocket: WebSocket,\n    frontend_queue: asyncio.Queue,\n) -> None:\n    \"\"\"Forward the message received from the agents to the frontend.\"\"\"\n    try:\n        while True:\n            msg: ServerEvents.EventBase = await frontend_queue.get()\n\n            # Send the message as JSON\n            await websocket.send_json(msg.model_dump())\n\n    except Exception as e:\n        print(f\"[ERROR] frontend_receive error: {e}\")\n        traceback.print_exc()\n\n\n@app.websocket(\"/ws/{user_id}/{session_id}\")\nasync def multi_agent_endpoint(\n    websocket: WebSocket,\n    user_id: str,\n    session_id: str,\n) -> None:\n    \"\"\"WebSocket endpoint for multi-agent realtime voice interaction.\"\"\"\n    try:\n        await websocket.accept()\n\n        logger.info(\n            \"Connected to WebSocket: user_id=%s, session_id=%s\",\n            user_id,\n            session_id,\n        )\n\n        # Create the queue to forward messages to the frontend\n        frontend_queue = asyncio.Queue()\n        asyncio.create_task(\n            frontend_receive(websocket, frontend_queue),\n        )\n\n        # Chat room and agents\n        chat_room = None\n\n        while True:\n            # Handle the incoming messages from the frontend\n            # i.e. ClientEvents\n            data = await websocket.receive_json()\n\n            client_event = ClientEvents.from_json(data)\n\n            if isinstance(\n                client_event,\n                ClientEvents.ClientSessionCreateEvent,\n            ):\n                # Create agents by the given session arguments\n                agent1_name = client_event.config.get(\"agent1_name\", \"Agent1\")\n                agent1_instructions = client_event.config.get(\n                    \"agent1_instructions\",\n                    \"You are a helpful assistant.\",\n                )\n\n                agent2_name = client_event.config.get(\"agent2_name\", \"Agent2\")\n                agent2_instructions = client_event.config.get(\n                    \"agent2_instructions\",\n                    \"You are a helpful assistant.\",\n                )\n\n                model_provider = client_event.config.get(\n                    \"model_provider\",\n                    \"dashscope\",\n                )\n\n                # Create the appropriate model based on provider\n                if model_provider == \"dashscope\":\n                    model1 = DashScopeRealtimeModel(\n                        model_name=\"qwen3-omni-flash-realtime\",\n                        api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n                        voice=\"Dylan\",\n                        enable_input_audio_transcription=False,\n                    )\n                    model2 = DashScopeRealtimeModel(\n                        model_name=\"qwen3-omni-flash-realtime\",\n                        api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n                        voice=\"Peter\",\n                        enable_input_audio_transcription=False,\n                    )\n\n                elif model_provider == \"gemini\":\n                    model1 = GeminiRealtimeModel(\n                        model_name=(\n                            \"gemini-2.5-flash-native-audio-preview-09-2025\"\n                        ),\n                        api_key=os.getenv(\"GEMINI_API_KEY\"),\n                        voice=\"Puck\",\n                    )\n                    model2 = GeminiRealtimeModel(\n                        model_name=(\n                            \"gemini-2.5-flash-native-audio-preview-09-2025\"\n                        ),\n                        api_key=os.getenv(\"GEMINI_API_KEY\"),\n                        voice=\"Charon\",\n                    )\n\n                elif model_provider == \"openai\":\n                    model1 = OpenAIRealtimeModel(\n                        model_name=\"gpt-4o-realtime-preview\",\n                        api_key=os.getenv(\"OPENAI_API_KEY\"),\n                        voice=\"alloy\",\n                    )\n                    model2 = OpenAIRealtimeModel(\n                        model_name=\"gpt-4o-realtime-preview\",\n                        api_key=os.getenv(\"OPENAI_API_KEY\"),\n                        voice=\"echo\",\n                    )\n                else:\n                    raise ValueError(\n                        f\"Unsupported model provider: {model_provider}\",\n                    )\n\n                # Create the first agent\n                agent1 = RealtimeAgent(\n                    name=agent1_name,\n                    sys_prompt=agent1_instructions,\n                    model=model1,\n                )\n\n                # Create the second agent\n                agent2 = RealtimeAgent(\n                    name=agent2_name,\n                    sys_prompt=agent2_instructions,\n                    model=model2,\n                )\n\n                # Create chat room with both agents\n                chat_room = ChatRoom(agents=[agent1, agent2])\n\n                await chat_room.start(frontend_queue)\n\n                # Send session_created event to frontend\n                await websocket.send_json(\n                    ServerEvents.ServerSessionCreatedEvent(\n                        session_id=session_id,\n                    ).model_dump(),\n                )\n\n                await agent1.model.send(\n                    TextBlock(\n                        type=\"text\",\n                        text=\"<system>Now you can talk.</system>\",\n                    ),\n                )\n\n            elif client_event.type == ClientEventType.CLIENT_SESSION_END:\n                # End the session with the chat room\n                if chat_room:\n                    await chat_room.stop()\n                    chat_room = None\n\n            else:\n                # Forward other events to the chat room\n                if chat_room:\n                    await chat_room.handle_input(client_event)\n\n    except Exception as e:\n        print(f\"[ERROR] WebSocket endpoint error: {e}\")\n        traceback.print_exc()\n        raise\n\n\nif __name__ == \"__main__\":\n    uvicorn.run(\n        \"run_server:app\",\n        host=\"localhost\",\n        port=8000,\n        reload=True,\n        log_level=\"info\",\n    )\n"
  },
  {
    "path": "pyproject.toml",
    "content": "\n[project]\nname = \"agentscope\"\ndynamic = [\"version\"]\ndescription = \"AgentScope: A Flexible yet Robust Multi-Agent Platform.\"\nreadme = \"README.md\"\nauthors = [\n    { name = \"SysML team of Alibaba Tongyi Lab\", email = \"gaodawei.gdw@alibaba-inc.com\" }\n]\nlicense = \"Apache-2.0\"\nkeywords = [\"deep-learning\", \"multi agents\", \"agents\"]\nclassifiers = [\n    \"Development Status :: 4 - Beta\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Operating System :: OS Independent\",\n    \"Intended Audience :: Developers\",\n    \"Intended Audience :: Science/Research\",\n    \"Topic :: Scientific/Engineering :: Artificial Intelligence\",\n]\nrequires-python = \">=3.10\"\ndependencies = [\n    \"aioitertools\",\n    \"anthropic\",\n    \"dashscope\",\n    \"docstring_parser\",\n    \"json5\",\n    \"json_repair\",\n    \"mcp>=1.13\",\n    \"numpy\",\n    \"openai\",\n    \"python-datauri\",\n    \"opentelemetry-api>=1.39.0\",\n    \"opentelemetry-sdk>=1.39.0\",\n    \"opentelemetry-exporter-otlp>=1.39.0\",\n    \"opentelemetry-semantic-conventions>=0.60b0\",\n    \"python-socketio\",\n    \"shortuuid\",\n    \"tiktoken\",\n    \"sounddevice\",\n    \"sqlalchemy\",\n    \"python-frontmatter\",\n    \"aiofiles\",\n]\n\n[project.optional-dependencies]\n# ------------ A2A protocol ------------\na2a = [\n    \"a2a-sdk\",\n    \"httpx\",\n    # TODO: split the card resolvers from the a2a dependency\n    \"nacos-sdk-python>=3.0.0\",\n]\n\n# ------------ Realtime -------------\nrealtime = [\"websockets>=14.0\", \"scipy\"]\n\n# ------------ Model APIs ------------\ngemini = [\"google-genai\"]\nollama = [\"ollama>=0.5.4\"]\nmodels = [\n    \"agentscope[ollama]\",\n    \"agentscope[gemini]\",\n]\n\n# ------------ Tokenizers ------------\ntokens = [\n    \"Pillow\",\n    \"transformers\",\n    \"jinja2\",\n]\n\n# ------------ Memory ------------\nredis_memory = [\"redis\"]\n\nmem0ai = [\n    \"mem0ai<=1.0.3\",\n    \"packaging\"\n]\nreme = [\"reme-ai>=0.2.0.3\"]\nmemory = [\n    \"agentscope[redis_memory]\",\n    \"agentscope[mem0ai]\",\n    \"agentscope[reme]\",\n]\n\n# ------------ RAG ------------\n# readers\ntext-reader = [\"nltk\"]\npdf-reader = [\n    \"agentscope[text-reader]\",\n    # TODO: the latest pypdf has some issues with parsing PDFs\n    #  (2026-01-13), so we fix the version here temporarily.\n    \"pypdf<=6.5.0\",\n]\ndocx-reader = [\n    \"agentscope[text-reader]\",\n    \"python-docx\"\n]\nexcel-reader = [\n    \"agentscope[text-reader]\",\n    \"pandas\",\n    \"openpyxl\",\n]\nppt-reader = [\n    \"agentscope[text-reader]\",\n    \"python-pptx\"\n]\nreaders = [\n    \"agentscope[text-reader]\",\n    \"agentscope[pdf-reader]\",\n    \"agentscope[docx-reader]\",\n    \"agentscope[excel-reader]\",\n    \"agentscope[ppt-reader]\",\n]\n\n# vdb\n# The qdrant-client >= 1.16.0 has conflicts with pymilvus, so we fix\n# the version to 1.15.1 here.\nqdrant = [\"qdrant-client==1.15.1\"]\nmilvus = [\"pymilvus[milvus_lite]\"]\nali_mysql = [\"mysql-connector-python\"]\nmongodb = [\"pymongo\"]\noceanbase = [\"pyobvector>=0.2.0,<0.3.0\"]\nvdbs = [\n    \"agentscope[ali_mysql]\",\n    \"agentscope[qdrant]\",\n    \"agentscope[milvus]\",\n    \"agentscope[mongodb]\",\n    \"agentscope[oceanbase]\",\n]\n\nrag = [\n    \"agentscope[readers]\",\n    \"agentscope[vdbs]\",\n]\n\n# ------------ Evaluation ------------\nevaluate = [\"ray\"]\n\n# ------------ Full ------------\nfull = [\n    \"agentscope[a2a]\",\n    \"agentscope[models]\",\n    \"agentscope[tokens]\",\n    \"agentscope[memory]\",\n    \"agentscope[rag]\",\n    \"agentscope[evaluate]\",\n    \"agentscope[realtime]\",\n]\n\n# ------------ Development ------------\ndev = [\n    # Include full dependencies from local package\n    \"agentscope[full]\",\n    # Development tools\n    \"pre-commit\",\n    \"pytest\",\n    \"pytest-forked\",\n    \"sphinx-gallery\",\n    \"furo\",\n    \"myst_parser\",\n    \"matplotlib\",\n    # For unittests\n    # For mocking redis in unittests\n    \"fakeredis\",\n    \"aiosqlite\",\n    \"greenlet\",\n    # For openjudge\n    \"py-openjudge\",\n]\n\n[project.urls]\nHomepage = \"https://github.com/agentscope-ai/agentscope\"\nDocumentation = \"https://doc.agentscope.io/\"\nRepository = \"https://github.com/agentscope-ai/agentscope\"\n\n[tool.setuptools]\npackages = { find = { where = [\"src\"] } }\ninclude-package-data = true\n\n[tool.setuptools.package-data]\n\"*\" = [\"py.typed\"]\n\n[build-system]\nrequires = [\"setuptools>=45\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[tool.setuptools.dynamic]\nversion = {attr = \"agentscope._version.__version__\"}\n"
  },
  {
    "path": "src/agentscope/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E402\n# pylint: disable=wrong-import-position\n\"\"\"The agentscope serialization module\"\"\"\nimport os\nimport warnings\nfrom contextvars import ContextVar\nfrom datetime import datetime\n\nimport requests\nimport shortuuid\n\nfrom ._run_config import _ConfigCls\n\n\ndef _generate_random_suffix(length: int) -> str:\n    \"\"\"Generate a random suffix.\"\"\"\n    return shortuuid.uuid()[:length]\n\n\n# A thread and async safe global configuration instance\n_config = _ConfigCls(\n    run_id=ContextVar(\"run_id\", default=shortuuid.uuid()),\n    project=ContextVar(\n        \"project\",\n        default=\"UnnamedProject_At\" + datetime.now().strftime(\"%Y%m%d\"),\n    ),\n    name=ContextVar(\n        \"name\",\n        default=datetime.now().strftime(\"%H%M%S_\")\n        + _generate_random_suffix(4),\n    ),\n    created_at=ContextVar(\n        \"created_at\",\n        default=datetime.now().strftime(\"%Y-%m-%d %H:%M:%S.%f\")[:-3],\n    ),\n    trace_enabled=ContextVar(\n        \"trace_enabled\",\n        default=False,\n    ),\n)\n\nfrom . import exception\nfrom . import module\nfrom . import message\nfrom . import model\nfrom . import tool\nfrom . import formatter\nfrom . import memory\nfrom . import agent\nfrom . import session\nfrom . import embedding\nfrom . import token\nfrom . import evaluate\nfrom . import pipeline\nfrom . import tracing\nfrom . import rag\nfrom . import a2a\nfrom . import realtime\n\nfrom ._logging import (\n    logger,\n    setup_logger,\n)\nfrom .hooks import _equip_as_studio_hooks\nfrom ._version import __version__\n\n# Raise each warning only once\nwarnings.filterwarnings(\"once\", category=DeprecationWarning)\n\n\ndef init(\n    project: str | None = None,\n    name: str | None = None,\n    run_id: str | None = None,\n    logging_path: str | None = None,\n    logging_level: str = \"INFO\",\n    studio_url: str | None = None,\n    tracing_url: str | None = None,\n) -> None:\n    \"\"\"Initialize the agentscope library.\n\n    Args:\n        project (`str | None`, optional):\n            The project name.\n        name (`str | None`, optional):\n            The name of the run.\n        run_id (`str | None`, optional):\n            The identity of a running instance, which can be an agent, or a\n            multi-agent system. The `run_id` is used in AgentScope-Studio to\n            distinguish different runs.\n        logging_path (`str | None`, optional):\n            The path to saving the log file. If not provided, logs will not be\n            saved.\n        logging_level (`str | None`, optional):\n            The logging level. Defaults to \"INFO\".\n        studio_url (`str | None`, optional):\n            The URL of the AgentScope Studio to connect to.\n        tracing_url (`str | None`, optional):\n            The URL of the tracing endpoint, which can connect to third-party\n            OpenTelemetry tracing platforms like Arize-Phoenix and Langfuse.\n            If not provided and `studio_url` is provided, it will send traces\n            to the AgentScope Studio's tracing endpoint.\n    \"\"\"\n\n    if project:\n        _config.project = project\n\n    if name:\n        _config.name = name\n\n    if run_id:\n        _config.run_id = run_id\n\n    setup_logger(logging_level, logging_path)\n\n    if studio_url:\n        # Register the run\n        data = {\n            \"id\": _config.run_id,\n            \"project\": _config.project,\n            \"name\": _config.name,\n            \"timestamp\": _config.created_at,\n            \"pid\": os.getpid(),\n            \"status\": \"running\",\n            # Deprecated fields\n            \"run_dir\": \"\",\n        }\n        response = requests.post(\n            url=f\"{studio_url}/trpc/registerRun\",\n            json=data,\n        )\n        response.raise_for_status()\n\n        from .agent import UserAgent, StudioUserInput\n\n        UserAgent.override_class_input_method(\n            StudioUserInput(\n                studio_url=studio_url,\n                run_id=_config.run_id,\n                max_retries=3,\n            ),\n        )\n\n        _equip_as_studio_hooks(studio_url)\n\n    if tracing_url:\n        endpoint = tracing_url\n    else:\n        endpoint = studio_url.strip(\"/\") + \"/v1/traces\" if studio_url else None\n\n    if endpoint:\n        from .tracing import setup_tracing\n\n        setup_tracing(endpoint=endpoint)\n        _config.trace_enabled = True\n\n\n__all__ = [\n    # modules\n    \"exception\",\n    \"module\",\n    \"message\",\n    \"model\",\n    \"tool\",\n    \"formatter\",\n    \"memory\",\n    \"agent\",\n    \"session\",\n    \"logger\",\n    \"embedding\",\n    \"token\",\n    \"evaluate\",\n    \"pipeline\",\n    \"tracing\",\n    \"rag\",\n    \"a2a\",\n    # functions\n    \"init\",\n    \"setup_logger\",\n    \"__version__\",\n]\n"
  },
  {
    "path": "src/agentscope/_logging.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The logger for agentscope.\"\"\"\n\nimport logging\n\n\n_DEFAULT_FORMAT = (\n    \"%(asctime)s | %(levelname)-7s | \"\n    \"%(module)s:%(funcName)s:%(lineno)s - %(message)s\"\n)\n\nlogger = logging.getLogger(\"as\")\n\n\ndef setup_logger(\n    level: str,\n    filepath: str | None = None,\n) -> None:\n    \"\"\"Set up the agentscope logger.\n\n    Args:\n        level (`str`):\n            The logging level, chosen from \"INFO\", \"DEBUG\", \"WARNING\",\n            \"ERROR\", \"CRITICAL\".\n        filepath (`str | None`, optional):\n            The filepath to save the logging output.\n    \"\"\"\n    if level not in [\"INFO\", \"DEBUG\", \"WARNING\", \"ERROR\", \"CRITICAL\"]:\n        raise ValueError(\n            f\"Invalid logging level: {level}. Must be one of \"\n            f\"'INFO', 'DEBUG', 'WARNING', 'ERROR', 'CRITICAL'.\",\n        )\n    logger.handlers.clear()\n    logger.setLevel(level)\n    handler = logging.StreamHandler()\n    handler.setFormatter(logging.Formatter(_DEFAULT_FORMAT))\n    logger.addHandler(handler)\n\n    if filepath:\n        handler = logging.FileHandler(filepath)\n        handler.setFormatter(logging.Formatter(_DEFAULT_FORMAT))\n        logger.addHandler(handler)\n\n    logger.propagate = False\n\n\nsetup_logger(\"INFO\")\n"
  },
  {
    "path": "src/agentscope/_run_config.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The run instance configuration in agentscope.\"\"\"\nfrom contextvars import ContextVar\n\n\nclass _ConfigCls:\n    \"\"\"The run instance configuration in agentscope.\"\"\"\n\n    def __init__(\n        self,\n        run_id: ContextVar[str],\n        project: ContextVar[str],\n        name: ContextVar[str],\n        created_at: ContextVar[str],\n        trace_enabled: ContextVar[bool],\n    ) -> None:\n        \"\"\"The constructor for _Config class.\"\"\"\n        # Copy the default context variables\n        self._run_id = run_id\n        self._created_at = created_at\n        self._project = project\n        self._name = name\n        self._trace_enabled = trace_enabled\n\n    @property\n    def run_id(self) -> str:\n        \"\"\"Get the run ID.\"\"\"\n        return self._run_id.get()\n\n    @run_id.setter\n    def run_id(self, value: str) -> None:\n        \"\"\"Set the run ID.\"\"\"\n        self._run_id.set(value)\n\n    @property\n    def created_at(self) -> str:\n        \"\"\"Get the creation time.\"\"\"\n        return self._created_at.get()\n\n    @created_at.setter\n    def created_at(self, value: str) -> None:\n        \"\"\"Set the creation time.\"\"\"\n        self._created_at.set(value)\n\n    @property\n    def project(self) -> str:\n        \"\"\"Get the project name.\"\"\"\n        return self._project.get()\n\n    @project.setter\n    def project(self, value: str) -> None:\n        \"\"\"Set the project name.\"\"\"\n        self._project.set(value)\n\n    @property\n    def name(self) -> str:\n        \"\"\"Get the run name.\"\"\"\n        return self._name.get()\n\n    @name.setter\n    def name(self, value: str) -> None:\n        \"\"\"Set the run name.\"\"\"\n        self._name.set(value)\n\n    @property\n    def trace_enabled(self) -> bool:\n        \"\"\"Get whether tracing is enabled.\"\"\"\n        return self._trace_enabled.get()\n\n    @trace_enabled.setter\n    def trace_enabled(self, value: bool) -> None:\n        \"\"\"Set whether tracing is enabled.\"\"\"\n        self._trace_enabled.set(value)\n"
  },
  {
    "path": "src/agentscope/_utils/__init__.py",
    "content": ""
  },
  {
    "path": "src/agentscope/_utils/_common.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The common utilities for agentscope library.\"\"\"\nimport asyncio\nimport base64\nimport functools\nimport inspect\nimport json\nimport os\nimport tempfile\nimport types\nimport typing\nimport uuid\nfrom datetime import datetime\nfrom typing import Any, Callable, Type, Dict\n\nimport numpy as np\nimport requests\nfrom docstring_parser import parse\nfrom json_repair import repair_json\nfrom pydantic import BaseModel, Field, create_model, ConfigDict\n\nfrom .._logging import logger\nfrom ..types import ToolFunction\n\nif typing.TYPE_CHECKING:\n    from mcp.types import Tool\nelse:\n    Tool = \"mcp.types.Tool\"\n\n\ndef _json_loads_with_repair(\n    json_str: str,\n) -> dict:\n    \"\"\"The given json_str maybe incomplete, e.g. '{\"key', so we need to\n    repair and load it into a Python object.\n\n    .. note::\n        This function is currently only used for parsing the streaming output\n        of the argument field in `tool_use`, so the parsed result must be a\n        dict.\n\n    Args:\n        json_str (`str`):\n            The JSON string to parse, which may be incomplete or malformed.\n\n    Returns:\n        `dict`:\n            A dictionary parsed from the JSON string after repair attempts.\n            Returns an empty dict if all repair attempts fail.\n    \"\"\"\n    try:\n        repaired = repair_json(json_str, stream_stable=True)\n        result = json.loads(repaired)\n        if isinstance(result, dict):\n            return result\n\n    except Exception:\n        if len(json_str) > 100:\n            log_str = json_str[:100] + \"...\"\n        else:\n            log_str = json_str\n\n        logger.warning(\n            \"Failed to load JSON dict from string: %s. Returning empty dict \"\n            \"instead.\",\n            log_str,\n        )\n\n    return {}\n\n\ndef _is_accessible_local_file(url: str) -> bool:\n    \"\"\"Check if the given URL is a local URL.\"\"\"\n    # First identify if it's an uri with 'file://' schema,\n    if url.startswith(\"file://\"):\n        local_path = url.removeprefix(\"file://\")\n        return os.path.isfile(local_path)\n    return os.path.isfile(url)\n\n\ndef _get_timestamp(add_random_suffix: bool = False) -> str:\n    \"\"\"Get the current timestamp in the format YYYY-MM-DD HH:MM:SS.sss.\"\"\"\n    timestamp = datetime.now().strftime(\"%Y-%m-%d %H:%M:%S.%f\")[:-3]\n\n    if add_random_suffix:\n        # Add a random suffix to the timestamp\n        timestamp += f\"_{os.urandom(3).hex()}\"\n\n    return timestamp\n\n\nasync def _is_async_func(func: Callable) -> bool:\n    \"\"\"Check if the given function is an async function, including\n    coroutine functions, async generators, and coroutine objects.\n    \"\"\"\n\n    return (\n        inspect.iscoroutinefunction(func)\n        or inspect.isasyncgenfunction(func)\n        or isinstance(func, types.CoroutineType)\n        or isinstance(func, types.GeneratorType)\n        and asyncio.iscoroutine(func)\n        or isinstance(func, functools.partial)\n        and await _is_async_func(func.func)\n    )\n\n\nasync def _execute_async_or_sync_func(\n    func: Callable,\n    *args: Any,\n    **kwargs: Any,\n) -> Any:\n    \"\"\"Execute an async or sync function based on its type.\n\n    Args:\n        func (`Callable`):\n            The function to be executed, which can be either async or sync.\n        *args (`Any`):\n            Positional arguments to be passed to the function.\n        **kwargs (`Any`):\n            Keyword arguments to be passed to the function.\n\n    Returns:\n        `Any`:\n            The result of the function execution.\n    \"\"\"\n\n    if await _is_async_func(func):\n        return await func(*args, **kwargs)\n\n    return func(*args, **kwargs)\n\n\ndef _get_bytes_from_web_url(\n    url: str,\n    max_retries: int = 3,\n) -> str:\n    \"\"\"Get the bytes from a given URL.\n\n    Args:\n        url (`str`):\n            The URL to fetch the bytes from.\n        max_retries (`int`, defaults to `3`):\n            The maximum number of retries.\n    \"\"\"\n    for _ in range(max_retries):\n        try:\n            response = requests.get(url)\n            response.raise_for_status()\n            return response.content.decode(\"utf-8\")\n\n        except UnicodeDecodeError:\n            return base64.b64encode(response.content).decode(\"ascii\")\n\n        except Exception as e:\n            logger.info(\n                \"Failed to fetch bytes from URL %s. Error %s. Retrying...\",\n                url,\n                str(e),\n            )\n\n    raise RuntimeError(\n        f\"Failed to fetch bytes from URL `{url}` after {max_retries} retries.\",\n    )\n\n\ndef _save_base64_data(\n    media_type: str,\n    base64_data: str,\n) -> str:\n    \"\"\"Save the base64 data to a temp file and return the file path. The\n    extension is guessed from the MIME type.\n\n    Args:\n        media_type (`str`):\n            The MIME type of the data, e.g. \"image/png\", \"audio/mpeg\".\n        base64_data (`str`):\n            The base64 data to be saved.\n    \"\"\"\n    extension = \".\" + media_type.split(\"/\")[-1]\n\n    with tempfile.NamedTemporaryFile(\n        suffix=extension,\n        delete=False,\n    ) as temp_file:\n        decoded_data = base64.b64decode(base64_data)\n        temp_file.write(decoded_data)\n        return temp_file.name\n\n\ndef _extract_json_schema_from_mcp_tool(tool: Tool) -> dict[str, Any]:\n    \"\"\"Extract JSON schema from MCP tool.\"\"\"\n\n    return {\n        \"type\": \"function\",\n        \"function\": {\n            \"name\": tool.name,\n            \"description\": tool.description,\n            \"parameters\": {\n                \"type\": \"object\",\n                \"properties\": tool.inputSchema.get(\n                    \"properties\",\n                    {},\n                ),\n                \"required\": tool.inputSchema.get(\n                    \"required\",\n                    [],\n                ),\n            },\n        },\n    }\n\n\ndef _remove_title_field(schema: dict) -> None:\n    \"\"\"Remove the title field from the JSON schema to avoid\n    misleading the LLM.\"\"\"\n    # The top level title field\n    if \"title\" in schema:\n        schema.pop(\"title\")\n\n    # properties\n    if \"properties\" in schema:\n        for prop in schema[\"properties\"].values():\n            if isinstance(prop, dict):\n                _remove_title_field(prop)\n\n    # items\n    if \"items\" in schema and isinstance(schema[\"items\"], dict):\n        _remove_title_field(schema[\"items\"])\n\n    # additionalProperties\n    if \"additionalProperties\" in schema and isinstance(\n        schema[\"additionalProperties\"],\n        dict,\n    ):\n        _remove_title_field(\n            schema[\"additionalProperties\"],\n        )\n\n\ndef _create_tool_from_base_model(\n    structured_model: Type[BaseModel],\n    tool_name: str = \"generate_structured_output\",\n) -> Dict[str, Any]:\n    \"\"\"Create a function tool definition from a Pydantic BaseModel.\n    This function converts a Pydantic BaseModel class into a tool definition\n    that can be used with function calling API. The resulting tool\n    definition includes the model's JSON schema as parameters, enabling\n    structured output generation by forcing the model to call this function\n    with properly formatted data.\n\n    Args:\n        structured_model (`Type[BaseModel]`):\n            A Pydantic BaseModel class that defines the expected structure\n            for the tool's output.\n        tool_name (`str`, default `\"generate_structured_output\"`):\n            The tool name that used to force the LLM to generate structured\n            output by calling this function.\n\n    Returns:\n        `Dict[str, Any]`: A tool definition dictionary compatible with\n            function calling API, containing type (\"function\") and\n            function dictionary with name, description, and parameters\n            (JSON schema).\n\n    .. code-block:: python\n        :caption: Example usage\n\n        from pydantic import BaseModel\n\n        class PersonInfo(BaseModel):\n            name: str\n            age: int\n            email: str\n\n        tool = _create_tool_from_base_model(PersonInfo, \"extract_person\")\n        print(tool[\"function\"][\"name\"])  # extract_person\n        print(tool[\"type\"])              # function\n\n    .. note:: The function automatically removes the 'title' field from\n        the JSON schema to ensure compatibility with function calling\n        format. This is handled by the internal ``_remove_title_field()``\n        function.\n    \"\"\"\n    schema = structured_model.model_json_schema()\n\n    _remove_title_field(schema)\n    tool_definition = {\n        \"type\": \"function\",\n        \"function\": {\n            \"name\": tool_name,\n            \"description\": \"Generate the required structured output with \"\n            \"this function\",\n            \"parameters\": schema,\n        },\n    }\n    return tool_definition\n\n\ndef _map_text_to_uuid(text: str) -> str:\n    \"\"\"Map the given text to a deterministic UUID string.\n\n    Args:\n        text (`str`):\n            The input text to be mapped to a UUID.\n\n    Returns:\n        `str`:\n            A deterministic UUID string derived from the input text.\n    \"\"\"\n    return str(uuid.uuid3(uuid.NAMESPACE_DNS, text))\n\n\ndef _parse_tool_function(\n    tool_func: ToolFunction,\n    include_long_description: bool,\n    include_var_positional: bool,\n    include_var_keyword: bool,\n) -> dict:\n    \"\"\"Extract JSON schema from the tool function's docstring\n\n    Args:\n        tool_func (`ToolFunction`):\n            The tool function to extract the JSON schema from.\n        include_long_description (`bool`):\n            Whether to include the long description in the JSON schema.\n        include_var_positional (`bool`):\n            Whether to include variable positional arguments in the JSON\n            schema.\n        include_var_keyword (`bool`):\n            Whether to include variable keyword arguments in the JSON schema.\n\n    Returns:\n        `dict`:\n            The extracted JSON schema.\n    \"\"\"\n    docstring = parse(tool_func.__doc__)\n    params_docstring = {_.arg_name: _.description for _ in docstring.params}\n\n    # Function description\n    descriptions = []\n    if docstring.short_description is not None:\n        descriptions.append(docstring.short_description)\n\n    if include_long_description and docstring.long_description is not None:\n        descriptions.append(docstring.long_description)\n\n    func_description = \"\\n\".join(descriptions)\n\n    # Create a dynamic model with the function signature\n    fields = {}\n    for name, param in inspect.signature(tool_func).parameters.items():\n        # Skip the `self` and `cls` parameters\n        if name in [\"self\", \"cls\"]:\n            continue\n\n        # Handle `**kwargs`\n        if param.kind == inspect.Parameter.VAR_KEYWORD:\n            if not include_var_keyword:\n                continue\n\n            fields[name] = (\n                Dict[str, Any]\n                if param.annotation == inspect.Parameter.empty\n                else Dict[str, param.annotation],  # type: ignore\n                Field(\n                    description=params_docstring.get(\n                        f\"**{name}\",\n                        params_docstring.get(name, None),\n                    ),\n                    default={}\n                    if param.default is param.empty\n                    else param.default,\n                ),\n            )\n\n        elif param.kind == inspect.Parameter.VAR_POSITIONAL:\n            if not include_var_positional:\n                continue\n\n            fields[name] = (\n                list[Any]\n                if param.annotation == inspect.Parameter.empty\n                else list[param.annotation],  # type: ignore\n                Field(\n                    description=params_docstring.get(\n                        f\"*{name}\",\n                        params_docstring.get(name, None),\n                    ),\n                    default=[]\n                    if param.default is param.empty\n                    else param.default,\n                ),\n            )\n\n        else:\n            fields[name] = (\n                Any\n                if param.annotation == inspect.Parameter.empty\n                else param.annotation,\n                Field(\n                    description=params_docstring.get(name, None),\n                    default=...\n                    if param.default is param.empty\n                    else param.default,\n                ),\n            )\n\n    base_model = create_model(\n        \"_StructuredOutputDynamicClass\",\n        __config__=ConfigDict(arbitrary_types_allowed=True),\n        **fields,\n    )\n    params_json_schema = base_model.model_json_schema()\n\n    # Remove the title from the json schema\n    _remove_title_field(params_json_schema)\n\n    func_json_schema: dict = {\n        \"type\": \"function\",\n        \"function\": {\n            \"name\": tool_func.__name__,\n            \"parameters\": params_json_schema,\n        },\n    }\n\n    if func_description not in [None, \"\"]:\n        func_json_schema[\"function\"][\"description\"] = func_description\n\n    return func_json_schema\n\n\ndef _resample_pcm_delta(\n    pcm_base64: str,\n    sample_rate: int,\n    target_rate: int,\n) -> str:\n    \"\"\"Resampling the input pcm base64 data into the target rate.\n\n    Args:\n        pcm_base64 (`str`):\n            The input base64 audio data in pcm format.\n        sample_rate (`int`):\n            The sampling rate of the input data.\n        target_rate (`int`):\n            The target rate of the input data.\n\n    Returns:\n        `str`:\n            The resampling base64 audio data in the required sampling\n            rate.\n    \"\"\"\n    pcm_data = base64.b64decode(pcm_base64)\n\n    # Into numpy array first\n    audio_array = np.frombuffer(pcm_data, dtype=np.int16)\n\n    # return directly if the same\n    if sample_rate == target_rate:\n        return pcm_base64\n\n    # compute the number of samples\n    num_samples = int(len(audio_array) * target_rate / sample_rate)\n\n    from scipy import signal\n\n    # Use scipy to resample\n    resampled_audio = signal.resample(audio_array, num_samples)\n\n    # Turn it back into bytes\n    resampled_audio = np.clip(resampled_audio, -32768, 32767).astype(np.int16)\n\n    # into base64\n    resampled_bytes = resampled_audio.tobytes()\n    resampled_base64 = base64.b64encode(resampled_bytes).decode(\"utf-8\")\n\n    return resampled_base64\n"
  },
  {
    "path": "src/agentscope/_utils/_mixin.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The mixin for agentscope.\"\"\"\n\n\nclass DictMixin(dict):\n    \"\"\"The dictionary mixin that allows attribute-style access.\"\"\"\n\n    __setattr__ = dict.__setitem__\n    __getattr__ = dict.__getitem__\n"
  },
  {
    "path": "src/agentscope/_version.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The version of agentscope.\"\"\"\n\n__version__ = \"1.0.17\"\n"
  },
  {
    "path": "src/agentscope/a2a/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The A2A related modules.\"\"\"\nfrom ._base import AgentCardResolverBase\nfrom ._file_resolver import FileAgentCardResolver\nfrom ._well_known_resolver import WellKnownAgentCardResolver\nfrom ._nacos_resolver import NacosAgentCardResolver\n\n\n__all__ = [\n    \"AgentCardResolverBase\",\n    \"FileAgentCardResolver\",\n    \"WellKnownAgentCardResolver\",\n    \"NacosAgentCardResolver\",\n]\n"
  },
  {
    "path": "src/agentscope/a2a/_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The A2A agent card resolver base class.\"\"\"\nfrom abc import abstractmethod\nfrom typing import Any, TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from a2a.types import AgentCard\nelse:\n    AgentCard = \"a2a.types.AgentCard\"\n\n\nclass AgentCardResolverBase:\n    \"\"\"Base class for A2A agent card resolvers, responsible for fetching\n    agent cards from various sources. Implementations must provide the\n    `get_agent_card` method to retrieve the agent card.\n    \"\"\"\n\n    @abstractmethod\n    async def get_agent_card(self, *args: Any, **kwargs: Any) -> AgentCard:\n        \"\"\"Get Agent Card from the configured source.\n\n        Returns:\n            `AgentCard`:\n                The resolved agent card object.\n        \"\"\"\n"
  },
  {
    "path": "src/agentscope/a2a/_file_resolver.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The JSON file based A2A agent card resolver.\"\"\"\nimport json\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nfrom ._base import AgentCardResolverBase\n\nif TYPE_CHECKING:\n    from a2a.types import AgentCard\nelse:\n    AgentCard = \"a2a.types.AgentCard\"\n\n\nclass FileAgentCardResolver(AgentCardResolverBase):\n    \"\"\"Agent card resolver that loads AgentCard from a JSON file.\n\n    The JSON file should contain an AgentCard object with the following\n    required fields:\n\n    - name (str): The name of the agent\n    - url (str): The URL of the agent\n    - version (str): The version of the agent\n    - capabilities (dict): The capabilities of the agent\n    - default_input_modes (list[str]): Default input modes\n    - default_output_modes (list[str]): Default output modes\n    - skills (list): List of agent skills\n\n    Example JSON file content:\n\n        .. code-block:: json\n\n            {\n                \"name\": \"RemoteAgent\",\n                \"url\": \"http://localhost:8000\",\n                \"description\": \"A remote A2A agent\",\n                \"version\": \"1.0.0\",\n                \"capabilities\": {},\n                \"default_input_modes\": [\"text/plain\"],\n                \"default_output_modes\": [\"text/plain\"],\n                \"skills\": []\n            }\n\n    \"\"\"\n\n    def __init__(\n        self,\n        file_path: str,\n    ) -> None:\n        \"\"\"Initialize the FileAgentCardResolver with the path to the JSON file.\n\n        Args:\n            file_path (`str`):\n                The path to the JSON file containing the agent card.\n        \"\"\"\n        self._file_path = file_path\n\n    async def get_agent_card(self) -> AgentCard:\n        \"\"\"Get the agent card from the JSON file.\n\n        Returns:\n            `AgentCard`:\n                The agent card loaded from the file.\n        \"\"\"\n        from a2a.types import AgentCard\n\n        path = Path(self._file_path)\n        if not path.exists():\n            raise FileNotFoundError(\n                f\"Agent card file not found: {self._file_path}\",\n            )\n\n        if not path.is_file():\n            raise ValueError(f\"Path is not a file: {self._file_path}\")\n\n        with path.open(\"r\", encoding=\"utf-8\") as f:\n            agent_json_data = json.load(f)\n            return AgentCard.model_validate(agent_json_data)\n"
  },
  {
    "path": "src/agentscope/a2a/_nacos_resolver.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The Nacos-based A2A Agent Card resolver.\"\"\"\n\nfrom typing import TYPE_CHECKING\n\nfrom ._base import AgentCardResolverBase\nfrom .._logging import logger\n\nif TYPE_CHECKING:\n    from a2a.types import AgentCard\n    from v2.nacos.common.client_config import ClientConfig\nelse:\n    AgentCard = \"a2a.types.AgentCard\"\n    ClientConfig = \"v2.nacos.common.client_config.ClientConfig\"\n\n\nclass NacosAgentCardResolver(AgentCardResolverBase):\n    \"\"\"Nacos-based A2A Agent Card resolver.\n\n    Nacos is a dynamic service discovery, configuration and service\n    management platform for building cloud native applications. This resolver\n    fetches the agent card from a Nacos server and subscribes to updates.\n    \"\"\"\n\n    def __init__(\n        self,\n        remote_agent_name: str,\n        nacos_client_config: ClientConfig,\n        version: str | None = None,\n    ) -> None:\n        \"\"\"Initialize the nacos agent card resolver.\n\n        Args:\n            remote_agent_name (`str`):\n                Name of the remote agent in Nacos.\n            nacos_client_config (`ClientConfig | None`, optional):\n                Nacos client configuration, where a `server_addresses`\n                parameter is required.\n            version (`str | None`, optional):\n                Version of the agent card to fetch. If None, fetches the\n                latest version. This version is also used when subscribing\n                to agent card updates.\n                Defaults to None (latest version).\n        \"\"\"\n        if not remote_agent_name:\n            raise ValueError(\n                \"The remote_agent_name cannot be empty.\",\n            )\n\n        if not nacos_client_config:\n            raise ValueError(\n                \"The nacos_client_config cannot be None.\",\n            )\n\n        self._nacos_client_config = nacos_client_config\n        self._remote_agent_name = remote_agent_name\n        self._version = version\n\n    async def get_agent_card(self) -> AgentCard:\n        \"\"\"Get agent card from Nacos with lazy initialization.\n\n        Returns:\n            `AgentCard`:\n                The resolved agent card from Nacos.\n        \"\"\"\n        try:\n            from v2.nacos.ai.model.ai_param import GetAgentCardParam\n            from v2.nacos.ai.nacos_ai_service import NacosAIService\n        except ImportError as e:\n            raise ImportError(\n                \"Please install the nacos sdk by running `pip install \"\n                \"nacos-sdk-python>=3.0.0` first.\",\n            ) from e\n\n        client = None\n        try:\n            client = await NacosAIService.create_ai_service(\n                self._nacos_client_config,\n            )\n\n            await client.start()\n            return await client.get_agent_card(\n                GetAgentCardParam(\n                    agent_name=self._remote_agent_name,\n                    version=self._version,\n                ),\n            )\n\n        finally:\n            if client:\n                # Close the Nacos client to free resources\n                try:\n                    await client.shutdown()\n                except Exception as e:\n                    logger.warning(\n                        \"Failed to shutdown Nacos client: %s\",\n                        str(e),\n                    )\n"
  },
  {
    "path": "src/agentscope/a2a/_well_known_resolver.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The A2A well-known agent card resolver.\"\"\"\nfrom typing import TYPE_CHECKING\nfrom urllib.parse import urlparse\n\nfrom ._base import AgentCardResolverBase\nfrom .._logging import logger\n\nif TYPE_CHECKING:\n    from a2a.types import AgentCard\nelse:\n    AgentCard = \"a2a.types.AgentCard\"\n\n\nclass WellKnownAgentCardResolver(AgentCardResolverBase):\n    \"\"\"Agent card resolver that loads AgentCard from a well-known URL.\"\"\"\n\n    def __init__(\n        self,\n        base_url: str,\n        agent_card_path: str | None = None,\n    ) -> None:\n        \"\"\"Initialize the WellKnownAgentCardResolver.\n\n        Args:\n            base_url (`str`):\n                The base URL to resolve the agent card from.\n            agent_card_path (`str | None`, optional):\n                The path to the agent card relative to the base URL.\n                Defaults to AGENT_CARD_WELL_KNOWN_PATH from a2a.utils.\n        \"\"\"\n        self._base_url = base_url\n        self._agent_card_path = agent_card_path\n\n    async def get_agent_card(self) -> AgentCard:\n        \"\"\"Get the agent card from the well-known URL.\n\n        Returns:\n            `AgentCard`:\n                The agent card loaded from the URL.\n        \"\"\"\n        import httpx\n        from a2a.client import A2ACardResolver\n        from a2a.utils import AGENT_CARD_WELL_KNOWN_PATH\n\n        try:\n            parsed_url = urlparse(self._base_url)\n            if not parsed_url.scheme or not parsed_url.netloc:\n                logger.error(\n                    \"[%s] Invalid URL format: %s\",\n                    self.__class__.__name__,\n                    self._base_url,\n                )\n                raise ValueError(\n                    f\"Invalid URL format: {self._base_url}\",\n                )\n\n            base_url = f\"{parsed_url.scheme}://{parsed_url.netloc}\"\n            relative_card_path = parsed_url.path\n\n            # Use default path if not specified\n            agent_card_path = (\n                self._agent_card_path\n                if self._agent_card_path is not None\n                else AGENT_CARD_WELL_KNOWN_PATH\n            )\n\n            # Use async context manager to ensure proper cleanup\n            async with httpx.AsyncClient(\n                timeout=httpx.Timeout(timeout=600),\n            ) as _http_client:\n                resolver = A2ACardResolver(\n                    httpx_client=_http_client,\n                    base_url=base_url,\n                    agent_card_path=agent_card_path,\n                )\n                return await resolver.get_agent_card(\n                    relative_card_path=relative_card_path,\n                )\n        except Exception as e:\n            logger.error(\n                \"[%s] Failed to resolve agent card from URL %s: %s\",\n                self.__class__.__name__,\n                self._base_url,\n                e,\n            )\n            raise RuntimeError(\n                f\"Failed to resolve AgentCard from URL \"\n                f\"{self._base_url}: {e}\",\n            ) from e\n"
  },
  {
    "path": "src/agentscope/agent/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The agent base class.\"\"\"\nfrom ._agent_base import AgentBase\nfrom ._react_agent_base import ReActAgentBase\nfrom ._react_agent import ReActAgent\nfrom ._user_input import (\n    UserInputBase,\n    UserInputData,\n    TerminalUserInput,\n    StudioUserInput,\n)\nfrom ._user_agent import UserAgent\nfrom ._a2a_agent import A2AAgent\nfrom ._realtime_agent import RealtimeAgent\n\n\n__all__ = [\n    \"AgentBase\",\n    \"ReActAgentBase\",\n    \"ReActAgent\",\n    \"UserInputData\",\n    \"UserInputBase\",\n    \"TerminalUserInput\",\n    \"StudioUserInput\",\n    \"UserAgent\",\n    \"A2AAgent\",\n    \"RealtimeAgent\",\n]\n"
  },
  {
    "path": "src/agentscope/agent/_a2a_agent.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"A2A agent implementation for AgentScope.\n\nThis module provides the A2A (Agent-to-Agent) protocol implementation,\nenabling AgentScope agents to communicate with remote agents using the\nA2A standard protocol.\n\"\"\"\nfrom __future__ import annotations\nfrom typing import TYPE_CHECKING, Any, Type\n\nimport httpx\nfrom pydantic import BaseModel\n\nfrom ._agent_base import AgentBase\nfrom ..message import Msg\nfrom ..formatter import A2AChatFormatter\n\nif TYPE_CHECKING:\n    from a2a.types import AgentCard\n    from a2a.client import ClientConfig, Consumer\n    from a2a.client.client_factory import TransportProducer\nelse:\n    AgentCard = \"a2a.types.AgentCard\"\n    ClientConfig = \"a2a.client.ClientConfig\"\n    Consumer = \"a2a.client.Consumer\"\n    TransportProducer = \"a2a.client.client_factory.TransportProducer\"\n\n\nclass A2AAgent(AgentBase):\n    \"\"\"An A2A agent implementation in AgentScope, which supports\n\n    - Communication with remote agents using the A2A protocol\n    - Bidirectional message conversion between AgentScope and A2A formats\n    - Task lifecycle management with streaming and polling\n    - Artifact handling and status tracking\n\n    .. note:: Due to the limitation of A2A protocol. The A2AAgent class\n\n    - Only support chatbot-scenario (a user and an assistant) interactions.\n     To support multi-agent interactions requires the server side to handle\n     the `name` field in the A2A messages properly.\n    - Does not support structured output in `reply()` method due to the lack\n     of structured output support in A2A protocol.\n    - Stores observed messages locally and merges them with input messages\n     when `reply()` is called. Observed messages are cleared after processing.\n    \"\"\"\n\n    def __init__(\n        self,\n        agent_card: AgentCard,\n        client_config: ClientConfig | None = None,\n        consumers: list[Consumer] | None = None,\n        additional_transport_producers: dict[str, TransportProducer]\n        | None = None,\n    ) -> None:\n        \"\"\"Initialize the A2A agent instance by the given agent card.\n\n        Args:\n            agent_card (`AgentCard`):\n                The agent card that contains the information about the remote\n                agent, such as its URL and capabilities.\n            client_config (`ClientConfig | None`, optional):\n                The configuration for the A2A client, including transport\n                preferences and streaming options.\n            consumers (`list[Consumer] | None`, optional):\n                The list of consumers for handling A2A client events.\n                These intercept request/response flows for logging,\n                metrics, and security.\n            additional_transport_producers (`dict[str, TransportProducer] | \\\n            None`, optional):\n                Additional transport producers for creating A2A clients\n                with specific transport protocols.\n        \"\"\"\n        super().__init__()\n\n        from a2a.types import AgentCard\n        from a2a.client import ClientConfig, ClientFactory\n\n        if not isinstance(agent_card, AgentCard):\n            raise ValueError(\n                f\"agent_card must be an instance of AgentCard, \"\n                f\"got {type(agent_card)}\",\n            )\n\n        self.name: str = agent_card.name\n        self.agent_card = agent_card\n\n        # Create the client factory so that we can create clients later\n        # in reply()\n        self._a2a_client_factory = ClientFactory(\n            config=client_config\n            or ClientConfig(\n                httpx_client=httpx.AsyncClient(\n                    timeout=httpx.Timeout(timeout=600),\n                ),\n            ),\n            consumers=consumers,\n        )\n\n        # Register additional transport producers if provided\n        if additional_transport_producers:\n            for label, producer in additional_transport_producers.items():\n                self._a2a_client_factory.register(\n                    label,\n                    producer,\n                )\n\n        # The variables to store observed messages\n        self._observed_msgs: list[Msg] = []\n\n        # The formatter used for message conversion\n        self.formatter = A2AChatFormatter()\n\n    def state_dict(self) -> dict:\n        \"\"\"Get the state dictionary of the A2A agent.\n\n        Returns:\n            `dict`:\n                The state dictionary containing the observed messages.\n        \"\"\"\n        return {\n            \"_observed_msgs\": [msg.to_dict() for msg in self._observed_msgs],\n        }\n\n    def load_state_dict(self, state_dict: dict, strict: bool = True) -> None:\n        \"\"\"Load the state dictionary into the module.\n\n        Args:\n            state_dict (`dict`):\n                The state dictionary to load.\n            strict (`bool`, defaults to `True`):\n                If `True`, raises an error if any key in the module is not\n                found in the state_dict. If `False`, skips missing keys.\n\n        Raises:\n            `KeyError`:\n                If a required key is missing in the state_dict when strict\n                is `True`.\n        \"\"\"\n        if \"_observed_msgs\" in state_dict:\n            self._observed_msgs = [\n                Msg.from_dict(d) for d in state_dict[\"_observed_msgs\"]\n            ]\n        else:\n            raise KeyError(\n                \"_observed_msgs key not found in state_dict\",\n            )\n\n        if strict:\n            for key in state_dict.keys():\n                if key != \"_observed_msgs\":\n                    raise KeyError(f\"Unexpected key {key} in state_dict\")\n\n    async def observe(self, msg: Msg | list[Msg] | None) -> None:\n        \"\"\"Receive the given message(s) without generating a reply.\n\n        The observed messages are stored and will be merged with the\n        input messages when `reply` is called. After `reply` completes,\n        the stored messages will be cleared.\n\n        Args:\n            msg (`Msg | list[Msg] | None`):\n                The message(s) to be observed. If None, no action is taken.\n        \"\"\"\n        if msg is None:\n            return\n\n        if isinstance(msg, Msg):\n            self._observed_msgs.append(msg)\n        elif isinstance(msg, list) and all(isinstance(m, Msg) for m in msg):\n            self._observed_msgs.extend(msg)\n        else:\n            raise TypeError(\n                f\"msg must be a Msg or a list of Msg, got {type(msg)}\",\n            )\n\n    async def reply(\n        self,\n        msg: Msg | list[Msg] | None = None,\n        **kwargs: Any,\n    ) -> Msg:\n        \"\"\"Send message(s) to the remote A2A agent and receive a response.\n\n        .. note:: This method merges any previously observed messages with the\n        input messages, sends them to the remote agent, and clears the\n        observed messages after processing.\n\n        .. note:: The A2A protocol does not support structured output, so the\n         `structured_model` parameter is not supported in this method.\n\n        Args:\n            msg (`Msg | list[Msg] | None`, optional):\n                The message(s) to send to the remote agent. Can be a single\n                Msg, a list of Msgs, or None. If None, only observed messages\n                will be sent. Defaults to None.\n\n        Returns:\n            `Msg`:\n                The response message from the remote agent. For tasks, this\n                may be either a status update message or the final artifacts\n                message, depending on the task state. If no messages are\n                provided (both msg and observed messages are empty), returns\n                a prompt message. If an error occurs during communication,\n                returns an error message.\n        \"\"\"\n        if \"structured_model\" in kwargs:\n            raise ValueError(\n                \"structured_model is not supported in A2AAgent.reply() \"\n                \"due to the lack of structured output support in A2A \"\n                \"protocol.\",\n            )\n\n        from a2a.types import Message as A2AMessage\n\n        # Merge observed messages with input messages\n        msgs_list = self._observed_msgs\n\n        if msg is not None:\n            if isinstance(msg, Msg):\n                msgs_list.append(msg)\n            else:\n                msgs_list.extend(msg)\n\n        # Create A2A client and send message\n        client = self._a2a_client_factory.create(\n            card=self.agent_card,\n        )\n\n        # Convert Msg objects into A2A Message object\n        a2a_message = await self.formatter.format([_ for _ in msgs_list if _])\n\n        response_msg = None\n        async for item in client.send_message(a2a_message):\n            if isinstance(item, A2AMessage):\n                response_msg = await self.formatter.format_a2a_message(\n                    self.name,\n                    item,\n                )\n                await self.print(response_msg)\n\n            elif isinstance(item, tuple):\n                task, _ = item\n\n                if task is not None:\n                    for _ in await self.formatter.format_a2a_task(\n                        self.name,\n                        task,\n                    ):\n                        await self.print(_)\n                        response_msg = _\n\n        # Clear the observed messages after processing\n        self._observed_msgs.clear()\n\n        if response_msg:\n            return response_msg\n\n        raise ValueError(\n            \"No response received from remote agent\",\n        )\n\n    # pylint: disable=unused-argument\n    async def handle_interrupt(\n        self,\n        msg: Msg | list[Msg] | None = None,\n        structured_model: Type[BaseModel] | None = None,\n    ) -> Msg:\n        \"\"\"The post-processing logic when the reply is interrupted by the\n        user or something else.\n        \"\"\"\n\n        response_msg = Msg(\n            self.name,\n            \"I noticed that you have interrupted me. What can I \"\n            \"do for you?\",\n            \"assistant\",\n            metadata={\n                # Expose this field to indicate the interruption\n                \"_is_interrupted\": True,\n            },\n        )\n\n        await self.print(response_msg, True)\n\n        # Add to observed messages for context in next reply\n        self._observed_msgs.append(response_msg)\n\n        return response_msg\n"
  },
  {
    "path": "src/agentscope/agent/_agent_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The agent base class in agentscope.\"\"\"\nimport asyncio\nimport io\nimport json\nimport os\nfrom asyncio import Task, Queue\nfrom collections import OrderedDict\nfrom copy import deepcopy\nfrom typing import Callable, Any\nimport base64\nimport shortuuid\nimport numpy as np\nfrom typing_extensions import deprecated\n\nfrom ._agent_meta import _AgentMeta\nfrom .._logging import logger\nfrom ..module import StateModule\nfrom ..message import (\n    Msg,\n    AudioBlock,\n    ToolUseBlock,\n    ToolResultBlock,\n    ImageBlock,\n    VideoBlock,\n)\nfrom ..types import AgentHookTypes\n\n\nclass AgentBase(StateModule, metaclass=_AgentMeta):\n    \"\"\"Base class for asynchronous agents.\"\"\"\n\n    id: str\n    \"\"\"The agent's unique identifier, generated using shortuuid.\"\"\"\n\n    supported_hook_types: list[str] = [\n        \"pre_reply\",\n        \"post_reply\",\n        \"pre_print\",\n        \"post_print\",\n        \"pre_observe\",\n        \"post_observe\",\n    ]\n    \"\"\"Supported hook types for the agent base class.\"\"\"\n\n    _class_pre_reply_hooks: dict[\n        str,\n        Callable[\n            [\n                \"AgentBase\",  # self\n                dict[str, Any],  # kwargs\n            ],\n            dict[str, Any] | None,  # The modified kwargs or None\n        ],\n    ] = OrderedDict()\n    \"\"\"The class-level hook functions that will be called before the reply\n    function, taking `self` object, the input arguments as input, and\n    generating the modified arguments (if needed). Then input arguments of the\n    reply function will be re-organized into a keyword arguments dictionary.\n    If the one hook returns a new dictionary, the modified arguments will be\n    passed to the next hook or the original reply function.\"\"\"\n\n    _class_post_reply_hooks: dict[\n        str,\n        Callable[\n            [\n                \"AgentBase\",  # self\n                dict[str, Any],  # kwargs\n                Msg,  # output, the output message\n            ],\n            Msg | None,\n        ],\n    ] = OrderedDict()\n    \"\"\"The class-level hook functions that will be called after the reply\n    function, which takes the `self` object and deep copied\n    positional and keyword arguments (args and kwargs), and the output message\n    as input. If the hook returns a message, the new message will be passed\n    to the next hook or the original reply function. Otherwise, the original\n    output will be passed instead.\"\"\"\n\n    _class_pre_print_hooks: dict[\n        str,\n        Callable[\n            [\n                \"AgentBase\",  # self\n                dict[str, Any],  # kwargs\n            ],\n            dict[str, Any] | None,  # The modified kwargs or None\n        ],\n    ] = OrderedDict()\n    \"\"\"The class-level hook functions that will be called before printing,\n    which takes the `self` object, a deep copied arguments dictionary as input,\n    and output the modified arguments (if needed). \"\"\"\n\n    _class_post_print_hooks: dict[\n        str,\n        Callable[\n            [\n                \"AgentBase\",  # self\n                dict[str, Any],  # kwargs\n                Any,  # output, `None` if no output\n            ],\n            Any,\n        ],\n    ] = OrderedDict()\n    \"\"\"The class-level hook functions that will be called after the speak\n    function, which takes the `self` object as input.\"\"\"\n\n    _class_pre_observe_hooks: dict[\n        str,\n        Callable[\n            [\n                \"AgentBase\",  # self\n                dict[str, Any],  # kwargs\n            ],\n            dict[str, Any] | None,  # The modified kwargs or None\n        ],\n    ] = OrderedDict()\n    \"\"\"The class-level hook functions that will be called before the observe\n    function, which takes the `self` object and a deep copied input\n    arguments dictionary as input. To change the input arguments, the hook\n    function needs to output the modified arguments dictionary, which will be\n    used as the input of the next hook function or the original observe\n    function.\"\"\"\n\n    _class_post_observe_hooks: dict[\n        str,\n        Callable[\n            [\n                \"AgentBase\",  # self\n                dict[str, Any],  # kwargs\n                None,  # The output, `None` if no output\n            ],\n            None,\n        ],\n    ] = OrderedDict()\n    \"\"\"The class-level hook functions that will be called after the observe\n    function, which takes the `self` object as input.\"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"Initialize the agent.\"\"\"\n        super().__init__()\n\n        self.id = shortuuid.uuid()\n\n        # The replying task and identify of the current replying\n        self._reply_task: Task | None = None\n        self._reply_id: str | None = None\n\n        # Initialize the instance-level hooks\n        self._instance_pre_print_hooks = OrderedDict()\n        self._instance_post_print_hooks = OrderedDict()\n\n        self._instance_pre_reply_hooks = OrderedDict()\n        self._instance_post_reply_hooks = OrderedDict()\n\n        self._instance_pre_observe_hooks = OrderedDict()\n        self._instance_post_observe_hooks = OrderedDict()\n\n        # The prefix used in streaming printing, which will save the\n        # accumulated text and audio streaming data for each message id.\n        # e.g. {\"text\": \"xxx\", \"audio\": (stream_obj, \"{base64_data}\")}\n        self._stream_prefix = {}\n\n        # The subscribers that will receive the reply message by their\n        # `observe` method. The key is the MsgHub id, and the value is the\n        # list of agents.\n        self._subscribers: dict[str, list[AgentBase]] = {}\n\n        # We add this variable in case developers want to disable the console\n        # output of the agent, e.g., in a production environment.\n        self._disable_console_output: bool = (\n            os.getenv(\n                \"AGENTSCOPE_DISABLE_CONSOLE_OUTPUT\",\n                \"false\",\n            ).lower()\n            == \"true\"\n        )\n\n        # The streaming message queue used to export the messages as a\n        # generator\n        self._disable_msg_queue: bool = True\n        self.msg_queue = None\n\n    async def observe(self, msg: Msg | list[Msg] | None) -> None:\n        \"\"\"Receive the given message(s) without generating a reply.\n\n        Args:\n            msg (`Msg | list[Msg] | None`):\n                The message(s) to be observed.\n        \"\"\"\n        raise NotImplementedError(\n            f\"The observe function is not implemented in\"\n            f\" {self.__class__.__name__} class.\",\n        )\n\n    async def reply(self, *args: Any, **kwargs: Any) -> Msg:\n        \"\"\"The main logic of the agent, which generates a reply based on the\n        current state and input arguments.\"\"\"\n        raise NotImplementedError(\n            \"The reply function is not implemented in \"\n            f\"{self.__class__.__name__} class.\",\n        )\n\n    async def print(\n        self,\n        msg: Msg,\n        last: bool = True,\n        speech: AudioBlock | list[AudioBlock] | None = None,\n    ) -> None:\n        \"\"\"The function to display the message.\n\n        Args:\n            msg (`Msg`):\n                The message object to be printed.\n            last (`bool`, defaults to `True`):\n                Whether this is the last one in streaming messages. For\n                non-streaming message, this should always be `True`.\n            speech (`AudioBlock | list[AudioBlock] | None`, optional):\n                The audio content block(s) to be played along with the\n                message.\n        \"\"\"\n        if not self._disable_msg_queue:\n            await self.msg_queue.put((deepcopy(msg), last, speech))\n            # Yield control to the event loop, allowing consumer coroutines\n            # to process messages from the queue. This prevents the producer\n            # from monopolizing the event loop.\n            await asyncio.sleep(0)\n\n        if self._disable_console_output:\n            return\n\n        # The accumulated textual content to print, including the text blocks\n        # and the thinking blocks\n        thinking_and_text_to_print = []\n\n        for block in msg.get_content_blocks():\n            if block[\"type\"] == \"text\":\n                self._print_text_block(\n                    msg.id,\n                    name_prefix=msg.name,\n                    text_content=block[\"text\"],\n                    thinking_and_text_to_print=thinking_and_text_to_print,\n                )\n\n            elif block[\"type\"] == \"thinking\":\n                self._print_text_block(\n                    msg.id,\n                    name_prefix=f\"{msg.name}(thinking)\",\n                    text_content=block[\"thinking\"],\n                    thinking_and_text_to_print=thinking_and_text_to_print,\n                )\n\n            elif last:\n                self._print_last_block(block, msg)\n\n        # Play audio block if exists\n        if isinstance(speech, list):\n            for audio_block in speech:\n                self._process_audio_block(msg.id, audio_block)\n        elif isinstance(speech, dict):\n            self._process_audio_block(msg.id, speech)\n\n        # Clean up resources if this is the last message in streaming\n        if last and msg.id in self._stream_prefix:\n            if \"audio\" in self._stream_prefix[msg.id]:\n                player, _ = self._stream_prefix[msg.id][\"audio\"]\n                # Close the miniaudio player\n                player.close()\n            stream_prefix = self._stream_prefix.pop(msg.id)\n            if \"text\" in stream_prefix and not stream_prefix[\"text\"].endswith(\n                \"\\n\",\n            ):\n                print()\n\n    def _process_audio_block(\n        self,\n        msg_id: str,\n        audio_block: AudioBlock,\n    ) -> None:\n        \"\"\"Process audio block content.\n\n        Args:\n            msg_id (`str`):\n                The unique identifier of the message\n            audio_block (`AudioBlock`):\n                The audio content block\n        \"\"\"\n        if \"source\" not in audio_block:\n            raise ValueError(\n                \"The audio block must contain the 'source' field.\",\n            )\n\n        if audio_block[\"source\"][\"type\"] == \"url\":\n            import urllib.request\n            import wave\n            import sounddevice as sd\n\n            url = audio_block[\"source\"][\"url\"]\n            try:\n                with urllib.request.urlopen(url) as response:\n                    audio_data = response.read()\n\n                with wave.open(io.BytesIO(audio_data), \"rb\") as wf:\n                    samplerate = wf.getframerate()\n                    n_frames = wf.getnframes()\n                    audio_frames = wf.readframes(n_frames)\n\n                    # Convert byte data to numpy array\n                    audio_np = np.frombuffer(audio_frames, dtype=np.int16)\n\n                    # Play audio\n                    sd.play(audio_np, samplerate)\n                    sd.wait()\n\n            except Exception as e:\n                logger.error(\n                    \"Failed to play audio from url %s: %s\",\n                    url,\n                    str(e),\n                )\n\n        elif audio_block[\"source\"][\"type\"] == \"base64\":\n            data = audio_block[\"source\"][\"data\"]\n\n            if msg_id not in self._stream_prefix:\n                self._stream_prefix[msg_id] = {}\n\n            audio_prefix = self._stream_prefix[msg_id].get(\"audio\", None)\n\n            import sounddevice as sd\n\n            # The player and the prefix data is cached for streaming audio\n            if audio_prefix:\n                player, audio_prefix_data = audio_prefix\n            else:\n                player = sd.OutputStream(\n                    samplerate=24000,\n                    channels=1,\n                    dtype=np.float32,\n                    blocksize=1024,\n                    latency=\"low\",\n                )\n                player.start()\n                audio_prefix_data = \"\"\n\n            # play the audio data\n            new_audio_data = data[len(audio_prefix_data) :]\n            if new_audio_data:\n                audio_bytes = base64.b64decode(new_audio_data)\n                audio_np = np.frombuffer(audio_bytes, dtype=np.int16)\n                audio_float = audio_np.astype(np.float32) / 32768.0\n\n                # Write to the audio output stream\n                player.write(audio_float)\n\n            # save the player and the prefix data\n            self._stream_prefix[msg_id][\"audio\"] = (\n                player,\n                data,\n            )\n\n        else:\n            raise ValueError(\n                \"Unsupported audio source type: \"\n                f\"{audio_block['source']['type']}\",\n            )\n\n    def _print_text_block(\n        self,\n        msg_id: str,\n        name_prefix: str,\n        text_content: str,\n        thinking_and_text_to_print: list[str],\n    ) -> None:\n        \"\"\"Print the text block and thinking block content.\n\n        Args:\n            msg_id (`str`):\n                The unique identifier of the message\n            name_prefix (`str`):\n                The prefix for the message, e.g. \"{name}: \" for text block and\n                \"{name}(thinking): \" for thinking block.\n            text_content (`str`):\n                The textual content to be printed.\n            thinking_and_text_to_print (`list[str]`):\n                A list of textual content to be printed together. Here we\n                gather the text and thinking blocks to print them together.\n        \"\"\"\n        thinking_and_text_to_print.append(\n            f\"{name_prefix}: {text_content}\",\n        )\n        # The accumulated text and thinking blocks to print\n        to_print = \"\\n\".join(thinking_and_text_to_print)\n\n        # The text prefix that has been printed\n        if msg_id not in self._stream_prefix:\n            self._stream_prefix[msg_id] = {}\n\n        text_prefix = self._stream_prefix[msg_id].get(\"text\", \"\")\n\n        # Only print when there is new text content\n        if len(to_print) > len(text_prefix):\n            print(to_print[len(text_prefix) :], end=\"\")\n\n            # Save the printed text prefix\n            self._stream_prefix[msg_id][\"text\"] = to_print\n\n    def _print_last_block(\n        self,\n        block: ToolUseBlock\n        | ToolResultBlock\n        | ImageBlock\n        | VideoBlock\n        | AudioBlock,\n        msg: Msg,\n    ) -> None:\n        \"\"\"Process and print the last content block, and the block type\n        is not text, or thinking.\n\n        Args:\n            block (`ToolUseBlock | ToolResultBlock | ImageBlock | VideoBlock \\\n            | AudioBlock`):\n                The content block to be printed\n            msg (`Msg`):\n                The message object\n        \"\"\"\n        # TODO: We should consider how to handle the multimodal blocks in the\n        #  terminal, since the base64 data may be too long to display.\n        if block.get(\"type\") in [\"image\", \"video\", \"audio\"]:\n            return\n\n        text_prefix = self._stream_prefix.get(msg.id, {}).get(\"text\", \"\")\n\n        if text_prefix:\n            # Add a newline to separate from previous text content\n            print_newline = \"\" if text_prefix.endswith(\"\\n\") else \"\\n\"\n            print(\n                f\"{print_newline}\"\n                f\"{json.dumps(block, indent=4, ensure_ascii=False)}\",\n            )\n        else:\n            print(\n                f\"{msg.name}:\"\n                f\" {json.dumps(block, indent=4, ensure_ascii=False)}\",\n            )\n\n    async def __call__(self, *args: Any, **kwargs: Any) -> Msg:\n        \"\"\"Call the reply function with the given arguments.\"\"\"\n        self._reply_id = shortuuid.uuid()\n\n        reply_msg: Msg | None = None\n        try:\n            self._reply_task = asyncio.current_task()\n            reply_msg = await self.reply(*args, **kwargs)\n\n        # The interruption is triggered by calling the interrupt method\n        except asyncio.CancelledError:\n            reply_msg = await self.handle_interrupt(*args, **kwargs)\n\n        finally:\n            # Broadcast the reply message to all subscribers\n            if reply_msg:\n                await self._broadcast_to_subscribers(reply_msg)\n            self._reply_task = None\n\n        return reply_msg\n\n    async def _broadcast_to_subscribers(\n        self,\n        msg: Msg | list[Msg] | None,\n    ) -> None:\n        \"\"\"Broadcast the message to all subscribers.\"\"\"\n        for subscribers in self._subscribers.values():\n            for subscriber in subscribers:\n                await subscriber.observe(msg)\n\n    async def handle_interrupt(\n        self,\n        *args: Any,\n        **kwargs: Any,\n    ) -> Msg:\n        \"\"\"The post-processing logic when the reply is interrupted by the\n        user or something else.\"\"\"\n        raise NotImplementedError(\n            f\"The handle_interrupt function is not implemented in \"\n            f\"{self.__class__.__name__}\",\n        )\n\n    async def interrupt(self, msg: Msg | list[Msg] | None = None) -> None:\n        \"\"\"Interrupt the current reply process.\"\"\"\n        if self._reply_task and not self._reply_task.done():\n            self._reply_task.cancel(msg)\n\n    def register_instance_hook(\n        self,\n        hook_type: AgentHookTypes,\n        hook_name: str,\n        hook: Callable,\n    ) -> None:\n        \"\"\"Register a hook to the agent instance, which only takes effect\n        for the current instance.\n\n        Args:\n            hook_type (`str`):\n                The type of the hook, indicating where the hook is to be\n                triggered.\n            hook_name (`str`):\n                The name of the hook. If the name is already registered, the\n                hook will be overwritten.\n            hook (`Callable`):\n                The hook function.\n        \"\"\"\n        if not isinstance(self, AgentBase):\n            raise TypeError(\n                \"The register_instance_hook method should be called on an \"\n                f\"instance of AsyncAgentBase, but got {self} of \"\n                f\"type {type(self)}.\",\n            )\n        hooks = getattr(self, f\"_instance_{hook_type}_hooks\")\n        hooks[hook_name] = hook\n\n    def remove_instance_hook(\n        self,\n        hook_type: AgentHookTypes,\n        hook_name: str,\n    ) -> None:\n        \"\"\"Remove an instance-level hook from the agent instance.\n\n        Args:\n            hook_type (`AgentHookTypes`):\n                The type of the hook, indicating where the hook is to be\n                triggered.\n            hook_name (`str`):\n                The name of the hook to remove.\n        \"\"\"\n        if not isinstance(self, AgentBase):\n            raise TypeError(\n                \"The remove_instance_hook method should be called on an \"\n                f\"instance of AsyncAgentBase, but got {self} of \"\n                f\"type {type(self)}.\",\n            )\n        hooks = getattr(self, f\"_instance_{hook_type}_hooks\")\n        if hook_name in hooks:\n            del hooks[hook_name]\n        else:\n            raise ValueError(\n                f\"Hook '{hook_name}' not found in '{hook_type}' hooks of \"\n                f\"{self.__class__.__name__} instance.\",\n            )\n\n    @classmethod\n    def register_class_hook(\n        cls,\n        hook_type: AgentHookTypes,\n        hook_name: str,\n        hook: Callable,\n    ) -> None:\n        \"\"\"The universal function to register a hook to the agent class, which\n        will take effect for all instances of the class.\n\n        Args:\n            hook_type (`AgentHookTypes`):\n                The type of the hook, indicating where the hook is to be\n                triggered.\n            hook_name (`str`):\n                The name of the hook. If the name is already registered, the\n                hook will be overwritten.\n            hook (`Callable`):\n                The hook function.\n        \"\"\"\n\n        assert (\n            hook_type in cls.supported_hook_types\n        ), f\"Invalid hook type: {hook_type}\"\n\n        hooks = getattr(cls, f\"_class_{hook_type}_hooks\")\n        hooks[hook_name] = hook\n\n    @classmethod\n    def remove_class_hook(\n        cls,\n        hook_type: AgentHookTypes,\n        hook_name: str,\n    ) -> None:\n        \"\"\"Remove a class-level hook from the agent class.\n\n        Args:\n            hook_type (`AgentHookTypes`):\n                The type of the hook, indicating where the hook is to be\n                triggered.\n            hook_name (`str`):\n                The name of the hook to remove.\n        \"\"\"\n\n        assert (\n            hook_type in cls.supported_hook_types\n        ), f\"Invalid hook type: {hook_type}\"\n        hooks = getattr(cls, f\"_class_{hook_type}_hooks\")\n        if hook_name in hooks:\n            del hooks[hook_name]\n\n        else:\n            raise ValueError(\n                f\"Hook '{hook_name}' not found in '{hook_type}' hooks of \"\n                f\"{cls.__name__} class.\",\n            )\n\n    @classmethod\n    def clear_class_hooks(\n        cls,\n        hook_type: AgentHookTypes | None = None,\n    ) -> None:\n        \"\"\"Clear all class-level hooks.\n\n        Args:\n            hook_type (`AgentHookTypes`, optional):\n                The type of the hook to clear. If not specified, all\n                class-level hooks will be cleared.\n        \"\"\"\n\n        if hook_type is None:\n            for typ in cls.supported_hook_types:\n                hooks = getattr(cls, f\"_class_{typ}_hooks\")\n                hooks.clear()\n        else:\n            assert (\n                hook_type in cls.supported_hook_types\n            ), f\"Invalid hook type: {hook_type}\"\n            hooks = getattr(cls, f\"_class_{hook_type}_hooks\")\n            hooks.clear()\n\n    def clear_instance_hooks(\n        self,\n        hook_type: AgentHookTypes | None = None,\n    ) -> None:\n        \"\"\"If `hook_type` is not specified, clear all instance-level hooks.\n        Otherwise, clear the specified type of instance-level hooks.\"\"\"\n        if hook_type is None:\n            for typ in self.supported_hook_types:\n                if not hasattr(self, f\"_instance_{typ}_hooks\"):\n                    raise ValueError(\n                        f\"Call super().__init__() in the constructor \"\n                        f\"to initialize the instance-level hooks for \"\n                        f\"{self.__class__.__name__}.\",\n                    )\n                hooks = getattr(self, f\"_instance_{typ}_hooks\")\n                hooks.clear()\n\n        else:\n            assert (\n                hook_type in self.supported_hook_types\n            ), f\"Invalid hook type: {hook_type}\"\n            if not hasattr(self, f\"_instance_{hook_type}_hooks\"):\n                raise ValueError(\n                    f\"Call super().__init__() in the constructor \"\n                    f\"to initialize the instance-level hooks for \"\n                    f\"{self.__class__.__name__}.\",\n                )\n            hooks = getattr(self, f\"_instance_{hook_type}_hooks\")\n            hooks.clear()\n\n    def reset_subscribers(\n        self,\n        msghub_name: str,\n        subscribers: list[\"AgentBase\"],\n    ) -> None:\n        \"\"\"Reset the subscribers of the agent.\n\n        Args:\n            msghub_name (`str`):\n                The name of the MsgHub that manages the subscribers.\n            subscribers (`list[AgentBase]`):\n                A list of agents that will receive the reply message from\n                this agent via their `observe` method.\n        \"\"\"\n        self._subscribers[msghub_name] = [_ for _ in subscribers if _ != self]\n\n    def remove_subscribers(self, msghub_name: str) -> None:\n        \"\"\"Remove the msghub subscribers by the given msg hub name.\n\n        Args:\n            msghub_name (`str`):\n                The name of the MsgHub that manages the subscribers.\n        \"\"\"\n        if msghub_name not in self._subscribers:\n            logger.warning(\n                \"MsgHub named '%s' not found\",\n                msghub_name,\n            )\n        else:\n            self._subscribers.pop(msghub_name)\n\n    @deprecated(\"Please use set_console_output_enabled() instead.\")\n    def disable_console_output(self) -> None:\n        \"\"\"This function will disable the console output of the agent, e.g.\n        in a production environment to avoid messy logs.\"\"\"\n        self._disable_console_output = True\n\n    def set_console_output_enabled(self, enabled: bool) -> None:\n        \"\"\"Enable or disable the console output of the agent. E.g. in a\n        production environment, you may want to disable the console output to\n        avoid messy logs.\n\n        Args:\n            enabled (`bool`):\n                If `True`, enable the console output. If `False`, disable\n                the console output.\n        \"\"\"\n        self._disable_console_output = not enabled\n\n    def set_msg_queue_enabled(\n        self,\n        enabled: bool,\n        queue: Queue | None = None,\n    ) -> None:\n        \"\"\"Enable or disable the message queue for streaming outputs.\n\n        Args:\n            enabled (`bool`):\n                If `True`, enable the message queue to allow streaming\n                outputs. If `False`, disable the message queue.\n            queue (`Queue | None`, optional):\n                The queue instance that will be used to initialize the\n                message queue when `enable` is `True`.\n        \"\"\"\n        if enabled:\n            if queue is None:\n                if self.msg_queue is None:\n                    self.msg_queue = asyncio.Queue(maxsize=100)\n            else:\n                self.msg_queue = queue\n        else:\n            self.msg_queue = None\n\n        self._disable_msg_queue = not enabled\n"
  },
  {
    "path": "src/agentscope/agent/_agent_meta.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The metaclass for agents in agentscope.\"\"\"\nimport inspect\nfrom copy import deepcopy\nfrom functools import wraps\nfrom typing import (\n    Any,\n    Dict,\n    TYPE_CHECKING,\n    Callable,\n)\n\nfrom .._utils._common import _execute_async_or_sync_func\n\nif TYPE_CHECKING:\n    from ._agent_base import AgentBase\nelse:\n    AgentBase = \"AgentBase\"\n\n\ndef _normalize_to_kwargs(\n    func: Callable,\n    self: Any,\n    *args: Any,\n    **kwargs: Any,\n) -> dict:\n    \"\"\"Normalize the provided positional and keyword arguments into a\n    keyword arguments dictionary that matches the function signature.\"\"\"\n    sig = inspect.signature(func)\n    try:\n        # Bind the provided arguments to the function signature\n        bound = sig.bind(self, *args, **kwargs)\n        # Apply the default values for parameters\n        bound.apply_defaults()\n\n        # Return the arguments in a dictionary format\n        res = dict(bound.arguments)\n        res.pop(\"self\")\n        return res\n\n    except TypeError as e:\n        # If failed to bind, we raise a TypeError with more context\n        param_names = list(sig.parameters.keys())\n        provided_args = len(args)\n        provided_kwargs = list(kwargs.keys())\n\n        raise TypeError(\n            f\"Failed to bind parameters for function '{func.__name__}': {e}\\n\"\n            f\"Expected parameters: {param_names}\\n\"\n            f\"Provided {provided_args} positional args and kwargs: \"\n            f\"{provided_kwargs}\",\n        ) from e\n\n\ndef _wrap_with_hooks(\n    original_func: Callable,\n) -> Callable:\n    \"\"\"A decorator to wrap the original async function with pre- and post-hooks\n\n    Args:\n        original_func (`Callable`):\n            The original async function to be wrapped with hooks.\n    \"\"\"\n    func_name = original_func.__name__.replace(\"_\", \"\")\n\n    @wraps(original_func)\n    async def async_wrapper(\n        self: AgentBase,\n        *args: Any,\n        **kwargs: Any,\n    ) -> Any:\n        \"\"\"The wrapped function, which call the pre- and post-hooks before and\n        after the original function.\"\"\"\n\n        # Unify all positional and keyword arguments into a keyword arguments\n        normalized_kwargs = _normalize_to_kwargs(\n            original_func,\n            self,\n            *args,\n            **kwargs,\n        )\n\n        current_normalized_kwargs = normalized_kwargs\n        assert (\n            hasattr(self, f\"_instance_pre_{func_name}_hooks\")\n            and hasattr(self, f\"_instance_post_{func_name}_hooks\")\n            and hasattr(self.__class__, f\"_class_pre_{func_name}_hooks\")\n            and hasattr(self.__class__, f\"_class_post_{func_name}_hooks\")\n        ), f\"Hooks for {func_name} not found in {self.__class__.__name__}\"\n\n        # pre-hooks\n        pre_hooks = list(\n            getattr(self, f\"_instance_pre_{func_name}_hooks\").values(),\n        ) + list(\n            getattr(self, f\"_class_pre_{func_name}_hooks\").values(),\n        )\n        for pre_hook in pre_hooks:\n            modified_keywords = await _execute_async_or_sync_func(\n                pre_hook,\n                self,\n                deepcopy(current_normalized_kwargs),\n            )\n            if modified_keywords is not None:\n                assert isinstance(modified_keywords, dict), (\n                    f\"Pre-hook must return a dict of keyword arguments, rather\"\n                    f\" than {type(modified_keywords)} from hook \"\n                    f\"{pre_hook.__name__}\"\n                )\n                current_normalized_kwargs = modified_keywords\n\n        # original function\n        # handle positional and keyword arguments specifically\n        args = current_normalized_kwargs.get(\"args\", [])\n        kwargs = current_normalized_kwargs.get(\"kwargs\", {})\n        others = {\n            k: v\n            for k, v in current_normalized_kwargs.items()\n            if k not in [\"args\", \"kwargs\"]\n        }\n        current_output = await original_func(\n            self,\n            *args,\n            **others,\n            **kwargs,\n        )\n\n        # post_hooks\n        post_hooks = list(\n            getattr(self, f\"_instance_post_{func_name}_hooks\").values(),\n        ) + list(\n            getattr(self, f\"_class_post_{func_name}_hooks\").values(),\n        )\n        for post_hook in post_hooks:\n            modified_output = await _execute_async_or_sync_func(\n                post_hook,\n                self,\n                deepcopy(current_normalized_kwargs),\n                deepcopy(current_output),\n            )\n            if modified_output is not None:\n                current_output = modified_output\n        return current_output\n\n    return async_wrapper\n\n\nclass _AgentMeta(type):\n    \"\"\"The agent metaclass that wraps the agent's reply, observe and print\n    functions with pre- and post-hooks.\"\"\"\n\n    def __new__(mcs, name: Any, bases: Any, attrs: Dict) -> Any:\n        \"\"\"Wrap the agent's functions with hooks.\"\"\"\n\n        for func_name in [\n            \"reply\",\n            \"print\",\n            \"observe\",\n        ]:\n            if func_name in attrs:\n                attrs[func_name] = _wrap_with_hooks(attrs[func_name])\n\n        return super().__new__(mcs, name, bases, attrs)\n\n\nclass _ReActAgentMeta(_AgentMeta):\n    \"\"\"The ReAct metaclass that adds pre- and post-hooks for the _reasoning\n    and _acting functions.\"\"\"\n\n    def __new__(mcs, name: Any, bases: Any, attrs: Dict) -> Any:\n        \"\"\"Wrap the ReAct agent's _reasoning and _acting functions with\n        hooks.\"\"\"\n\n        for func_name in [\n            \"_reasoning\",\n            \"_acting\",\n        ]:\n            if func_name in attrs:\n                attrs[func_name] = _wrap_with_hooks(attrs[func_name])\n\n        return super().__new__(mcs, name, bases, attrs)\n"
  },
  {
    "path": "src/agentscope/agent/_react_agent.py",
    "content": "# -*- coding: utf-8 -*-\n# TODO: simplify the ReActAgent class\n# pylint: disable=not-an-iterable, too-many-lines\n# mypy: disable-error-code=\"list-item\"\n\"\"\"ReAct agent class in agentscope.\"\"\"\nimport asyncio\nfrom enum import Enum\nfrom typing import Type, Any, AsyncGenerator, Literal\n\nfrom pydantic import BaseModel, ValidationError, Field\n\nfrom ._utils import _AsyncNullContext\nfrom ._react_agent_base import ReActAgentBase\nfrom .._logging import logger\nfrom ..formatter import FormatterBase\nfrom ..memory import MemoryBase, LongTermMemoryBase, InMemoryMemory\nfrom ..message import (\n    Msg,\n    ToolUseBlock,\n    ToolResultBlock,\n    TextBlock,\n    AudioBlock,\n)\nfrom ..model import ChatModelBase\nfrom ..rag import KnowledgeBase, Document\nfrom ..plan import PlanNotebook\nfrom ..token import TokenCounterBase\nfrom ..tool import Toolkit, ToolResponse\nfrom ..tracing import trace_reply\nfrom ..tts import TTSModelBase\n\n\nclass _QueryRewriteModel(BaseModel):\n    \"\"\"The structured model used for query rewriting.\"\"\"\n\n    rewritten_query: str = Field(\n        description=(\n            \"The rewritten query, which should be specific and concise. \"\n        ),\n    )\n\n\nclass SummarySchema(BaseModel):\n    \"\"\"The compressed memory model, used to generate summary of old memories\"\"\"\n\n    task_overview: str = Field(\n        max_length=300,\n        description=(\n            \"The user's core request and success criteria.\\n\"\n            \"Any clarifications or constraints they specified\"\n        ),\n    )\n    current_state: str = Field(\n        max_length=300,\n        description=(\n            \"What has been completed so far.\\n\"\n            \"File created, modified, or analyzed (with paths if relevant).\\n\"\n            \"Key outputs or artifacts produced.\"\n        ),\n    )\n    important_discoveries: str = Field(\n        max_length=300,\n        description=(\n            \"Technical constraints or requirements uncovered.\\n\"\n            \"Decisions made and their rationale.\\n\"\n            \"Errors encountered and how they were resolved.\\n\"\n            \"What approaches were tried that didn't work (and why)\"\n        ),\n    )\n    next_steps: str = Field(\n        max_length=200,\n        description=(\n            \"Specific actions needed to complete the task.\\n\"\n            \"Any blockers or open questions to resolve.\\n\"\n            \"Priority order if multiple steps remain\"\n        ),\n    )\n    context_to_preserve: str = Field(\n        max_length=300,\n        description=(\n            \"User preferences or style requirements.\\n\"\n            \"Domain-specific details that aren't obvious.\\n\"\n            \"Any promises made to the user\"\n        ),\n    )\n\n\nclass _MemoryMark(str, Enum):\n    \"\"\"The memory marks used in the ReAct agent.\"\"\"\n\n    HINT = \"hint\"\n    \"\"\"Used to mark the hint messages that will be cleared after use.\"\"\"\n\n    COMPRESSED = \"compressed\"\n    \"\"\"Used to mark the compressed messages in the memory.\"\"\"\n\n\nclass ReActAgent(ReActAgentBase):\n    \"\"\"A ReAct agent implementation in AgentScope, which supports\n\n    - Realtime steering\n    - API-based (parallel) tool calling\n    - Hooks around reasoning, acting, reply, observe and print functions\n    - Structured output generation\n    \"\"\"\n\n    class CompressionConfig(BaseModel):\n        \"\"\"The compression related configuration in AgentScope\"\"\"\n\n        model_config = {\"arbitrary_types_allowed\": True}\n        \"\"\"Allow arbitrary types in the pydantic model.\"\"\"\n\n        enable: bool\n        \"\"\"Whether to enable the auto compression feature.\"\"\"\n\n        agent_token_counter: TokenCounterBase\n        \"\"\"The token counter for the agent's model, which must be consistent\n        with the model used in the agent.\"\"\"\n\n        trigger_threshold: int\n        \"\"\"The token threshold to trigger the compression process. When the\n        total token count in the memory exceeds this threshold, the\n        compression will be activated.\"\"\"\n\n        keep_recent: int = 3\n        \"\"\"The number of most recent messages to keep uncompressed in the\n        memory to preserve the recent context.\"\"\"\n\n        compression_prompt: str = (\n            \"<system-hint>You have been working on the task described above \"\n            \"but have not yet completed it. \"\n            \"Now write a continuation summary that will allow you to resume \"\n            \"work efficiently in a future context window where the \"\n            \"conversation history will be replaced with this summary. \"\n            \"Your summary should be structured, concise, and actionable.\"\n            \"</system-hint>\"\n        )\n        \"\"\"The prompt used to guide the compression model to generate the\n        compressed summary, which will be wrapped into a user message and\n        attach to the end of the current memory.\"\"\"\n\n        summary_template: str = (\n            \"<system-info>Here is a summary of your previous work\\n\"\n            \"# Task Overview\\n\"\n            \"{task_overview}\\n\\n\"\n            \"# Current State\\n\"\n            \"{current_state}\\n\\n\"\n            \"# Important Discoveries\\n\"\n            \"{important_discoveries}\\n\\n\"\n            \"# Next Steps\\n\"\n            \"{next_steps}\\n\\n\"\n            \"# Context to Preserve\\n\"\n            \"{context_to_preserve}\"\n            \"</system-info>\"\n        )\n        \"\"\"The string template to present the compressed summary to the agent,\n        which will be formatted with the fields from the\n        `compression_summary_model`.\"\"\"\n\n        summary_schema: Type[BaseModel] = SummarySchema\n        \"\"\"The structured model used to guide the agent to generate the\n        structured compressed summary.\"\"\"\n\n        compression_model: ChatModelBase | None = None\n        \"\"\"The compression model used to generate the compressed summary. If\n        not provided, the agent's model will be used.\"\"\"\n\n        compression_formatter: FormatterBase | None = None\n        \"\"\"The corresponding formatter form the compression model, when the\n        `compression_model` is provided, the `compression_formatter` must also\n        be provided.\"\"\"\n\n    finish_function_name: str = \"generate_response\"\n    \"\"\"The name of the function used to generate structured output. Only\n    registered when structured output model is provided in the reply call.\"\"\"\n\n    def __init__(\n        self,\n        name: str,\n        sys_prompt: str,\n        model: ChatModelBase,\n        formatter: FormatterBase,\n        toolkit: Toolkit | None = None,\n        memory: MemoryBase | None = None,\n        long_term_memory: LongTermMemoryBase | None = None,\n        long_term_memory_mode: Literal[\n            \"agent_control\",\n            \"static_control\",\n            \"both\",\n        ] = \"both\",\n        enable_meta_tool: bool = False,\n        parallel_tool_calls: bool = False,\n        knowledge: KnowledgeBase | list[KnowledgeBase] | None = None,\n        enable_rewrite_query: bool = True,\n        plan_notebook: PlanNotebook | None = None,\n        print_hint_msg: bool = False,\n        max_iters: int = 10,\n        tts_model: TTSModelBase | None = None,\n        compression_config: CompressionConfig | None = None,\n    ) -> None:\n        \"\"\"Initialize the ReAct agent\n\n        Args:\n            name (`str`):\n                The name of the agent.\n            sys_prompt (`str`):\n                The system prompt of the agent.\n            model (`ChatModelBase`):\n                The chat model used by the agent.\n            formatter (`FormatterBase`):\n                The formatter used to format the messages into the required\n                format of the model API provider.\n            toolkit (`Toolkit | None`, optional):\n                A `Toolkit` object that contains the tool functions. If not\n                provided, a default empty `Toolkit` will be created.\n            memory (`MemoryBase | None`, optional):\n                The memory used to store the dialogue history. If not provided,\n                a default `InMemoryMemory` will be created, which stores\n                messages in a list in memory.\n            long_term_memory (`LongTermMemoryBase | None`, optional):\n                The optional long-term memory, which will provide two tool\n                functions: `retrieve_from_memory` and `record_to_memory`, and\n                will attach the retrieved information to the system prompt\n                before each reply.\n            enable_meta_tool (`bool`, defaults to `False`):\n                If `True`, a meta tool function `reset_equipped_tools` will be\n                added to the toolkit, which allows the agent to manage its\n                equipped tools dynamically.\n            long_term_memory_mode (`Literal['agent_control', 'static_control',\\\n              'both']`, defaults to `both`):\n                The mode of the long-term memory. If `agent_control`, two\n                tool functions `retrieve_from_memory` and `record_to_memory`\n                will be registered in the toolkit to allow the agent to\n                manage the long-term memory. If `static_control`, retrieving\n                and recording will happen in the beginning and end of\n                each reply respectively.\n            parallel_tool_calls (`bool`, defaults to `False`):\n                When LLM generates multiple tool calls, whether to execute\n                them in parallel.\n            knowledge (`KnowledgeBase | list[KnowledgeBase] | None`, optional):\n                The knowledge object(s) used by the agent to retrieve\n                relevant documents at the beginning of each reply.\n            enable_rewrite_query (`bool`, defaults to `True`):\n                Whether ask the agent to rewrite the user input query before\n                retrieving from the knowledge base(s), e.g. rewrite \"Who am I\"\n                to \"{user's name}\" to get more relevant documents. Only works\n                when the knowledge base(s) is provided.\n            plan_notebook (`PlanNotebook | None`, optional):\n                The plan notebook instance, allow the agent to finish the\n                complex task by decomposing it into a sequence of subtasks.\n            print_hint_msg (`bool`, defaults to `False`):\n                Whether to print the hint messages, including the reasoning\n                hint from the plan notebook, the retrieved information from\n                the long-term memory and knowledge base(s).\n            max_iters (`int`, defaults to `10`):\n                The maximum number of iterations of the reasoning-acting loops.\n            tts_model (`TTSModelBase | None` optional):\n                The TTS model used by the agent.\n            compression_config (`CompressionConfig | None`, optional):\n                The compression configuration. If provided, the auto\n                compression will be activated.\n        \"\"\"\n        super().__init__()\n\n        assert long_term_memory_mode in [\n            \"agent_control\",\n            \"static_control\",\n            \"both\",\n        ]\n\n        # Static variables in the agent\n        self.name = name\n        self._sys_prompt = sys_prompt\n        self.max_iters = max_iters\n        self.model = model\n        self.formatter = formatter\n        self.tts_model = tts_model\n        self.compression_config = compression_config\n\n        # -------------- Memory management --------------\n        # Record the dialogue history in the memory\n        self.memory = memory or InMemoryMemory()\n        # If provide the long-term memory, it will be used to retrieve info\n        # in the beginning of each reply, and the result will be added to the\n        # system prompt\n        self.long_term_memory = long_term_memory\n\n        # The long-term memory mode\n        self._static_control = long_term_memory and long_term_memory_mode in [\n            \"static_control\",\n            \"both\",\n        ]\n        self._agent_control = long_term_memory and long_term_memory_mode in [\n            \"agent_control\",\n            \"both\",\n        ]\n\n        # -------------- Tool management --------------\n        # If None, a default Toolkit will be created\n        self.toolkit = toolkit or Toolkit()\n        if self._agent_control:\n            # Adding two tool functions into the toolkit to allow self-control\n            self.toolkit.register_tool_function(\n                long_term_memory.record_to_memory,\n            )\n            self.toolkit.register_tool_function(\n                long_term_memory.retrieve_from_memory,\n            )\n        # Add a meta tool function to allow agent-controlled tool management\n        if enable_meta_tool:\n            self.toolkit.register_tool_function(\n                self.toolkit.reset_equipped_tools,\n            )\n\n        self.parallel_tool_calls = parallel_tool_calls\n\n        # -------------- RAG management --------------\n        # The knowledge base(s) used by the agent\n        if isinstance(knowledge, KnowledgeBase):\n            knowledge = [knowledge]\n        self.knowledge: list[KnowledgeBase] = knowledge or []\n        self.enable_rewrite_query = enable_rewrite_query\n\n        # -------------- Plan management --------------\n        # Equipped the plan-related tools provided by the plan notebook as\n        # a tool group named \"plan_related\". So that the agent can activate\n        # the plan tools by the meta tool function\n        self.plan_notebook = None\n        if plan_notebook:\n            self.plan_notebook = plan_notebook\n            # When enable_meta_tool is True, plan tools are in plan_related\n            # group and active by agent.\n            # Otherwise, plan tools in basic group and always active.\n            if enable_meta_tool:\n                self.toolkit.create_tool_group(\n                    \"plan_related\",\n                    description=self.plan_notebook.description,\n                )\n                for tool in plan_notebook.list_tools():\n                    self.toolkit.register_tool_function(\n                        tool,\n                        group_name=\"plan_related\",\n                    )\n            else:\n                for tool in plan_notebook.list_tools():\n                    self.toolkit.register_tool_function(\n                        tool,\n                    )\n\n        # If print the reasoning hint messages\n        self.print_hint_msg = print_hint_msg\n\n        # The maximum number of iterations of the reasoning-acting loops\n        self.max_iters = max_iters\n\n        # Variables to record the intermediate state\n\n        # If required structured output model is provided\n        self._required_structured_model: Type[BaseModel] | None = None\n\n        # -------------- State registration and hooks --------------\n        # Register the status variables\n        self.register_state(\"name\")\n        self.register_state(\"_sys_prompt\")\n\n    @property\n    def sys_prompt(self) -> str:\n        \"\"\"The dynamic system prompt of the agent.\"\"\"\n        agent_skill_prompt = self.toolkit.get_agent_skill_prompt()\n        if agent_skill_prompt:\n            return self._sys_prompt + \"\\n\\n\" + agent_skill_prompt\n        else:\n            return self._sys_prompt\n\n    @trace_reply\n    async def reply(  # pylint: disable=too-many-branches\n        self,\n        msg: Msg | list[Msg] | None = None,\n        structured_model: Type[BaseModel] | None = None,\n    ) -> Msg:\n        \"\"\"Generate a reply based on the current state and input arguments.\n\n        Args:\n            msg (`Msg | list[Msg] | None`, optional):\n                The input message(s) to the agent.\n            structured_model (`Type[BaseModel] | None`, optional):\n                The required structured output model. If provided, the agent\n                is expected to generate structured output in the `metadata`\n                field of the output message.\n\n        Returns:\n            `Msg`:\n                The output message generated by the agent.\n        \"\"\"\n        # Record the input message(s) in the memory\n        await self.memory.add(msg)\n\n        # -------------- Retrieval process --------------\n        # Retrieve relevant records from the long-term memory if activated\n        await self._retrieve_from_long_term_memory(msg)\n        # Retrieve relevant documents from the knowledge base(s) if any\n        await self._retrieve_from_knowledge(msg)\n\n        # Control if LLM generates tool calls in each reasoning step\n        tool_choice: Literal[\"auto\", \"none\", \"required\"] | None = None\n\n        # -------------- Structured output management --------------\n        self._required_structured_model = structured_model\n        # Record structured output model if provided\n        if structured_model:\n            # Register generate_response tool only when structured output\n            # is required\n            if self.finish_function_name not in self.toolkit.tools:\n                self.toolkit.register_tool_function(\n                    getattr(self, self.finish_function_name),\n                )\n\n            # Set the structured output model\n            self.toolkit.set_extended_model(\n                self.finish_function_name,\n                structured_model,\n            )\n            tool_choice = \"required\"\n        else:\n            # Remove generate_response tool if no structured output is required\n            self.toolkit.remove_tool_function(self.finish_function_name)\n\n        # -------------- The reasoning-acting loop --------------\n        # Cache the structured output generated in the finish function call\n        structured_output = None\n        reply_msg = None\n        for _ in range(self.max_iters):\n            # -------------- Memory compression --------------\n            await self._compress_memory_if_needed()\n\n            # -------------- The reasoning process --------------\n            msg_reasoning = await self._reasoning(tool_choice)\n\n            # -------------- The acting process --------------\n            futures = [\n                self._acting(tool_call)\n                for tool_call in msg_reasoning.get_content_blocks(\n                    \"tool_use\",\n                )\n            ]\n            # Parallel tool calls or not\n            if self.parallel_tool_calls:\n                structured_outputs = await asyncio.gather(*futures)\n            else:\n                # Sequential tool calls\n                structured_outputs = [await _ for _ in futures]\n\n            # -------------- Check for exit condition --------------\n            # If structured output is still not satisfied\n            if self._required_structured_model:\n                # Remove None results\n                structured_outputs = [_ for _ in structured_outputs if _]\n\n                msg_hint = None\n                # If the acting step generates structured outputs\n                if structured_outputs:\n                    # Cache the structured output data\n                    structured_output = structured_outputs[-1]\n\n                    # Prepare textual response\n                    if msg_reasoning.has_content_blocks(\"text\"):\n                        # Re-use the existing text response if any to avoid\n                        # duplicate text generation\n                        reply_msg = Msg(\n                            self.name,\n                            msg_reasoning.get_content_blocks(\"text\"),\n                            \"assistant\",\n                            metadata=structured_output,\n                        )\n                        break\n\n                    # Generate a textual response in the next iteration\n                    msg_hint = Msg(\n                        \"user\",\n                        \"<system-hint>Now generate a text \"\n                        \"response based on your current situation\"\n                        \"</system-hint>\",\n                        \"user\",\n                    )\n                    await self.memory.add(\n                        msg_hint,\n                        marks=_MemoryMark.HINT,\n                    )\n\n                    # Just generate text response in the next reasoning step\n                    tool_choice = \"none\"\n                    # The structured output is generated successfully\n                    self._required_structured_model = None\n\n                elif not msg_reasoning.has_content_blocks(\"tool_use\"):\n                    # If structured output is required but no tool call is\n                    # made, remind the llm to go on the task\n                    msg_hint = Msg(\n                        \"user\",\n                        \"<system-hint>Structured output is \"\n                        f\"required, go on to finish your task or call \"\n                        f\"'{self.finish_function_name}' to generate the \"\n                        f\"required structured output.</system-hint>\",\n                        \"user\",\n                    )\n                    await self.memory.add(msg_hint, marks=_MemoryMark.HINT)\n                    # Require tool call in the next reasoning step\n                    tool_choice = \"required\"\n\n                if msg_hint and self.print_hint_msg:\n                    await self.print(msg_hint)\n\n            elif not msg_reasoning.has_content_blocks(\"tool_use\"):\n                # Exit the loop when no structured output is required (or\n                # already satisfied) and only text response is generated\n                msg_reasoning.metadata = structured_output\n                reply_msg = msg_reasoning\n                break\n\n        # When the maximum iterations are reached\n        # and no reply message is generated\n        if reply_msg is None:\n            reply_msg = await self._summarizing()\n            reply_msg.metadata = structured_output\n            await self.memory.add(reply_msg)\n\n        # Post-process the memory, long-term memory\n        if self._static_control:\n            await self.long_term_memory.record(\n                [\n                    *await self.memory.get_memory(\n                        exclude_mark=_MemoryMark.COMPRESSED,\n                    ),\n                ],\n            )\n\n        return reply_msg\n\n    # pylint: disable=too-many-branches\n    async def _reasoning(\n        self,\n        tool_choice: Literal[\"auto\", \"none\", \"required\"] | None = None,\n    ) -> Msg:\n        \"\"\"Perform the reasoning process.\"\"\"\n\n        if self.plan_notebook:\n            # Insert the reasoning hint from the plan notebook\n            hint_msg = await self.plan_notebook.get_current_hint()\n            if self.print_hint_msg and hint_msg:\n                await self.print(hint_msg)\n            await self.memory.add(hint_msg, marks=_MemoryMark.HINT)\n\n        # Convert Msg objects into the required format of the model API\n        prompt = await self.formatter.format(\n            msgs=[\n                Msg(\"system\", self.sys_prompt, \"system\"),\n                *await self.memory.get_memory(\n                    exclude_mark=_MemoryMark.COMPRESSED\n                    if self.compression_config\n                    and self.compression_config.enable\n                    else None,\n                ),\n            ],\n        )\n        # Clear the hint messages after use\n        await self.memory.delete_by_mark(mark=_MemoryMark.HINT)\n\n        res = await self.model(\n            prompt,\n            tools=self.toolkit.get_json_schemas(),\n            tool_choice=tool_choice,\n        )\n\n        # handle output from the model\n        interrupted_by_user = False\n        msg = None\n\n        # TTS model context manager\n        tts_context = self.tts_model or _AsyncNullContext()\n        speech: AudioBlock | list[AudioBlock] | None = None\n\n        try:\n            async with tts_context:\n                msg = Msg(name=self.name, content=[], role=\"assistant\")\n                if self.model.stream:\n                    async for content_chunk in res:\n                        msg.content = content_chunk.content\n\n                        # The speech generated from multimodal (audio) models\n                        # e.g. Qwen-Omni and GPT-AUDIO\n                        speech = msg.get_content_blocks(\"audio\") or None\n\n                        # Push to TTS model if available\n                        if (\n                            self.tts_model\n                            and self.tts_model.supports_streaming_input\n                        ):\n                            tts_res = await self.tts_model.push(msg)\n                            speech = tts_res.content\n\n                        await self.print(msg, False, speech=speech)\n\n                else:\n                    msg.content = list(res.content)\n\n                if self.tts_model:\n                    # Push to TTS model and block to receive the full speech\n                    # synthesis result\n                    tts_res = await self.tts_model.synthesize(msg)\n                    if self.tts_model.stream:\n                        async for tts_chunk in tts_res:\n                            speech = tts_chunk.content\n                            await self.print(msg, False, speech=speech)\n                    else:\n                        speech = tts_res.content\n\n                await self.print(msg, True, speech=speech)\n\n                # Add a tiny sleep to yield the last message object in the\n                # message queue\n                await asyncio.sleep(0.001)\n\n        except asyncio.CancelledError as e:\n            interrupted_by_user = True\n            raise e from None\n\n        finally:\n            # None will be ignored by the memory\n            await self.memory.add(msg)\n\n            # Post-process for user interruption\n            if interrupted_by_user and msg:\n                # Fake tool results\n                tool_use_blocks: list = msg.get_content_blocks(\n                    \"tool_use\",\n                )\n                for tool_call in tool_use_blocks:\n                    msg_res = Msg(\n                        \"system\",\n                        [\n                            ToolResultBlock(\n                                type=\"tool_result\",\n                                id=tool_call[\"id\"],\n                                name=tool_call[\"name\"],\n                                output=\"The tool call has been interrupted \"\n                                \"by the user.\",\n                            ),\n                        ],\n                        \"system\",\n                    )\n                    await self.memory.add(msg_res)\n                    await self.print(msg_res, True)\n        return msg\n\n    async def _acting(self, tool_call: ToolUseBlock) -> dict | None:\n        \"\"\"Perform the acting process, and return the structured output if\n        it's generated and verified in the finish function call.\n\n        Args:\n            tool_call (`ToolUseBlock`):\n                The tool use block to be executed.\n\n        Returns:\n            `Union[dict, None]`:\n                Return the structured output if it's verified in the finish\n                function call, otherwise return None.\n        \"\"\"\n\n        tool_res_msg = Msg(\n            \"system\",\n            [\n                ToolResultBlock(\n                    type=\"tool_result\",\n                    id=tool_call[\"id\"],\n                    name=tool_call[\"name\"],\n                    output=[],\n                ),\n            ],\n            \"system\",\n        )\n        try:\n            # Execute the tool call\n            tool_res = await self.toolkit.call_tool_function(tool_call)\n\n            # Async generator handling\n            async for chunk in tool_res:\n                # Turn into a tool result block\n                tool_res_msg.content[0][  # type: ignore[index]\n                    \"output\"\n                ] = chunk.content\n\n                await self.print(tool_res_msg, chunk.is_last)\n\n                # Raise the CancelledError to handle the interruption in the\n                # handle_interrupt function\n                if chunk.is_interrupted:\n                    raise asyncio.CancelledError()\n\n                # Return message if generate_response is called successfully\n                if (\n                    tool_call[\"name\"] == self.finish_function_name\n                    and chunk.metadata\n                    and chunk.metadata.get(\"success\", False)\n                ):\n                    # Only return the structured output\n                    return chunk.metadata.get(\"structured_output\")\n\n            return None\n\n        finally:\n            # Record the tool result message in the memory\n            await self.memory.add(tool_res_msg)\n\n    async def observe(self, msg: Msg | list[Msg] | None) -> None:\n        \"\"\"Receive observing message(s) without generating a reply.\n\n        Args:\n            msg (`Msg | list[Msg] | None`):\n                The message or messages to be observed.\n        \"\"\"\n        await self.memory.add(msg)\n\n    async def _summarizing(self) -> Msg:\n        \"\"\"Generate a response when the agent fails to solve the problem in\n        the maximum iterations.\"\"\"\n\n        hint_msg = Msg(\n            \"user\",\n            \"You have failed to generate response within the maximum \"\n            \"iterations. Now respond directly by summarizing the current \"\n            \"situation.\",\n            role=\"user\",\n        )\n\n        # Generate a reply by summarizing the current situation\n        prompt = await self.formatter.format(\n            [\n                Msg(\"system\", self.sys_prompt, \"system\"),\n                *await self.memory.get_memory(\n                    exclude_mark=_MemoryMark.COMPRESSED\n                    if self.compression_config\n                    and self.compression_config.enable\n                    else None,\n                ),\n                hint_msg,\n            ],\n        )\n        # TODO: handle the structured output here, maybe force calling the\n        #  finish_function here\n        res = await self.model(prompt)\n\n        # TTS model context manager\n        tts_context = self.tts_model or _AsyncNullContext()\n        speech: AudioBlock | list[AudioBlock] | None = None\n\n        async with tts_context:\n            res_msg = Msg(self.name, [], \"assistant\")\n            if isinstance(res, AsyncGenerator):\n                async for chunk in res:\n                    res_msg.content = chunk.content\n\n                    # The speech generated from multimodal (audio) models\n                    # e.g. Qwen-Omni and GPT-AUDIO\n                    speech = res_msg.get_content_blocks(\"audio\") or None\n\n                    # Push to TTS model if available\n                    if (\n                        self.tts_model\n                        and self.tts_model.supports_streaming_input\n                    ):\n                        tts_res = await self.tts_model.push(res_msg)\n                        speech = tts_res.content\n\n                    await self.print(res_msg, False, speech=speech)\n\n            else:\n                res_msg.content = res.content\n\n            if self.tts_model:\n                # Push to TTS model and block to receive the full speech\n                # synthesis result\n                tts_res = await self.tts_model.synthesize(res_msg)\n                if self.tts_model.stream:\n                    async for tts_chunk in tts_res:\n                        speech = tts_chunk.content\n                        await self.print(res_msg, False, speech=speech)\n                else:\n                    speech = tts_res.content\n\n            await self.print(res_msg, True, speech=speech)\n\n            return res_msg\n\n    # pylint: disable=unused-argument\n    async def handle_interrupt(\n        self,\n        msg: Msg | list[Msg] | None = None,\n        structured_model: Type[BaseModel] | None = None,\n    ) -> Msg:\n        \"\"\"The post-processing logic when the reply is interrupted by the\n        user or something else.\n\n        Args:\n            msg (`Msg | list[Msg] | None`, optional):\n                The input message(s) to the agent.\n            structured_model (`Type[BaseModel] | None`, optional):\n                The required structured output model.\n        \"\"\"\n\n        response_msg = Msg(\n            self.name,\n            \"I noticed that you have interrupted me. What can I \"\n            \"do for you?\",\n            \"assistant\",\n            metadata={\n                # Expose this field to indicate the interruption\n                \"_is_interrupted\": True,\n            },\n        )\n\n        await self.print(response_msg, True)\n        await self.memory.add(response_msg)\n        return response_msg\n\n    def generate_response(\n        self,\n        **kwargs: Any,\n    ) -> ToolResponse:\n        \"\"\"\n        Generate required structured output by this function and return it\n        \"\"\"\n\n        structured_output = None\n        # Prepare structured output\n        if self._required_structured_model:\n            try:\n                # Use the metadata field of the message to store the\n                # structured output\n                structured_output = (\n                    self._required_structured_model.model_validate(\n                        kwargs,\n                    ).model_dump()\n                )\n\n            except ValidationError as e:\n                return ToolResponse(\n                    content=[\n                        TextBlock(\n                            type=\"text\",\n                            text=f\"Arguments Validation Error: {e}\",\n                        ),\n                    ],\n                    metadata={\n                        \"success\": False,\n                        \"structured_output\": {},\n                    },\n                )\n        else:\n            logger.warning(\n                \"The generate_response function is called when no structured \"\n                \"output model is required.\",\n            )\n\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=\"Successfully generated response.\",\n                ),\n            ],\n            metadata={\n                \"success\": True,\n                \"structured_output\": structured_output,\n            },\n            is_last=True,\n        )\n\n    async def _retrieve_from_long_term_memory(\n        self,\n        msg: Msg | list[Msg] | None,\n    ) -> None:\n        \"\"\"Insert the retrieved information from the long-term memory into\n        the short-term memory as a Msg object.\n\n        Args:\n            msg (`Msg | list[Msg] | None`):\n                The input message to the agent.\n        \"\"\"\n        if self._static_control and msg:\n            # Retrieve information from the long-term memory if available\n            retrieved_info = await self.long_term_memory.retrieve(msg)\n            if retrieved_info:\n                retrieved_msg = Msg(\n                    name=\"long_term_memory\",\n                    content=\"<long_term_memory>The content below are \"\n                    \"retrieved from long-term memory, which maybe \"\n                    f\"useful:\\n{retrieved_info}</long_term_memory>\",\n                    role=\"user\",\n                )\n                if self.print_hint_msg:\n                    await self.print(retrieved_msg, True)\n                await self.memory.add(retrieved_msg)\n\n    async def _retrieve_from_knowledge(\n        self,\n        msg: Msg | list[Msg] | None,\n    ) -> None:\n        \"\"\"Insert the retrieved documents from the RAG knowledge base(s) if\n        available.\n\n        Args:\n            msg (`Msg | list[Msg] | None`):\n                The input message to the agent.\n        \"\"\"\n        if self.knowledge and msg:\n            # Prepare the user input query\n            query = None\n            if isinstance(msg, Msg):\n                query = msg.get_text_content()\n            elif isinstance(msg, list):\n                texts = []\n                for m in msg:\n                    text = m.get_text_content()\n                    if text:\n                        texts.append(text)\n                query = \"\\n\".join(texts)\n\n            # Skip if the query is empty\n            if not query:\n                return\n\n            # Rewrite the query by the LLM if enabled\n            if self.enable_rewrite_query:\n                stream_tmp = self.model.stream\n                try:\n                    rewrite_prompt = await self.formatter.format(\n                        msgs=[\n                            Msg(\"system\", self.sys_prompt, \"system\"),\n                            *await self.memory.get_memory(\n                                exclude_mark=_MemoryMark.COMPRESSED\n                                if self.compression_config\n                                and self.compression_config.enable\n                                else None,\n                            ),\n                            Msg(\n                                \"user\",\n                                \"<system-hint>Now you need to rewrite \"\n                                \"the above user query to be more specific and \"\n                                \"concise for knowledge retrieval. For \"\n                                \"example, rewrite the query 'what happened \"\n                                \"last day' to 'what happened on 2023-10-01' \"\n                                \"(assuming today is 2023-10-02).\"\n                                \"</system-hint>\",\n                                \"user\",\n                            ),\n                        ],\n                    )\n                    self.model.stream = False\n                    res = await self.model(\n                        rewrite_prompt,\n                        structured_model=_QueryRewriteModel,\n                    )\n                    if res.metadata and res.metadata.get(\"rewritten_query\"):\n                        query = res.metadata[\"rewritten_query\"]\n\n                except Exception as e:\n                    logger.warning(\n                        \"Skipping the query rewriting due to error: %s\",\n                        str(e),\n                    )\n                finally:\n                    self.model.stream = stream_tmp\n\n            docs: list[Document] = []\n            for kb in self.knowledge:\n                # retrieve the user input query\n                docs.extend(\n                    await kb.retrieve(query=query),\n                )\n            if docs:\n                # Rerank by the relevance score\n                docs = sorted(\n                    docs,\n                    key=lambda doc: doc.score or 0.0,\n                    reverse=True,\n                )\n                # Prepare the retrieved knowledge string\n                retrieved_msg = Msg(\n                    name=\"user\",\n                    content=[\n                        TextBlock(\n                            type=\"text\",\n                            text=(\n                                \"<retrieved_knowledge>Use the following \"\n                                \"content from the knowledge base(s) if it's \"\n                                \"helpful:\\n\"\n                            ),\n                        ),\n                        *[_.metadata.content for _ in docs],\n                        TextBlock(\n                            type=\"text\",\n                            text=\"</retrieved_knowledge>\",\n                        ),\n                    ],\n                    role=\"user\",\n                )\n                if self.print_hint_msg:\n                    await self.print(retrieved_msg, True)\n                await self.memory.add(retrieved_msg)\n\n    async def _compress_memory_if_needed(self) -> None:\n        \"\"\"Compress the memory content if needed.\"\"\"\n        if (\n            self.compression_config is None\n            or not self.compression_config.enable\n        ):\n            return\n\n        # Obtain the messages that have not been compressed yet\n        to_compressed_msgs = await self.memory.get_memory(\n            exclude_mark=_MemoryMark.COMPRESSED,\n        )\n\n        # keep the recent n messages uncompressed, note messages with tool\n        #  use and result pairs should be kept together\n        n_keep = 0\n        accumulated_tool_call_ids = set()\n        for i in range(len(to_compressed_msgs) - 1, -1, -1):\n            msg = to_compressed_msgs[i]\n            for block in msg.get_content_blocks(\"tool_result\"):\n                accumulated_tool_call_ids.add(block[\"id\"])\n\n            for block in msg.get_content_blocks(\"tool_use\"):\n                if block[\"id\"] in accumulated_tool_call_ids:\n                    accumulated_tool_call_ids.remove(block[\"id\"])\n\n            # Handle the tool use/result pairs\n            if len(accumulated_tool_call_ids) == 0:\n                n_keep += 1\n\n            # Break if reach the number of messages to keep\n            if n_keep >= self.compression_config.keep_recent:\n                # Remove the messages that should be kept uncompressed\n                to_compressed_msgs = to_compressed_msgs[:i]\n                break\n\n        # Skip compression if no messages to compress\n        if not to_compressed_msgs:\n            return\n\n        # Calculate the token\n        prompt = await self.formatter.format(\n            [\n                Msg(\"system\", self.sys_prompt, \"system\"),\n                *to_compressed_msgs,\n            ],\n        )\n        n_tokens = await self.compression_config.agent_token_counter.count(\n            prompt,\n        )\n\n        if n_tokens > self.compression_config.trigger_threshold:\n            logger.info(\n                \"Memory compression is triggered (%d > \"\n                \"threshold %d) for agent %s.\",\n                n_tokens,\n                self.compression_config.trigger_threshold,\n                self.name,\n            )\n\n            # The formatter used for compression\n            compression_formatter = (\n                self.compression_config.compression_formatter or self.formatter\n            )\n\n            # Prepare the prompt used to compress the memories\n            compression_prompt = await compression_formatter.format(\n                [\n                    Msg(\"system\", self.sys_prompt, \"system\"),\n                    *to_compressed_msgs,\n                    Msg(\n                        \"user\",\n                        self.compression_config.compression_prompt,\n                        \"user\",\n                    ),\n                ],\n            )\n\n            # TODO: What if the compressed messages include multimodal blocks?\n            # Use the specified compression model if provided\n            compression_model = (\n                self.compression_config.compression_model or self.model\n            )\n            res = await compression_model(\n                compression_prompt,\n                structured_model=(self.compression_config.summary_schema),\n            )\n\n            # Obtain the structured output from the model response\n            last_chunk = None\n            if compression_model.stream:\n                async for chunk in res:\n                    last_chunk = chunk\n            else:\n                last_chunk = res\n\n            # Format the compressed memory summary\n            if last_chunk.metadata:\n                # Update the compressed summary in the memory storage\n                await self.memory.update_compressed_summary(\n                    self.compression_config.summary_template.format(\n                        **last_chunk.metadata,\n                    ),\n                )\n\n                # Mark the compressed messages in the memory storage\n                await self.memory.update_messages_mark(\n                    msg_ids=[_.id for _ in to_compressed_msgs],\n                    new_mark=_MemoryMark.COMPRESSED,\n                )\n\n                logger.info(\n                    \"Finished compressing %d messages in agent %s.\",\n                    len(to_compressed_msgs),\n                    self.name,\n                )\n\n            else:\n                logger.warning(\n                    \"Failed to obtain compression summary from the model \"\n                    \"structured output in agent %s.\",\n                    self.name,\n                )\n"
  },
  {
    "path": "src/agentscope/agent/_react_agent_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The base class for ReAct agent in agentscope.\"\"\"\nfrom abc import abstractmethod\nfrom collections import OrderedDict\nfrom typing import Callable, Any\n\nfrom ._agent_base import AgentBase\nfrom ._agent_meta import _ReActAgentMeta\nfrom ..message import Msg\n\n\nclass ReActAgentBase(AgentBase, metaclass=_ReActAgentMeta):\n    \"\"\"The ReAct agent base class.\n\n    To support ReAct algorithm, this class extends the AgentBase class by\n    adding two abstract interfaces: reasoning and acting, while supporting\n    hook functions at four positions: pre-reasoning, post-reasoning,\n    pre-acting, and post-acting by the `_ReActAgentMeta` metaclass.\n    \"\"\"\n\n    supported_hook_types: list[str] = [\n        \"pre_reply\",\n        \"post_reply\",\n        \"pre_print\",\n        \"post_print\",\n        \"pre_observe\",\n        \"post_observe\",\n        \"pre_reasoning\",\n        \"post_reasoning\",\n        \"pre_acting\",\n        \"post_acting\",\n    ]\n    \"\"\"Supported hook types for the agent base class.\"\"\"\n\n    _class_pre_reasoning_hooks: dict[\n        str,\n        Callable[\n            [\n                \"ReActAgentBase\",  # self\n                dict[str, Any],  # kwargs\n            ],\n            dict[str, Any] | None,  # The modified kwargs or None\n        ],\n    ] = OrderedDict()\n    \"\"\"The class-level pre-reasoning hooks, taking `self` object, the input\n    arguments as input\"\"\"\n\n    _class_post_reasoning_hooks: dict[\n        str,\n        Callable[\n            [\n                \"ReActAgentBase\",  # self\n                dict[str, Any],  # kwargs\n                Any,  # output\n            ],\n            Msg | None,  # the modified output message or None\n        ],\n    ] = OrderedDict()\n    \"\"\"The class-level post-reasoning hooks, taking `self` object, the input\n    arguments and the output message as input, and return the modified output\n    message or None if no modification is needed.\"\"\"\n\n    _class_pre_acting_hooks: dict[\n        str,\n        Callable[\n            [\n                \"ReActAgentBase\",  # self\n                dict[str, Any],  # kwargs\n            ],\n            dict[str, Any] | None,  # The modified kwargs or None\n        ],\n    ] = OrderedDict()\n    \"\"\"The class-level pre-acting hooks, taking `self` object, the input\n    arguments as input, and return the modified input arguments or None if no\n    modification is needed.\"\"\"\n\n    _class_post_acting_hooks: dict[\n        str,\n        Callable[\n            [\n                \"ReActAgentBase\",  # self\n                dict[str, Any],  # kwargs\n                Any,  # output\n            ],\n            Msg | None,  # the modified output message or None\n        ],\n    ] = OrderedDict()\n    \"\"\"The class-level post-acting hooks, taking `self` object, the input\n    arguments and the output message as input, and return the modified output\n    message or None if no modification is needed.\"\"\"\n\n    def __init__(\n        self,\n    ) -> None:\n        \"\"\"Initialize the ReAct agent base class.\"\"\"\n        super().__init__()\n\n        # Init reasoning and acting hooks\n        self._instance_pre_reasoning_hooks = OrderedDict()\n        self._instance_post_reasoning_hooks = OrderedDict()\n        self._instance_pre_acting_hooks = OrderedDict()\n        self._instance_post_acting_hooks = OrderedDict()\n\n    @abstractmethod\n    async def _reasoning(\n        self,\n        *args: Any,\n        **kwargs: Any,\n    ) -> Any:\n        \"\"\"The reasoning process of the ReAct agent, which will be wrapped\n        with pre- and post-hooks.\"\"\"\n\n    @abstractmethod\n    async def _acting(self, *args: Any, **kwargs: Any) -> Any:\n        \"\"\"The acting process of the ReAct agent, which will be wrapped with\n        pre- and post-hooks.\"\"\"\n"
  },
  {
    "path": "src/agentscope/agent/_realtime_agent.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The realtime agent class.\"\"\"\nimport asyncio\nfrom asyncio import Queue\n\nimport shortuuid\n\nfrom .._logging import logger\nfrom .._utils._common import _resample_pcm_delta\nfrom ..message import (\n    AudioBlock,\n    Base64Source,\n    TextBlock,\n    ImageBlock,\n    ToolUseBlock,\n    ToolResultBlock,\n)\nfrom ..module import StateModule\nfrom ..realtime import (\n    ModelEvents,\n    RealtimeModelBase,\n    ServerEvents,\n    ClientEvents,\n)\nfrom ..tool import Toolkit\n\n\nclass RealtimeAgent(StateModule):\n    \"\"\"The realtime agent class. Different from the `AgentBase` class,\n    this class is designed for real-time interaction scenarios, such as\n    realtime chat, voice assistants, etc.\n\n    Example:\n        This realtime agent requires a queue to handle outgoing messages to\n        the frontend and other agents, and its lifecycle is managed by the\n        `start` and `stop` methods.\n\n        .. code-block:: python\n            :caption: An example of using the RealtimeAgent class.\n\n            from agentscope.agent import RealtimeAgent\n            from agentscope.realtime import DashScopeRealtimeModel\n            import asyncio\n\n            agent = RealtimeAgent(\n                name=\"Friday\",\n                sys_prompt=\"You are a helpful assistant.\",\n                model=DashScopeRealtimeModel(\n                    model_name=\"qwen3-omni-flash-realtime\",\n                    api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n                )\n            )\n\n            queue = asyncio.Queue()\n            await agent.start(queue)\n\n            # handle the outgoing messages from the agent in another asyncio\n            # task\n            ...\n\n            await agent.stop()\n\n    \"\"\"\n\n    def __init__(\n        self,\n        name: str,\n        sys_prompt: str,\n        model: RealtimeModelBase,\n        toolkit: Toolkit | None = None,\n    ) -> None:\n        \"\"\"Initialize the RealtimeAgent class.\n\n        Args:\n            name (`str`):\n                The name of the agent.\n            sys_prompt (`str`):\n                The system prompt of the agent.\n            model (`RealtimeModelBase`):\n                The realtime model used by the agent.\n            toolkit (`Toolkit | None`, optional):\n                A `Toolkit` object that contains the tool functions. If not\n                provided, a default empty `Toolkit` will be created.\n        \"\"\"\n        super().__init__()\n\n        self.id = shortuuid.uuid()\n        self.name = name\n        self.sys_prompt = sys_prompt\n        self.model = model\n        self.toolkit = toolkit\n\n        # A queue to handle the incoming events from other agents or the\n        # frontend.\n        self._incoming_queue = Queue()\n        self._external_event_handling_task = None\n\n        # The queue to gather model responses.\n        self._model_response_queue = Queue()\n        self._model_response_handling_task = None\n\n    async def start(self, outgoing_queue: Queue) -> None:\n        \"\"\"Establish a connection for real-time interaction.\n\n        Args:\n            outgoing_queue (`Queue`):\n                The queue to push messages to the frontend and other agents.\n        \"\"\"\n        # Start the realtime model connection.\n        await self.model.connect(\n            self._model_response_queue,\n            instructions=self.sys_prompt,\n            tools=self.toolkit.get_json_schemas() if self.toolkit else None,\n        )\n\n        # Start the forwarding loop.\n        self._external_event_handling_task = asyncio.create_task(\n            self._forward_loop(),\n        )\n\n        # Start the response handling loop.\n        self._model_response_handling_task = asyncio.create_task(\n            self._model_response_loop(outgoing_queue),\n        )\n\n    async def stop(self) -> None:\n        \"\"\"Close the connection.\"\"\"\n\n        if not self._external_event_handling_task.done():\n            self._external_event_handling_task.cancel()\n\n        await self.model.disconnect()\n\n    async def _forward_loop(self) -> None:\n        \"\"\"The loop to forward messages from other agents or the frontend to\n        the realtime model for processing.\n\n        outside ==> agent ==> realtime model\n        \"\"\"\n        logger.info(\n            \"Agent '%s' begins the loops to receive external events\",\n            self.name,\n        )\n\n        while True:\n            event = await self._incoming_queue.get()\n\n            match event:\n                # Only handle the events that we need\n                case ServerEvents.AgentResponseAudioDeltaEvent() as event:\n                    # Convert the sample rate to the required format by the\n                    # model\n                    receive_rate = event.format.rate\n                    if self.model.input_sample_rate != receive_rate:\n                        delta = _resample_pcm_delta(\n                            event.delta,\n                            receive_rate,\n                            self.model.input_sample_rate,\n                        )\n\n                    else:\n                        delta = event.delta\n\n                    await self.model.send(\n                        AudioBlock(\n                            type=\"audio\",\n                            source=Base64Source(\n                                type=\"base64\",\n                                media_type=event.format.type,\n                                data=delta,\n                            ),\n                        ),\n                    )\n\n                case ServerEvents.AgentResponseAudioDoneEvent():\n                    # Send a silence audio block to indicate the end of audio\n                    pass\n\n                case ClientEvents.ClientAudioAppendEvent() as event:\n                    # Construct media_type from format info\n                    # format contains: {\"sample_rate\": 16000, \"encoding\":\n                    # \"pcm16\"}\n                    # encoding = event.format.get(\"encoding\", \"pcm16\")\n                    # media_type = (\n                    #     f\"audio/{encoding.replace('16', '')}\"\n                    #     if \"pcm\" in encoding\n                    #     else \"audio/pcm\"\n                    # )\n\n                    await self.model.send(\n                        AudioBlock(\n                            type=\"audio\",\n                            source=Base64Source(\n                                type=\"base64\",\n                                media_type=event.format.type,\n                                data=event.audio,\n                            ),\n                        ),\n                    )\n\n                case ClientEvents.ClientTextAppendEvent() as event:\n                    await self.model.send(\n                        TextBlock(\n                            type=\"text\",\n                            text=event.text,\n                        ),\n                    )\n                case ClientEvents.ClientImageAppendEvent() as event:\n                    # Construct media_type from format info\n                    media_type = event.format.get(\"type\", \"image/jpeg\")\n\n                    await self.model.send(\n                        ImageBlock(\n                            type=\"image\",\n                            source=Base64Source(\n                                type=\"base64\",\n                                media_type=media_type,\n                                data=event.image,\n                            ),\n                        ),\n                    )\n\n    async def _model_response_loop(self, outgoing_queue: Queue) -> None:\n        \"\"\"The loop to handle model responses and forward them to the\n        frontend and other agents.\n\n        realtime model ==> agent ==> outside\n\n        Args:\n            outgoing_queue (`Queue`):\n                The queue to push messages to the frontend and other agents.\n        \"\"\"\n        while True:\n            model_event = await self._model_response_queue.get()\n\n            agent_kwargs = {\"agent_id\": self.id, \"agent_name\": self.name}\n\n            agent_event = None\n            match model_event:\n                # The events that can be converted from model events to agent\n                #  events directly\n                case (\n                    ModelEvents.ModelResponseCreatedEvent()\n                    | ModelEvents.ModelResponseDoneEvent()\n                    | ModelEvents.ModelResponseAudioDeltaEvent()\n                    | ModelEvents.ModelResponseAudioDoneEvent()\n                    | ModelEvents.ModelResponseAudioTranscriptDeltaEvent()\n                    | ModelEvents.ModelResponseAudioTranscriptDoneEvent()\n                    | ModelEvents.ModelResponseToolUseDeltaEvent()\n                    | ModelEvents.ModelInputTranscriptionDeltaEvent()\n                    | ModelEvents.ModelInputTranscriptionDoneEvent()\n                    | ModelEvents.ModelInputStartedEvent()\n                    | ModelEvents.ModelInputDoneEvent()\n                    | ModelEvents.ModelErrorEvent()\n                ) as event:\n                    # Directly map the model event to agent event\n                    agent_event = ServerEvents.from_model_event(\n                        event,\n                        **agent_kwargs,\n                    )\n\n                # The events that need special handling\n                case ModelEvents.ModelSessionCreatedEvent():\n                    # Send the agent ready event to the outside.\n                    agent_event = ServerEvents.AgentReadyEvent(**agent_kwargs)\n\n                case ModelEvents.ModelSessionEndedEvent():\n                    # Send the agent session ended event to the outside.\n                    agent_event = ServerEvents.AgentEndedEvent(**agent_kwargs)\n\n                # The tool use done that requires executing the tool\n                # Such event may generate multiple outgoing events:\n                # 1. Tool use done event\n                # 2. Tool result event\n                case ModelEvents.ModelResponseToolUseDoneEvent() as event:\n                    # Send the tool use done event immediately\n                    done_event = ServerEvents.AgentResponseToolUseDoneEvent(\n                        response_id=event.response_id,\n                        item_id=event.item_id,\n                        tool_use=event.tool_use,\n                        **agent_kwargs,\n                    )\n\n                    # Directly put the done event to the outgoing queue\n                    await outgoing_queue.put(done_event)\n\n                    # Then execute the tool call using accumulated arguments\n                    if self.toolkit:\n                        # Execute the tool call asynchronously\n                        asyncio.create_task(\n                            self._acting(\n                                tool_use=event.tool_use,\n                                outgoing_queue=outgoing_queue,\n                            ),\n                        )\n\n                case _:\n                    logger.debug(\n                        \"Unknown model event type: %s\",\n                        type(model_event),\n                    )\n\n            if agent_event is not None:\n                # Put the processed response to the outgoing queue.\n                await outgoing_queue.put(agent_event)\n\n    async def handle_input(\n        self,\n        event: ClientEvents.EventBase | ServerEvents.EventBase,\n    ) -> None:\n        \"\"\"Handle the input message from the frontend or the other agents.\n\n        Args:\n            event (`ClientEvents.EventBase | ServerEvents.EventBase`):\n                The input event from the frontend or other agents.\n        \"\"\"\n        await self._incoming_queue.put(event)\n\n    async def _acting(\n        self,\n        tool_use: ToolUseBlock,\n        outgoing_queue: Queue,\n    ) -> None:\n        \"\"\"Execute the tool call and send the result back to the outside (\n        frontend or other agents).\n\n        Args:\n            tool_use (`ToolUseBlock`):\n                The tool use block containing the tool call information.\n            outgoing_queue (`Queue`):\n                The queue to push messages to the frontend and other agents.\n        \"\"\"\n        if not self.toolkit:\n            return\n\n        res = await self.toolkit.call_tool_function(tool_use)\n\n        last_chunk = None\n        async for chunk in res:\n            last_chunk = chunk\n\n        if last_chunk:\n            tool_result_block = ToolResultBlock(\n                type=\"tool_result\",\n                id=tool_use.get(\"id\"),\n                name=tool_use.get(\"name\"),\n                output=last_chunk.content,\n            )\n\n            # Send the tool result back to the model\n            await self.model.send(tool_result_block)\n\n            # Also send event to frontend/other agents\n            outgoing_event = ServerEvents.AgentResponseToolResultEvent(\n                tool_result=tool_result_block,\n                agent_id=self.id,\n                agent_name=self.name,\n            )\n\n            await outgoing_queue.put(outgoing_event)\n"
  },
  {
    "path": "src/agentscope/agent/_user_agent.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The user agent class.\"\"\"\nfrom typing import Type, Any\n\nfrom pydantic import BaseModel\n\nfrom ._agent_base import AgentBase\nfrom ._user_input import UserInputBase, TerminalUserInput\nfrom ..message import Msg\n\n\nclass UserAgent(AgentBase):\n    \"\"\"The class for user interaction, allowing developers to handle the user\n    input from different sources, such as web UI, cli, and other interfaces.\n    \"\"\"\n\n    _input_method: UserInputBase = TerminalUserInput()\n    \"\"\"The user input method, can be overridden by calling the\n    `register_instance/class_input_method` function.\"\"\"\n\n    def __init__(\n        self,\n        name: str,\n    ) -> None:\n        \"\"\"Initialize the user agent with a name.\"\"\"\n        super().__init__()\n\n        self.name = name\n\n    async def reply(\n        self,\n        msg: Msg | list[Msg] | None = None,\n        structured_model: Type[BaseModel] | None = None,\n    ) -> Msg:\n        \"\"\"Receive input message(s) and generate a reply message from the user.\n\n        Args:\n            msg (`Msg | list[Msg] | None`, defaults to `None`):\n                The message(s) to be replied. If `None`, the agent will wait\n                for user input.\n            structured_model (`Type[BaseModel] | None`, defaults to `None`):\n                A child class of `pydantic.BaseModel` that defines the\n                structured output format. If provided, the user will be\n                prompted to fill in the required fields.\n\n        Returns:\n            `Msg`:\n                The reply message generated by the user.\n        \"\"\"\n\n        # Get the input from the specified input method.\n        input_data = await self._input_method(\n            agent_id=self.id,\n            agent_name=self.name,\n            structured_model=structured_model,\n        )\n\n        blocks_input = input_data.blocks_input\n        if (\n            blocks_input\n            and len(blocks_input) == 1\n            and blocks_input[0].get(\"type\") == \"text\"\n        ):\n            # Turn blocks_input into a string if only one text block exists\n            blocks_input = blocks_input[0].get(\"text\")\n\n        msg = Msg(\n            self.name,\n            content=blocks_input,\n            role=\"user\",\n            metadata=input_data.structured_input,\n        )\n\n        await self.print(msg)\n\n        return msg\n\n    def override_instance_input_method(\n        self,\n        input_method: UserInputBase,\n    ) -> None:\n        \"\"\"Override the input method of the current UserAgent instance.\n\n        Args:\n            input_method (`UserInputBase`):\n                The callable input method, which should be an object of a\n                class that inherits from `UserInputBase`.\n        \"\"\"\n        if not isinstance(input_method, UserInputBase):\n            raise ValueError(\n                f\"The input method should be an instance of the child class \"\n                f\"of `UserInputBase`, but got {type(input_method)} instead.\",\n            )\n        self._input_method = input_method\n\n    @classmethod\n    def override_class_input_method(\n        cls,\n        input_method: UserInputBase,\n    ) -> None:\n        \"\"\"Override the input method of the current UserAgent class.\n\n        Args:\n            input_method (`UserInputBase`):\n                The callable input method, which should be an object of a\n                class that inherits from `UserInputBase`.\n        \"\"\"\n        if not isinstance(input_method, UserInputBase):\n            raise ValueError(\n                f\"The input method should be an instance of the child class \"\n                f\"of `UserInputBase`, but got {type(input_method)} instead.\",\n            )\n        cls._input_method = input_method\n\n    async def handle_interrupt(\n        self,\n        *args: Any,\n        **kwargs: Any,\n    ) -> Msg:\n        \"\"\"The post-processing logic when the reply is interrupted by the\n        user or something else.\"\"\"\n        raise NotImplementedError(\n            f\"The handle_interrupt function is not implemented in \"\n            f\"{self.__class__.__name__}\",\n        )\n\n    async def observe(self, msg: Msg | list[Msg] | None) -> None:\n        \"\"\"Observe the message(s) from the other agents or the environment.\"\"\"\n"
  },
  {
    "path": "src/agentscope/agent/_user_input.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The user input related classes.\"\"\"\nimport json.decoder\nimport time\nfrom abc import abstractmethod\nfrom dataclasses import dataclass\nfrom queue import Queue\nfrom threading import Event\nfrom typing import Any, Type, List\n\nimport jsonschema\nimport requests\nimport shortuuid\nimport socketio\nfrom pydantic import BaseModel\nimport json5\n\nfrom .. import _config\nfrom .._logging import logger\nfrom ..message import (\n    TextBlock,\n    VideoBlock,\n    AudioBlock,\n    ImageBlock,\n)\n\n\n@dataclass\nclass UserInputData:\n    \"\"\"The user input data.\"\"\"\n\n    blocks_input: List[TextBlock | ImageBlock | AudioBlock | VideoBlock] = None\n    \"\"\"The text input from the user\"\"\"\n\n    structured_input: dict[str, Any] | None = None\n    \"\"\"The structured input from the user\"\"\"\n\n\nclass UserInputBase:\n    \"\"\"The base class used to handle the user input from different sources.\"\"\"\n\n    @abstractmethod\n    async def __call__(\n        self,\n        agent_id: str,\n        agent_name: str,\n        *args: Any,\n        structured_model: Type[BaseModel] | None = None,\n        **kwargs: Any,\n    ) -> UserInputData:\n        \"\"\"The user input method, which returns the user input and the\n        required structured data.\n\n        Args:\n            agent_id (`str`):\n                The agent identifier.\n            agent_name (`str`):\n                The agent name.\n            structured_model (`Type[BaseModel] | None`, optional):\n                A base model class that defines the structured input format.\n\n        Returns:\n            `UserInputData`:\n                The user input data.\n        \"\"\"\n\n\nclass TerminalUserInput(UserInputBase):\n    \"\"\"The terminal user input.\"\"\"\n\n    def __init__(self, input_hint: str = \"User Input: \") -> None:\n        \"\"\"Initialize the terminal user input with a hint.\"\"\"\n        self.input_hint = input_hint\n\n    async def __call__(\n        self,\n        agent_id: str,\n        agent_name: str,\n        *args: Any,\n        structured_model: Type[BaseModel] | None = None,\n        **kwargs: Any,\n    ) -> UserInputData:\n        \"\"\"Handle the user input from the terminal.\n\n        Args:\n            agent_id (`str`):\n                The agent identifier.\n            agent_name (`str`):\n                The agent name.\n            structured_model (`Type[BaseModel] | None`, optional):\n                A base model class that defines the structured input format.\n\n        Returns:\n            `UserInputData`:\n                The user input data.\n        \"\"\"\n\n        text_input = (\n            input(self.input_hint)\n            .encode(\"utf-8\", errors=\"ignore\")\n            .decode(\"utf-8\")\n        )\n\n        structured_input = None\n        if structured_model is not None:\n            structured_input = {}\n\n            json_schema = structured_model.model_json_schema()\n            required = json_schema.get(\"required\", [])\n            print(\"Structured input (press Enter to skip for optional):)\")\n\n            for key, item in json_schema.get(\"properties\").items():\n                requirements = {**item}\n                requirements.pop(\"title\")\n\n                while True:\n                    res = input(f\"\\t{key} ({requirements}): \")\n\n                    if res == \"\":\n                        if key in required:\n                            print(f\"Key {key} is required.\")\n                            continue\n\n                        res = item.get(\"default\", None)\n\n                    if item.get(\"type\").lower() == \"integer\":\n                        try:\n                            res = json5.loads(res)\n                        except json.decoder.JSONDecodeError as e:\n                            print(\n                                \"\\033[31mInvalid input with error:\\n\"\n                                \"```\\n\"\n                                f\"{e}\\n\"\n                                \"```\\033[0m\",\n                            )\n                            continue\n\n                    try:\n                        jsonschema.validate(res, item)\n                        structured_input[key] = res\n                        break\n                    except jsonschema.ValidationError as e:\n                        print(\n                            f\"\\033[31mValidation error:\\n```\\n{e}\\n```\\033[0m\",\n                        )\n                        time.sleep(0.5)\n\n        return UserInputData(\n            blocks_input=[TextBlock(type=\"text\", text=text_input)],\n            structured_input=structured_input,\n        )\n\n\nclass StudioUserInput(UserInputBase):\n    \"\"\"The class that host the user input on the AgentScope Studio.\"\"\"\n\n    _websocket_namespace: str = \"/python\"\n\n    def __init__(\n        self,\n        studio_url: str,\n        run_id: str,\n        max_retries: int = 3,\n        reconnect_attempts: int = 3,\n        reconnection_delay: int = 1,\n        reconnection_delay_max: int = 5,\n    ) -> None:\n        \"\"\"Initialize the StudioUserInput object.\n\n        Args:\n            studio_url (`str`):\n                The URL of the AgentScope Studio.\n            run_id (`str`):\n                The current run identity.\n            max_retries (`int`, defaults to `3`):\n                The maximum number of retries to get user input.\n        \"\"\"\n        self._is_connected = False\n        self._is_reconnecting = False\n\n        self.studio_url = studio_url\n        self.run_id = run_id\n        self.max_retries = max_retries\n\n        # Init Websocket\n        self.sio = socketio.Client(\n            reconnection=True,\n            reconnection_attempts=reconnect_attempts,\n            reconnection_delay=reconnection_delay,\n            reconnection_delay_max=reconnection_delay_max,\n        )\n        self.input_queues = {}\n        self.input_events = {}\n\n        @self.sio.on(\"connect\", namespace=self._websocket_namespace)\n        def on_connect() -> None:\n            self._is_connected = True\n            logger.info(\n                'Connected to AgentScope Studio at \"%s\" with '\n                'run name \"%s\".',\n                self.studio_url,\n                run_id,\n            )\n            logger.info(\n                \"View the run at: %s/projects/%s\",\n                self.studio_url,\n                _config.project,\n            )\n\n        @self.sio.on(\"disconnect\", namespace=self._websocket_namespace)\n        def on_disconnect() -> None:\n            self._is_connected = False\n            logger.info(\n                \"Disconnected from AgentScope Studio at %s\",\n                self.studio_url,\n            )\n\n        @self.sio.on(\"reconnect\", namespace=self._websocket_namespace)\n        def on_reconnect(attempt_number: int) -> None:\n            self._is_connected = True\n            self._is_reconnecting = False\n            logger.info(\n                \"Reconnected to AgentScope Studio at %s with run_id %s after \"\n                \"%d attempts\",\n                self.studio_url,\n                self.run_id,\n                attempt_number,\n            )\n\n        @self.sio.on(\"reconnect_attempt\", namespace=self._websocket_namespace)\n        def on_reconnect_attempt(attempt_number: int) -> None:\n            self._is_reconnecting = True\n            logger.info(\n                \"Attempting to reconnect to AgentScope Studio at %s \"\n                \"(attempt %d)\",\n                self.studio_url,\n                attempt_number,\n            )\n\n        @self.sio.on(\"reconnect_failed\", namespace=self._websocket_namespace)\n        def on_reconnect_failed() -> None:\n            self._is_reconnecting = False\n            logger.error(\n                \"Failed to reconnect to AgentScope Studio at %s\",\n                self.studio_url,\n            )\n\n        @self.sio.on(\"reconnect_error\", namespace=self._websocket_namespace)\n        def on_reconnect_error(error: Any) -> None:\n            logger.error(\n                \"Error while reconnecting to AgentScope Studio at %s: %s\",\n                self.studio_url,\n                str(error),\n            )\n\n        # The AgentScope Studio backend send the \"sendUserInput\" event to\n        # the current python run\n        @self.sio.on(\"forwardUserInput\", namespace=self._websocket_namespace)\n        def receive_user_input(\n            request_id: str,\n            blocks_input: List[\n                TextBlock | ImageBlock | AudioBlock | VideoBlock\n            ],\n            structured_input: dict[str, Any],\n        ) -> None:\n            if request_id in self.input_queues:\n                self.input_queues[request_id].put(\n                    UserInputData(\n                        blocks_input=blocks_input,\n                        structured_input=structured_input,\n                    ),\n                )\n                self.input_events[request_id].set()\n\n        try:\n            self.sio.connect(\n                f\"{self.studio_url}\",\n                namespaces=[\"/python\"],\n                auth={\"run_id\": self.run_id},\n            )\n        except Exception as e:\n            raise RuntimeError(\n                f\"Failed to connect to AgentScope Studio at {self.studio_url}\",\n            ) from e\n\n    def _ensure_connected(\n        self,\n        timeout: float = 30.0,\n        check_interval: float = 5.0,\n    ) -> None:\n        \"\"\"Ensure the connection is established or wait for reconnection.\n\n        Args:\n            timeout (`float`):\n                Maximum time to wait for reconnection in seconds. Defaults\n                to 30.0.\n            check_interval (`float`):\n                Interval between connection checks in seconds. Defaults to 1.0.\n\n        Raises:\n            `RuntimeError`:\n                If connection cannot be established within timeout.\n        \"\"\"\n        if self._is_connected:\n            return\n\n        if self._is_reconnecting:\n            start_time = time.time()\n            while self._is_reconnecting:\n                # Check timeout\n                elapsed_time = time.time() - start_time\n                if elapsed_time > timeout:\n                    raise RuntimeError(\n                        f\"Reconnection timeout after {elapsed_time} seconds\",\n                    )\n\n                # Log status\n                logger.info(\n                    \"Waiting for reconnection... (%.1fs / %.1fs)\",\n                    elapsed_time,\n                    timeout,\n                )\n\n                # Wait for next check\n                time.sleep(check_interval)\n\n            # After reconnection attempt completed, check final status\n            if self._is_connected:\n                return\n\n        # Not connected and not reconnecting\n        raise RuntimeError(\n            f\"Not connected to AgentScope Studio at {self.studio_url}.\",\n        )\n\n    async def __call__(  # type: ignore[override]\n        self,\n        agent_id: str,\n        agent_name: str,\n        *args: Any,\n        structured_model: Type[BaseModel] | None = None,\n    ) -> UserInputData:\n        \"\"\"Get the user input from AgentScope Studio.\n\n        Args:\n            agent_id (`str`):\n                The identity of the agent.\n            agent_name (`str`):\n                The name of the agent.\n            structured_model (`Type[BaseModel] | None`, optional):\n                The base model class of the structured input.\n\n        Raises:\n            `RuntimeError`:\n                Failed to get user input from AgentScope Studio.\n\n        Returns:\n            `UserInputData`:\n                The user input.\n        \"\"\"\n        self._ensure_connected()\n\n        request_id = shortuuid.uuid()\n\n        self.input_queues[request_id] = Queue()\n        self.input_events[request_id] = Event()\n\n        if structured_model is None:\n            structured_input = None\n        else:\n            structured_input = structured_model.model_json_schema()\n\n        n_retry = 0\n        while True:\n            try:\n                response = requests.post(\n                    f\"{self.studio_url}/trpc/requestUserInput\",\n                    json={\n                        \"requestId\": request_id,\n                        \"runId\": self.run_id,\n                        \"agentId\": agent_id,\n                        \"agentName\": agent_name,\n                        \"structuredInput\": structured_input,\n                    },\n                )\n                response.raise_for_status()\n                break\n            except Exception as e:\n                if n_retry < self.max_retries:\n                    n_retry += 1\n                    continue\n\n                raise RuntimeError(\n                    \"Failed to get user input from AgentScope Studio\",\n                ) from e\n\n        try:\n            self.input_events[request_id].wait()\n            response_data = self.input_queues[request_id].get()\n            return response_data\n\n        finally:\n            self.input_queues.pop(request_id, None)\n            self.input_events.pop(request_id, None)\n\n    def __del__(self) -> None:\n        \"\"\"Cleanup socket connection when object it destroyed\"\"\"\n        try:\n            self.sio.disconnect()\n        except Exception as e:\n            logger.error(\n                \"Failed to disconnect from AgentScope Studio at %s: %s\",\n                self.studio_url,\n                str(e),\n            )\n"
  },
  {
    "path": "src/agentscope/agent/_utils.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Utils for agents in agentscope.\"\"\"\nfrom typing import Any\n\n\nclass _AsyncNullContext:\n    \"\"\"An async null context manager.\"\"\"\n\n    async def __aenter__(self) -> None:\n        return None\n\n    async def __aexit__(\n        self,\n        exc_type: Any,\n        exc_val: Any,\n        exc_tb: Any,\n    ) -> None:\n        return None\n"
  },
  {
    "path": "src/agentscope/embedding/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The embedding module in agentscope.\"\"\"\n\nfrom ._embedding_base import EmbeddingModelBase\nfrom ._embedding_usage import EmbeddingUsage\nfrom ._embedding_response import EmbeddingResponse\nfrom ._dashscope_embedding import DashScopeTextEmbedding\nfrom ._dashscope_multimodal_embedding import DashScopeMultiModalEmbedding\nfrom ._openai_embedding import OpenAITextEmbedding\nfrom ._gemini_embedding import GeminiTextEmbedding\nfrom ._ollama_embedding import OllamaTextEmbedding\nfrom ._cache_base import EmbeddingCacheBase\nfrom ._file_cache import FileEmbeddingCache\n\n\n__all__ = [\n    \"EmbeddingModelBase\",\n    \"EmbeddingUsage\",\n    \"EmbeddingResponse\",\n    \"DashScopeTextEmbedding\",\n    \"DashScopeMultiModalEmbedding\",\n    \"OpenAITextEmbedding\",\n    \"GeminiTextEmbedding\",\n    \"OllamaTextEmbedding\",\n    \"EmbeddingCacheBase\",\n    \"FileEmbeddingCache\",\n]\n"
  },
  {
    "path": "src/agentscope/embedding/_cache_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The embedding cache base class.\"\"\"\nfrom abc import abstractmethod\nfrom typing import List, Any\n\nfrom ..types import (\n    JSONSerializableObject,\n    Embedding,\n)\n\n\nclass EmbeddingCacheBase:\n    \"\"\"Base class for embedding caches, which is responsible for storing and\n    retrieving embeddings.\"\"\"\n\n    @abstractmethod\n    async def store(\n        self,\n        embeddings: List[Embedding],\n        identifier: JSONSerializableObject,\n        overwrite: bool = False,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Store the embeddings with the given identifier.\n\n        Args:\n            embeddings (`List[Embedding]`):\n                The embeddings to store.\n            identifier (`JSONSerializableObject`):\n                The identifier to distinguish the embeddings.\n            overwrite (`bool`, defaults to `False`):\n                Whether to overwrite existing embeddings with the same\n                identifier. If `True`, existing embeddings will be replaced.\n        \"\"\"\n\n    @abstractmethod\n    async def retrieve(\n        self,\n        identifier: JSONSerializableObject,\n    ) -> List[Embedding] | None:\n        \"\"\"Retrieve the embeddings with the given identifier. If not\n        found, return `None`.\n\n        Args:\n            identifier (`JSONSerializableObject`):\n                The identifier to retrieve the embeddings.\n        \"\"\"\n\n    @abstractmethod\n    async def remove(\n        self,\n        identifier: JSONSerializableObject,\n    ) -> None:\n        \"\"\"Remove the embeddings with the given identifier.\n\n        Args:\n            identifier (`JSONSerializableObject`):\n                The identifier to remove the embeddings.\n        \"\"\"\n\n    @abstractmethod\n    async def clear(self) -> None:\n        \"\"\"Clear all cached embeddings.\"\"\"\n"
  },
  {
    "path": "src/agentscope/embedding/_dashscope_embedding.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The dashscope embedding module in agentscope.\"\"\"\nfrom datetime import datetime\nfrom typing import Any, List, Literal\n\nfrom ._cache_base import EmbeddingCacheBase\nfrom ._embedding_response import EmbeddingResponse\nfrom ._embedding_usage import EmbeddingUsage\nfrom ._embedding_base import EmbeddingModelBase\nfrom .._logging import logger\nfrom ..message import TextBlock\n\n\nclass DashScopeTextEmbedding(EmbeddingModelBase):\n    \"\"\"DashScope text embedding API class.\n\n    .. note:: From the `official documentation\n    <https://bailian.console.aliyun.com/?tab=api#/api/?type=model&url=2712515>`_:\n\n     - The max batch size that DashScope text embedding API\n     supports is 10 for `text-embedding-v4` and `text-embedding-v3` models, and\n     25 for `text-embedding-v2` and `text-embedding-v1` models.\n     - The max token limit for a single input is 8192 tokens for `v4` and `v3`\n     models, and 2048 tokens for `v2` and `v1` models.\n\n    \"\"\"\n\n    supported_modalities: list[str] = [\"text\"]\n    \"\"\"This class only supports text input.\"\"\"\n\n    def __init__(\n        self,\n        api_key: str,\n        model_name: str,\n        dimensions: int = 1024,\n        embedding_cache: EmbeddingCacheBase | None = None,\n    ) -> None:\n        \"\"\"Initialize the DashScope text embedding model class.\n\n        Args:\n            api_key (`str`):\n                The dashscope API key.\n            model_name (`str`):\n                The name of the embedding model.\n            dimensions (`int`, defaults to 1024):\n                The dimension of the embedding vector, refer to the\n                `official documentation\n                <https://bailian.console.aliyun.com/?tab=api#/api/?type=model&url=2712515>`_\n                for more details.\n            embedding_cache (`EmbeddingCacheBase`):\n                The embedding cache class instance, used to cache the\n                embedding results to avoid repeated API calls.\n        \"\"\"\n        super().__init__(model_name, dimensions)\n\n        self.api_key = api_key\n        self.embedding_cache = embedding_cache\n        self.batch_size_limit = 10\n\n    async def _call_api(self, kwargs: dict[str, Any]) -> EmbeddingResponse:\n        \"\"\"Call the DashScope embedding API by the given keyword arguments.\"\"\"\n\n        if self.embedding_cache:\n            cached_embeddings = await self.embedding_cache.retrieve(\n                identifier=kwargs,\n            )\n            if cached_embeddings:\n                return EmbeddingResponse(\n                    embeddings=cached_embeddings,\n                    usage=EmbeddingUsage(\n                        tokens=0,\n                        time=0,\n                    ),\n                    source=\"cache\",\n                )\n\n        import dashscope\n\n        start_time = datetime.now()\n        response = dashscope.embeddings.TextEmbedding.call(\n            api_key=self.api_key,\n            **kwargs,\n        )\n        time = (datetime.now() - start_time).total_seconds()\n\n        if response.status_code != 200:\n            raise RuntimeError(\n                f\"Failed to get embedding from DashScope API: {response}\",\n            )\n\n        if self.embedding_cache:\n            await self.embedding_cache.store(\n                identifier=kwargs,\n                embeddings=[\n                    _[\"embedding\"] for _ in response.output[\"embeddings\"]\n                ],\n            )\n\n        return EmbeddingResponse(\n            embeddings=[_[\"embedding\"] for _ in response.output[\"embeddings\"]],\n            usage=EmbeddingUsage(\n                tokens=response.usage[\"total_tokens\"],\n                time=time,\n            ),\n        )\n\n    async def __call__(\n        self,\n        text: List[str | TextBlock],\n        **kwargs: Any,\n    ) -> EmbeddingResponse:\n        \"\"\"Call the DashScope embedding API.\n\n        Args:\n            text (`List[str | TextBlock]`):\n                The input text to be embedded. It can be a list of strings.\n        \"\"\"\n        gather_text = []\n        for _ in text:\n            if isinstance(_, dict) and \"text\" in _:\n                gather_text.append(_[\"text\"])\n            elif isinstance(_, str):\n                gather_text.append(_)\n            else:\n                raise ValueError(\n                    \"Input text must be a list of strings or TextBlock dicts.\",\n                )\n\n        if len(gather_text) > self.batch_size_limit:\n            logger.info(\n                \"The input texts (%d) will be embedded with %d API calls due \"\n                f\"to the batch size limit of {self.batch_size_limit} for \"\n                f\"DashScope embedding API.\",\n                len(gather_text),\n                (len(gather_text) + self.batch_size_limit - 1)\n                // self.batch_size_limit,\n            )\n\n        # Handle the batch size limit for DashScope embedding API\n        collected_embeddings = []\n        collected_time = 0.0\n        collected_tokens = 0\n        collected_source: Literal[\"cache\", \"api\"] = \"cache\"\n        for _ in range(0, len(gather_text), self.batch_size_limit):\n            batch_texts = gather_text[_ : _ + self.batch_size_limit]\n            batch_kwargs = {\n                \"input\": batch_texts,\n                \"model\": self.model_name,\n                \"dimension\": self.dimensions,\n                **kwargs,\n            }\n\n            res = await self._call_api(batch_kwargs)\n\n            collected_embeddings.extend(res.embeddings)\n            collected_time += res.usage.time\n            if res.usage.tokens:\n                collected_tokens += res.usage.tokens\n            if res.source == \"api\":\n                collected_source = \"api\"\n\n        return EmbeddingResponse(\n            embeddings=collected_embeddings,\n            usage=EmbeddingUsage(\n                tokens=collected_tokens,\n                time=collected_time,\n            ),\n            source=collected_source,\n        )\n"
  },
  {
    "path": "src/agentscope/embedding/_dashscope_multimodal_embedding.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The dashscope multimodal embedding model in agentscope.\"\"\"\nfrom datetime import datetime\nfrom typing import Any, Literal\n\nfrom ._cache_base import EmbeddingCacheBase\nfrom ._embedding_response import EmbeddingResponse\nfrom ._embedding_usage import EmbeddingUsage\nfrom ._embedding_base import EmbeddingModelBase\nfrom ..message import (\n    VideoBlock,\n    ImageBlock,\n    TextBlock,\n)\n\n\nclass DashScopeMultiModalEmbedding(EmbeddingModelBase):\n    \"\"\"The DashScope multimodal embedding API, supporting text, image and\n    video embedding.\"\"\"\n\n    supported_modalities: list[str] = [\"text\", \"image\", \"video\"]\n    \"\"\"This class supports text, image and video input.\"\"\"\n\n    def __init__(\n        self,\n        api_key: str,\n        model_name: str,\n        dimensions: int | None = None,\n        embedding_cache: EmbeddingCacheBase | None = None,\n    ) -> None:\n        \"\"\"Initialize the DashScope multimodal embedding model class.\n\n        Args:\n            api_key (`str`):\n                The dashscope API key.\n            model_name (`str`):\n                The name of the embedding model, e.g. \"multimodal-embedding-\n                v1\", \"tongyi-embedding-vision-plus\".\n            dimensions (`int`, defaults to 1024):\n                The dimension of the embedding vector, refer to the\n                `official documentation\n                <https://bailian.console.aliyun.com/?tab=api#/api/?type=model&url=2712517>`_\n                for more details.\n            embedding_cache (`EmbeddingCacheBase`):\n                The embedding cache class instance, used to cache the\n                embedding results to avoid repeated API calls.\n        \"\"\"\n        path_doc = (\n            \"https://bailian.console.aliyun.com/?tab=api#/api/?type=model&\"\n            \"url=2712517\"\n        )\n        self.batch_size_limit = 1\n\n        if model_name.startswith(\"tongyi-embedding-vision-plus\"):\n            self.batch_size_limit = 8\n            if dimensions is None:\n                dimensions = 1152\n            elif dimensions != 1152:\n                raise ValueError(\n                    f\"The dimension of model {model_name} must be  1152, \"\n                    \"refer to the official documentation for more details: \"\n                    f\"{path_doc}\",\n                )\n        if model_name.startswith(\"tongyi-embedding-vision-flash\"):\n            self.batch_size_limit = 8\n            if dimensions is None:\n                dimensions = 768\n            elif dimensions != 768:\n                raise ValueError(\n                    f\"The dimension of model {model_name} must be  768, \"\n                    \"refer to the official documentation for more details: \"\n                    f\"{path_doc}\",\n                )\n        if model_name.startswith(\"multimodal-embedding-v\"):\n            if dimensions is None:\n                dimensions = 1024\n            elif dimensions != 1024:\n                raise ValueError(\n                    f\"The dimension of model {model_name} must be  1024, \"\n                    \"refer to the official documentation for more details: \"\n                    f\"{path_doc}\",\n                )\n        refined_dimensions: int = 1024\n        if dimensions is not None:\n            refined_dimensions = dimensions\n        super().__init__(model_name, refined_dimensions)\n\n        self.api_key = api_key\n        self.embedding_cache = embedding_cache\n\n    async def __call__(\n        self,\n        inputs: list[TextBlock | ImageBlock | VideoBlock],\n        **kwargs: Any,\n    ) -> EmbeddingResponse:\n        \"\"\"Call the DashScope multimodal embedding API, which accepts text,\n        image, and video data.\n\n        Args:\n            inputs (`list[TextBlock | ImageBlock | VideoBlock]`):\n                The input data to be embedded. It can be a list of text,\n                image, and video blocks.\n\n        Returns:\n            `EmbeddingResponse`:\n                The embedding response object, which contains the embeddings\n                and usage information.\n        \"\"\"\n        # check data type\n        formatted_data = []\n        for _ in inputs:\n            if (\n                not isinstance(_, dict)\n                or \"type\" not in _\n                or _[\"type\"]\n                not in [\n                    \"text\",\n                    \"image\",\n                    \"video\",\n                ]\n            ):\n                raise ValueError(\n                    f\"Invalid data : {_}. It should be a list of \"\n                    \"TextBlock, ImageBlock, or VideoBlock.\",\n                )\n            if (\n                _[\"type\"] == \"video\"\n                and _.get(\"source\", {}).get(\"type\") != \"url\"\n            ):\n                raise ValueError(\n                    f\"The multimodal embedding API only supports URL input \"\n                    f\"for video data, but got {_}.\",\n                )\n\n            if _[\"type\"] == \"text\":\n                assert \"text\" in _, (\n                    f\"Invalid text block: {_}. It should contain a \"\n                    f\"'text' field.\",\n                )\n                formatted_data.append({\"text\": _[\"text\"]})\n\n            elif _[\"type\"] == \"video\":\n                formatted_data.append({\"video\": _[\"source\"][\"url\"]})\n\n            elif (\n                _[\"type\"] == \"image\"\n                and \"source\" in _\n                and _[\"source\"].get(\"type\") in [\"base64\", \"url\"]\n            ):\n                typ = _[\"source\"][\"type\"]\n                if typ == \"base64\":\n                    formatted_data.append(\n                        {\n                            \"image\": f'data:{_[\"source\"][\"media_type\"]};'\n                            f'base64,{_[\"source\"][\"data\"]}',\n                        },\n                    )\n                elif typ == \"url\":\n                    formatted_data.append(\n                        {\"image\": _[\"source\"][\"url\"]},\n                    )\n            else:\n                raise ValueError(\n                    f\"Invalid block {_}. It should be a valid TextBlock, \"\n                    f\"ImageBlock, or VideoBlock.\",\n                )\n\n        # Handle the batch size limit of the DashScope multimodal embedding API\n        collected_embeddings = []\n        collected_time = 0.0\n        collected_tokens = 0\n        collected_source: Literal[\"cache\", \"api\"] = \"cache\"\n        for _ in range(0, len(formatted_data), self.batch_size_limit):\n            batch_data = formatted_data[_ : _ + self.batch_size_limit]\n            batch_kwargs = {\n                \"input\": batch_data,\n                \"model\": self.model_name,\n                **kwargs,\n            }\n            res = await self._call_api(batch_kwargs)\n\n            collected_embeddings.extend(res.embeddings)\n            collected_time += res.usage.time\n            if res.usage.tokens:\n                collected_tokens += res.usage.tokens\n            if res.source == \"api\":\n                collected_source = \"api\"\n\n        return EmbeddingResponse(\n            embeddings=collected_embeddings,\n            usage=EmbeddingUsage(\n                tokens=collected_tokens,\n                time=collected_time,\n            ),\n            source=collected_source,\n        )\n\n    async def _call_api(self, kwargs: dict[str, Any]) -> EmbeddingResponse:\n        \"\"\"\n        Call the DashScope multimodal embedding API by the given arguments.\n        \"\"\"\n        # Search in cache first\n        if self.embedding_cache:\n            cached_embeddings = await self.embedding_cache.retrieve(\n                identifier=kwargs,\n            )\n            if cached_embeddings:\n                return EmbeddingResponse(\n                    embeddings=cached_embeddings,\n                    usage=EmbeddingUsage(\n                        tokens=0,\n                        time=0,\n                    ),\n                    source=\"cache\",\n                )\n\n        import dashscope\n\n        kwargs[\"api_key\"] = self.api_key\n\n        start_time = datetime.now()\n        res = dashscope.MultiModalEmbedding.call(**kwargs)\n        time = (datetime.now() - start_time).total_seconds()\n\n        if res.status_code != 200:\n            raise RuntimeError(\n                f\"Failed to get embedding from DashScope API: {res}\",\n            )\n\n        return EmbeddingResponse(\n            embeddings=[_[\"embedding\"] for _ in res.output[\"embeddings\"]],\n            usage=EmbeddingUsage(\n                tokens=res.usage.get(\n                    \"image_tokens\",\n                    0,\n                )\n                + res.usage.get(\n                    \"input_tokens\",\n                    0,\n                ),\n                time=time,\n            ),\n            source=\"api\",\n        )\n"
  },
  {
    "path": "src/agentscope/embedding/_embedding_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The embedding model base class.\"\"\"\nfrom typing import Any\n\nfrom ._embedding_response import EmbeddingResponse\n\n\nclass EmbeddingModelBase:\n    \"\"\"Base class for embedding models.\"\"\"\n\n    model_name: str\n    \"\"\"The embedding model name\"\"\"\n\n    supported_modalities: list[str]\n    \"\"\"The supported data modalities, e.g. \"text\", \"image\", \"video\".\"\"\"\n\n    dimensions: int\n    \"\"\"The dimensions of the embedding vector.\"\"\"\n\n    def __init__(\n        self,\n        model_name: str,\n        dimensions: int,\n    ) -> None:\n        \"\"\"Initialize the embedding model base class.\n\n        Args:\n            model_name (`str`):\n                The name of the embedding model.\n            dimensions (`int`):\n                The dimension of the embedding vector.\n        \"\"\"\n        self.model_name = model_name\n        self.dimensions = dimensions\n\n    async def __call__(\n        self,\n        *args: Any,\n        **kwargs: Any,\n    ) -> EmbeddingResponse:\n        \"\"\"Call the embedding API with the given arguments.\"\"\"\n        raise NotImplementedError(\n            f\"The {self.__class__.__name__} class does not implement \"\n            f\"the __call__ method.\",\n        )\n"
  },
  {
    "path": "src/agentscope/embedding/_embedding_response.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The embedding response class.\"\"\"\nfrom dataclasses import dataclass, field\nfrom typing import Literal, List\n\nfrom ._embedding_usage import EmbeddingUsage\nfrom .._utils._common import _get_timestamp\nfrom .._utils._mixin import DictMixin\nfrom ..types import Embedding\n\n\n@dataclass\nclass EmbeddingResponse(DictMixin):\n    \"\"\"The embedding response class.\"\"\"\n\n    embeddings: List[Embedding]\n    \"\"\"The embedding data\"\"\"\n\n    id: str = field(default_factory=lambda: _get_timestamp(True))\n    \"\"\"The identity of the embedding response\"\"\"\n\n    created_at: str = field(default_factory=_get_timestamp)\n    \"\"\"The timestamp of the embedding response creation\"\"\"\n\n    type: Literal[\"embedding\"] = field(default_factory=lambda: \"embedding\")\n    \"\"\"The type of the response, must be `embedding`.\"\"\"\n\n    usage: EmbeddingUsage | None = field(default_factory=lambda: None)\n    \"\"\"The usage of the embedding model API invocation, if available.\"\"\"\n\n    source: Literal[\"cache\", \"api\"] = field(default_factory=lambda: \"api\")\n    \"\"\"If the response comes from the cache or the API.\"\"\"\n"
  },
  {
    "path": "src/agentscope/embedding/_embedding_usage.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The embedding usage class in agentscope.\"\"\"\nfrom dataclasses import dataclass, field\nfrom typing import Literal\n\nfrom .._utils._mixin import DictMixin\n\n\n@dataclass\nclass EmbeddingUsage(DictMixin):\n    \"\"\"The usage of an embedding model API invocation.\"\"\"\n\n    time: float\n    \"\"\"The time used in seconds.\"\"\"\n\n    tokens: int | None = field(default_factory=lambda: None)\n    \"\"\"The number of tokens used, if available.\"\"\"\n\n    type: Literal[\"embedding\"] = field(default_factory=lambda: \"embedding\")\n    \"\"\"The type of the usage, must be `embedding`.\"\"\"\n"
  },
  {
    "path": "src/agentscope/embedding/_file_cache.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"A file embedding cache implementation for storing and retrieving\nembeddings in binary files.\"\"\"\nimport hashlib\nimport json\nimport os\nfrom typing import Any, List\n\nimport numpy as np\n\nfrom ._cache_base import EmbeddingCacheBase\nfrom .._logging import logger\nfrom ..types import (\n    Embedding,\n    JSONSerializableObject,\n)\n\n\nclass FileEmbeddingCache(EmbeddingCacheBase):\n    \"\"\"The embedding cache class that stores each embeddings vector in\n    binary files.\"\"\"\n\n    def __init__(\n        self,\n        cache_dir: str = \"./.cache/embeddings\",\n        max_file_number: int | None = None,\n        max_cache_size: int | None = None,\n    ) -> None:\n        \"\"\"Initialize the file embedding cache class.\n\n        Args:\n            cache_dir (`str`, defaults to `\"./.cache/embeddings\"`):\n                The directory to store the embedding files.\n            max_file_number (`int | None`, defaults to `None`):\n                The maximum number of files to keep in the cache directory. If\n                exceeded, the oldest files will be removed.\n            max_cache_size (`int | None`, defaults to `None`):\n                The maximum size of the cache directory in MB. If exceeded,\n                the oldest files will be removed until the size is within the\n                limit.\n        \"\"\"\n        self._cache_dir = os.path.abspath(cache_dir)\n        self.max_file_number = max_file_number\n        self.max_cache_size = max_cache_size\n\n    @property\n    def cache_dir(self) -> str:\n        \"\"\"The cache directory where the embedding files are stored.\"\"\"\n        if not os.path.exists(self._cache_dir):\n            os.makedirs(self._cache_dir, exist_ok=True)\n        return self._cache_dir\n\n    async def store(\n        self,\n        embeddings: List[Embedding],\n        identifier: JSONSerializableObject,\n        overwrite: bool = False,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Store the embeddings with the given identifier.\n\n        Args:\n            embeddings (`List[Embedding]`):\n                The embeddings to store.\n            identifier (`JSONSerializableObject`):\n                The identifier to distinguish the embeddings, which will be\n                used to generate a hashable filename, so it should be\n                JSON serializable (e.g. a string, number, list, dict).\n            overwrite (`bool`, defaults to `False`):\n                Whether to overwrite existing embeddings with the same\n                identifier. If `True`, existing embeddings will be replaced.\n        \"\"\"\n        filename = self._get_filename(identifier)\n        path_file = os.path.join(self.cache_dir, filename)\n\n        if os.path.exists(path_file):\n            if not os.path.isfile(path_file):\n                raise RuntimeError(\n                    f\"Path {path_file} exists but is not a file.\",\n                )\n\n            if overwrite:\n                np.save(path_file, embeddings)\n                await self._maintain_cache_dir()\n        else:\n            np.save(path_file, embeddings)\n            await self._maintain_cache_dir()\n\n    async def retrieve(\n        self,\n        identifier: JSONSerializableObject,\n    ) -> List[Embedding] | None:\n        \"\"\"Retrieve the embeddings with the given identifier. If not found,\n        return `None`.\n\n        Args:\n            identifier (`JSONSerializableObject`):\n                The identifier to retrieve the embeddings, which will be\n                used to generate a hashable filename, so it should be\n                JSON serializable (e.g. a string, number, list, dict).\n        \"\"\"\n        filename = self._get_filename(identifier)\n        path_file = os.path.join(self.cache_dir, filename)\n\n        if os.path.exists(path_file):\n            return np.load(os.path.join(self.cache_dir, filename)).tolist()\n        return None\n\n    async def remove(self, identifier: JSONSerializableObject) -> None:\n        \"\"\"Remove the embeddings with the given identifier.\n\n        Args:\n            identifier (`JSONSerializableObject`):\n                The identifiers to remove the embeddings, which will be\n                used to generate a hashable filename, so it should be\n                JSON serializable (e.g. a string, number, list, dict).\n        \"\"\"\n        filename = self._get_filename(identifier)\n        path_file = os.path.join(self.cache_dir, filename)\n\n        if os.path.exists(path_file):\n            os.remove(path_file)\n        else:\n            raise FileNotFoundError(f\"File {path_file} does not exist.\")\n\n    async def clear(self) -> None:\n        \"\"\"Clear the cache directory by removing all files.\"\"\"\n        for filename in os.listdir(self.cache_dir):\n            if filename.endswith(\".npy\"):\n                os.remove(os.path.join(self.cache_dir, filename))\n\n    def _get_cache_size(self) -> float:\n        \"\"\"Get the current size of the cache directory in MB.\"\"\"\n        total_size = 0\n        for filename in os.listdir(self.cache_dir):\n            if filename.endswith(\".npy\"):\n                path_file = os.path.join(self.cache_dir, filename)\n                if os.path.isfile(path_file):\n                    total_size += os.path.getsize(path_file)\n        return total_size / (1024.0 * 1024.0)\n\n    @staticmethod\n    def _get_filename(identifier: JSONSerializableObject) -> str:\n        \"\"\"Generate a filename based on the identifier.\"\"\"\n        json_str = json.dumps(identifier, ensure_ascii=False)\n        return hashlib.sha256(json_str.encode(\"utf-8\")).hexdigest() + \".npy\"\n\n    async def _maintain_cache_dir(self) -> None:\n        \"\"\"Maintain the cache directory by removing old files if the number of\n        files exceeds the maximum limit or if the cache size exceeds the\n        maximum size.\"\"\"\n        files = [\n            (_.name, _.stat().st_mtime)\n            for _ in os.scandir(self.cache_dir)\n            if _.is_file() and _.name.endswith(\".npy\")\n        ]\n        files.sort(key=lambda x: x[1])\n\n        if self.max_file_number and len(files) > self.max_file_number:\n            for file_name, _ in files[: 0 - self.max_file_number]:\n                os.remove(os.path.join(self.cache_dir, file_name))\n                logger.info(\n                    \"Remove cached embedding file %s for limited number \"\n                    \"of files (%d).\",\n                    file_name,\n                    self.max_file_number,\n                )\n            files = files[0 - self.max_file_number :]\n\n        if (\n            self.max_cache_size is not None\n            and self._get_cache_size() > self.max_cache_size\n        ):\n            removed_files = []\n            for filename, _ in files:\n                os.remove(os.path.join(self.cache_dir, filename))\n                removed_files.append(filename)\n                if self._get_cache_size() <= self.max_cache_size:\n                    break\n\n            if removed_files:\n                logger.info(\n                    \"Remove %d cached embedding file(s) for limited \"\n                    \"cache size (%d MB).\",\n                    len(removed_files),\n                    self.max_cache_size,\n                )\n"
  },
  {
    "path": "src/agentscope/embedding/_gemini_embedding.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The gemini text embedding model class.\"\"\"\nfrom datetime import datetime\nfrom typing import Any, List\n\nfrom ._embedding_response import EmbeddingResponse\nfrom ._embedding_usage import EmbeddingUsage\nfrom ._cache_base import EmbeddingCacheBase\nfrom ._embedding_base import EmbeddingModelBase\nfrom ..message import TextBlock\n\n\nclass GeminiTextEmbedding(EmbeddingModelBase):\n    \"\"\"The Gemini text embedding model.\"\"\"\n\n    supported_modalities: list[str] = [\"text\"]\n    \"\"\"This class only supports text input.\"\"\"\n\n    def __init__(\n        self,\n        api_key: str,\n        model_name: str,\n        dimensions: int = 3072,\n        embedding_cache: EmbeddingCacheBase | None = None,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Initialize the Gemini text embedding model class.\n\n        Args:\n            api_key (`str`):\n                The Gemini API key.\n            model_name (`str`):\n                The name of the embedding model.\n            dimensions (`int`, defaults to 3072):\n                The dimension of the embedding vector, refer to the\n                `official documentation\n                <https://ai.google.dev/gemini-api/docs/embeddings?hl=zh-cn#control-embedding-size>`_\n                for more details.\n            embedding_cache (`EmbeddingCacheBase | None`, defaults to `None`):\n                The embedding cache class instance, used to cache the\n                embedding results to avoid repeated API calls.\n        \"\"\"\n        from google import genai\n\n        super().__init__(model_name, dimensions)\n\n        self.client = genai.Client(api_key=api_key, **kwargs)\n        self.embedding_cache = embedding_cache\n\n    async def __call__(\n        self,\n        text: List[str | TextBlock],\n        **kwargs: Any,\n    ) -> EmbeddingResponse:\n        \"\"\"The Gemini embedding API call.\n\n        Args:\n            text (`List[str | TextBlock]`):\n                The input text to be embedded. It can be a list of strings.\n\n        # TODO: handle the batch size limit\n        \"\"\"\n        gather_text = []\n        for _ in text:\n            if isinstance(_, dict) and \"text\" in _:\n                gather_text.append(_[\"text\"])\n            elif isinstance(_, str):\n                gather_text.append(_)\n            else:\n                raise ValueError(\n                    \"Input text must be a list of strings or TextBlock dicts.\",\n                )\n\n        kwargs = {\n            \"model\": self.model_name,\n            \"contents\": gather_text,\n            \"config\": kwargs,\n        }\n\n        if self.embedding_cache:\n            cached_embeddings = await self.embedding_cache.retrieve(\n                identifier=kwargs,\n            )\n            if cached_embeddings:\n                return EmbeddingResponse(\n                    embeddings=cached_embeddings,\n                    usage=EmbeddingUsage(\n                        tokens=0,\n                        time=0,\n                    ),\n                    source=\"cache\",\n                )\n\n        start_time = datetime.now()\n        response = self.client.models.embed_content(**kwargs)\n        time = (datetime.now() - start_time).total_seconds()\n\n        if self.embedding_cache:\n            await self.embedding_cache.store(\n                identifier=kwargs,\n                embeddings=[_.values for _ in response.embeddings],\n            )\n\n        return EmbeddingResponse(\n            embeddings=[_.values for _ in response.embeddings],\n            usage=EmbeddingUsage(\n                time=time,\n            ),\n        )\n"
  },
  {
    "path": "src/agentscope/embedding/_ollama_embedding.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The ollama text embedding model class.\"\"\"\nfrom datetime import datetime\nfrom typing import List, Any\n\nfrom ._embedding_response import EmbeddingResponse\nfrom ._embedding_usage import EmbeddingUsage\nfrom ._cache_base import EmbeddingCacheBase\nfrom ..embedding import EmbeddingModelBase\nfrom ..message import TextBlock\n\n\nclass OllamaTextEmbedding(EmbeddingModelBase):\n    \"\"\"The Ollama embedding model.\"\"\"\n\n    supported_modalities: list[str] = [\"text\"]\n    \"\"\"This class only supports text input.\"\"\"\n\n    def __init__(\n        self,\n        model_name: str,\n        dimensions: int,\n        host: str | None = None,\n        embedding_cache: EmbeddingCacheBase | None = None,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Initialize the Ollama text embedding model class.\n\n        Args:\n            model_name (`str`):\n                The name of the embedding model.\n            dimensions (`int`):\n                The dimension of the embedding vector, the parameter should be\n                provided according to the model used.\n            host (`str | None`, defaults to `None`):\n                The host URL for the Ollama API.\n            embedding_cache (`EmbeddingCacheBase | None`, defaults to `None`):\n                The embedding cache class instance, used to cache the\n                embedding results to avoid repeated API calls.\n        \"\"\"\n        import ollama\n\n        super().__init__(model_name, dimensions)\n\n        self.client = ollama.AsyncClient(host=host, **kwargs)\n        self.embedding_cache = embedding_cache\n\n    async def __call__(\n        self,\n        text: List[str | TextBlock],\n        **kwargs: Any,\n    ) -> EmbeddingResponse:\n        \"\"\"Call the Ollama embedding API.\n\n        Args:\n            text (`List[str | TextBlock]`):\n                The input text to be embedded. It can be a list of strings.\n        \"\"\"\n        gather_text = []\n        for _ in text:\n            if isinstance(_, dict) and \"text\" in _:\n                gather_text.append(_[\"text\"])\n            elif isinstance(_, str):\n                gather_text.append(_)\n            else:\n                raise ValueError(\n                    \"Input text must be a list of strings or TextBlock dicts.\",\n                )\n\n        kwargs = {\n            \"input\": gather_text,\n            \"model\": self.model_name,\n            \"dimensions\": self.dimensions,\n            **kwargs,\n        }\n\n        if self.embedding_cache:\n            cached_embeddings = await self.embedding_cache.retrieve(\n                identifier=kwargs,\n            )\n            if cached_embeddings:\n                return EmbeddingResponse(\n                    embeddings=cached_embeddings,\n                    usage=EmbeddingUsage(\n                        tokens=0,\n                        time=0,\n                    ),\n                    source=\"cache\",\n                )\n\n        start_time = datetime.now()\n        response = await self.client.embed(**kwargs)\n        time = (datetime.now() - start_time).total_seconds()\n\n        if self.embedding_cache:\n            await self.embedding_cache.store(\n                identifier=kwargs,\n                embeddings=response.embeddings,\n            )\n\n        return EmbeddingResponse(\n            embeddings=response.embeddings,\n            usage=EmbeddingUsage(\n                time=time,\n            ),\n        )\n"
  },
  {
    "path": "src/agentscope/embedding/_openai_embedding.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The OpenAI text embedding model class.\"\"\"\nfrom datetime import datetime\nfrom typing import Any, List\n\nfrom ._embedding_response import EmbeddingResponse\nfrom ._embedding_usage import EmbeddingUsage\nfrom ._cache_base import EmbeddingCacheBase\nfrom ._embedding_base import EmbeddingModelBase\nfrom ..message import TextBlock\n\n\nclass OpenAITextEmbedding(EmbeddingModelBase):\n    \"\"\"OpenAI text embedding model class.\"\"\"\n\n    supported_modalities: list[str] = [\"text\"]\n    \"\"\"This class only supports text input.\"\"\"\n\n    def __init__(\n        self,\n        api_key: str,\n        model_name: str,\n        dimensions: int = 1024,\n        embedding_cache: EmbeddingCacheBase | None = None,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Initialize the OpenAI text embedding model class.\n\n        Args:\n            api_key (`str`):\n                The OpenAI API key.\n            model_name (`str`):\n                The name of the embedding model.\n            dimensions (`int`, defaults to 1024):\n                The dimension of the embedding vector.\n            embedding_cache (`EmbeddingCacheBase | None`, defaults to `None`):\n                The embedding cache class instance, used to cache the\n                embedding results to avoid repeated API calls.\n\n        # TODO: handle batch size limit and token limit\n        \"\"\"\n        import openai\n\n        super().__init__(model_name, dimensions)\n\n        self.client = openai.AsyncClient(api_key=api_key, **kwargs)\n        self.embedding_cache = embedding_cache\n\n    async def __call__(\n        self,\n        text: List[str | TextBlock],\n        **kwargs: Any,\n    ) -> EmbeddingResponse:\n        \"\"\"Call the OpenAI embedding API.\n\n        Args:\n            text (`List[str | TextBlock]`):\n                The input text to be embedded. It can be a list of strings.\n        \"\"\"\n        gather_text = []\n        for _ in text:\n            if isinstance(_, dict) and \"text\" in _:\n                gather_text.append(_[\"text\"])\n            elif isinstance(_, str):\n                gather_text.append(_)\n            else:\n                raise ValueError(\n                    \"Input text must be a list of strings or TextBlock dicts.\",\n                )\n\n        kwargs = {\n            \"input\": gather_text,\n            \"model\": self.model_name,\n            \"dimensions\": self.dimensions,\n            \"encoding_format\": \"float\",\n            **kwargs,\n        }\n\n        if self.embedding_cache:\n            cached_embeddings = await self.embedding_cache.retrieve(\n                identifier=kwargs,\n            )\n            if cached_embeddings:\n                return EmbeddingResponse(\n                    embeddings=cached_embeddings,\n                    usage=EmbeddingUsage(\n                        tokens=0,\n                        time=0,\n                    ),\n                    source=\"cache\",\n                )\n\n        start_time = datetime.now()\n        response = await self.client.embeddings.create(**kwargs)\n        time = (datetime.now() - start_time).total_seconds()\n\n        if self.embedding_cache:\n            await self.embedding_cache.store(\n                identifier=kwargs,\n                embeddings=[_.embedding for _ in response.data],\n            )\n\n        return EmbeddingResponse(\n            embeddings=[_.embedding for _ in response.data],\n            usage=EmbeddingUsage(\n                tokens=response.usage.total_tokens,\n                time=time,\n            ),\n        )\n"
  },
  {
    "path": "src/agentscope/evaluate/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The evaluation module in AgentScope.\"\"\"\n\nfrom ._evaluator import (\n    EvaluatorBase,\n    RayEvaluator,\n    GeneralEvaluator,\n)\nfrom ._metric_base import (\n    MetricBase,\n    MetricResult,\n    MetricType,\n)\nfrom ._task import Task\nfrom ._solution import SolutionOutput\nfrom ._benchmark_base import BenchmarkBase\nfrom ._evaluator_storage import (\n    EvaluatorStorageBase,\n    FileEvaluatorStorage,\n)\nfrom ._ace_benchmark import (\n    ACEBenchmark,\n    ACEAccuracy,\n    ACEProcessAccuracy,\n    ACEPhone,\n)\n\n__all__ = [\n    \"BenchmarkBase\",\n    \"EvaluatorBase\",\n    \"RayEvaluator\",\n    \"GeneralEvaluator\",\n    \"MetricBase\",\n    \"MetricResult\",\n    \"MetricType\",\n    \"EvaluatorStorageBase\",\n    \"FileEvaluatorStorage\",\n    \"Task\",\n    \"SolutionOutput\",\n    \"ACEBenchmark\",\n    \"ACEAccuracy\",\n    \"ACEProcessAccuracy\",\n    \"ACEPhone\",\n]\n"
  },
  {
    "path": "src/agentscope/evaluate/_ace_benchmark/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The ACE benchmark related implementations in AgentScope.\"\"\"\n\nfrom ._ace_benchmark import ACEBenchmark\nfrom ._ace_metric import (\n    ACEAccuracy,\n    ACEProcessAccuracy,\n)\nfrom ._ace_tools_zh import ACEPhone\n\n__all__ = [\n    \"ACEBenchmark\",\n    \"ACEPhone\",\n    \"ACEAccuracy\",\n    \"ACEProcessAccuracy\",\n]\n"
  },
  {
    "path": "src/agentscope/evaluate/_ace_benchmark/_ace_benchmark.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The ACE benchmark class in agentscope. The code is implemented with\nreference to the `ACEBench <https://github.com/ACEBench/ACEBench>`_\nunder the MIT license.\"\"\"\nimport json\nimport os\nfrom typing import Generator\n\nimport json5\nimport requests\nfrom tqdm import tqdm\n\nfrom ._ace_metric import ACEAccuracy, ACEProcessAccuracy\nfrom ._ace_tools_zh import ACEPhone\nfrom .._benchmark_base import BenchmarkBase\nfrom .._task import Task\n\n\nclass ACEBenchmark(BenchmarkBase):\n    \"\"\"The ACE benchmark for evaluating AI agents.\"\"\"\n\n    data_dir_url: str = (\n        \"https://raw.githubusercontent.com/ACEBench/ACEBench/main/data_all\"\n    )\n    \"\"\"The URL to the data dir\"\"\"\n\n    data_subdir: list[str] = [\n        # \"data_en\",  # TODO: enable English version\n        \"data_zh\",\n    ]\n\n    ground_truth_dir: str = \"possible_answer\"\n\n    data_files: list[str] = [\n        \"data_agent_multi_step.json\",\n        \"data_agent_multi_turn.json\",\n        # \"data_normal_atom_bool.json\",\n        # \"data_normal_atom_enum.json\",\n        # \"data_normal_atom_list.json\",\n        # \"data_normal_atom_number.json\",\n        # \"data_normal_atom_object_deep.json\",\n        # \"data_normal_atom_object_short.json\",\n        #\n        # \"data_normal_multi_turn_user_adjust.json\",\n        # \"data_normal_multi_turn_user_switch.json\",\n        #\n        # \"data_normal_preference.json\",\n        # \"data_normal_similar_api.json\",\n        # \"data_normal_single_turn_parallel_function.json\",\n        # \"data_normal_single_turn_single_function.json\",\n        #\n        # \"data_special_error_param.json\",\n        # \"data_special_incomplete.json\",\n        # \"data_special_irrelevant.json\",\n    ]\n    \"\"\"The data filenames\"\"\"\n\n    def __init__(\n        self,\n        data_dir: str,\n    ) -> None:\n        \"\"\"Initialize the ACEBenchmark\n\n        Args:\n            data_dir (`str`):\n                The directory where the dataset is downloaded and saved.\n        \"\"\"\n        super().__init__(\n            name=\"ACEBench\",\n            description=\"The ACE benchmark for evaluating AI agents.\",\n        )\n\n        self.data_dir = os.path.abspath(data_dir)\n\n        if os.path.exists(data_dir) and not os.path.isdir(data_dir):\n            raise RuntimeError(\n                f\"The data_dir `{data_dir}` is not a valid directory path.\",\n            )\n\n        os.makedirs(data_dir, exist_ok=True)\n\n        if not self._verify_data():\n            self._download_data()\n\n        self.dataset = self._load_data()\n\n    def _load_data(self) -> list[dict]:\n        \"\"\"Load the dataset from the data directory.\"\"\"\n        dataset = []\n        for subdir in self.data_subdir:\n            for filename in self.data_files:\n                file_path = os.path.join(self.data_dir, subdir, filename)\n\n                gt_path = os.path.join(\n                    self.data_dir,\n                    subdir,\n                    self.ground_truth_dir,\n                    filename,\n                )\n                gt_dataset = {}\n                with open(gt_path, \"r\", encoding=\"utf-8\") as gt_file:\n                    for line in gt_file:\n                        gt_data = json5.loads(line)\n                        gt_dataset[gt_data[\"id\"]] = gt_data\n\n                with open(file_path, \"r\", encoding=\"utf-8\") as f:\n                    for line in f:\n                        data = json5.loads(line)\n                        gt = gt_dataset[data[\"id\"]]\n                        gt.pop(\"id\", None)\n                        data[\"ground_truth\"] = gt[\"ground_truth\"]\n                        data[\"mile_stone\"] = gt[\"mile_stone\"]\n                        data[\"language\"] = subdir.rsplit(\n                            \"_\",\n                            maxsplit=1,\n                        )[-1]\n                        data[\"tags\"] = {\n                            \"language\": data[\"language\"],\n                            \"category\": filename.split(\n                                \".\",\n                                maxsplit=1,\n                            )[0].removeprefix(\n                                \"data_\",\n                            ),\n                        }\n                        dataset.append(data)\n\n        return dataset\n\n    def _verify_data(self) -> bool:\n        \"\"\"Verify the data completeness and integrity.\"\"\"\n        for subdir in self.data_subdir:\n            for filename in self.data_files:\n                file_path = os.path.join(self.data_dir, subdir, filename)\n                if not os.path.exists(file_path):\n                    return False\n\n                gt_path = os.path.join(\n                    self.data_dir,\n                    subdir,\n                    self.ground_truth_dir,\n                    filename,\n                )\n                if not os.path.exists(gt_path):\n                    return False\n\n        return True\n\n    def _download_data(self) -> None:\n        \"\"\"Download the data from the URL\"\"\"\n        for subdir in self.data_subdir:\n            subdir_path = os.path.join(self.data_dir, subdir)\n            subdir_gt_path = os.path.join(subdir_path, self.ground_truth_dir)\n            os.makedirs(subdir_path, exist_ok=True)\n            os.makedirs(subdir_gt_path, exist_ok=True)\n            for filename in tqdm(\n                self.data_files,\n                desc=f\"Downloading {subdir}\",\n            ):\n                response = requests.get(\n                    f\"{self.data_dir_url}/{subdir}/{filename}\",\n                )\n                response.raise_for_status()\n                with open(os.path.join(subdir_path, filename), \"wb\") as f:\n                    f.write(response.content)\n\n                gt_response = requests.get(\n                    f\"{self.data_dir_url}/{subdir}/\"\n                    f\"{self.ground_truth_dir}/{filename}\",\n                )\n                gt_response.raise_for_status()\n                with open(os.path.join(subdir_gt_path, filename), \"wb\") as f:\n                    f.write(gt_response.content)\n\n    @staticmethod\n    def _data_to_task(item: dict) -> Task:\n        \"\"\"Convert a dataset item to a Task object.\"\"\"\n        # Start the simulated phone and load initial configuration\n        ace_phone = ACEPhone()\n        ace_phone.load_initial_config(item[\"initial_config\"])\n\n        # Obtain tool functions\n        tools: list[tuple] = []\n        for function_schema in item[\"function\"]:\n            name = function_schema[\"name\"]\n\n            # Handle the schema differences\n            formatted_schema = json.loads(\n                json.dumps(\n                    function_schema,\n                ).replace(\n                    '\"type\": \"dict\"',\n                    '\"type\": \"object\"',\n                ),\n            )\n\n            tool_function = ace_phone.get_tool_function(name)\n            tools.append(\n                (\n                    tool_function,\n                    {\n                        \"type\": \"function\",\n                        \"function\": formatted_schema,\n                    },\n                ),\n            )\n\n        return Task(\n            id=item[\"id\"],\n            input=item[\"question\"],\n            ground_truth={\n                \"state\": item[\"ground_truth\"],\n                \"mile_stone\": item.get(\"mile_stone\", []),\n            },\n            tags=item.get(\"tags\", {}),\n            metrics=[\n                ACEAccuracy(item[\"ground_truth\"]),\n                ACEProcessAccuracy(item[\"mile_stone\"]),\n            ],\n            metadata={\n                # The phone is used to extract the final state after finishing\n                # the task.\n                \"phone\": ace_phone,\n                # The provided tools for this task, used to equip the agent\n                \"tools\": tools,\n            },\n        )\n\n    def __iter__(self) -> Generator[Task, None, None]:\n        \"\"\"Iterate over the benchmark.\"\"\"\n        for item in self.dataset:\n            yield self._data_to_task(item)\n\n    def __getitem__(self, index: int) -> Task:\n        \"\"\"Get a task by index.\"\"\"\n        return self._data_to_task(self.dataset[index])\n\n    def __len__(self) -> int:\n        \"\"\"Get the length of the benchmark.\"\"\"\n        return len(self.dataset)\n"
  },
  {
    "path": "src/agentscope/evaluate/_ace_benchmark/_ace_metric.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The ACE benchmark metric implementations in AgentScope.\"\"\"\n\nfrom .._solution import SolutionOutput\nfrom .._metric_base import MetricBase, MetricResult, MetricType\n\n\nclass ACEProcessAccuracy(MetricBase):\n    \"\"\"The ace benchmark process accuracy metric.\"\"\"\n\n    def __init__(\n        self,\n        mile_stone: list[str],\n    ) -> None:\n        \"\"\"Initialize the AceBench process accuracy metric.\"\"\"\n        super().__init__(\n            name=\"process_accuracy\",\n            metric_type=MetricType.NUMERICAL,\n            description=\"The AceBench Agent eval process accuracy metric.\",\n        )\n        self.mile_stone = mile_stone\n\n    async def __call__(\n        self,\n        solution: SolutionOutput,\n    ) -> MetricResult:\n        \"\"\"Calculate the metric result.\"\"\"\n\n        # Turn the tool use block sequence into ACEBench format\n        # e.g. func(arg1='dfd', arg2=44)\n        gathered_trajectory = []\n        for tool_call in solution.trajectory:\n            if tool_call.get(\"type\") == \"tool_use\":\n                function_name = tool_call.get(\"name\")\n                kwargs = tool_call.get(\"input\")\n\n                gathered_kwargs = []\n                for key, value in kwargs.items():\n                    if isinstance(value, str):\n                        gathered_kwargs.append(\n                            f\"{key}='{value}'\",\n                        )\n\n                    else:\n                        gathered_kwargs.append(\n                            f\"{key}={value}\",\n                        )\n\n                kwargs_str = \", \".join(gathered_kwargs)\n                gathered_trajectory.append(\n                    f\"[{function_name}({kwargs_str})]\",\n                )\n\n        for stone in self.mile_stone:\n            if stone not in gathered_trajectory:\n                return MetricResult(\n                    name=self.name,\n                    result=0,\n                    message=f\"Error: Missing milestone '{stone}' in \"\n                    \"the given trajectory.\",\n                )\n\n        return MetricResult(\n            name=self.name,\n            result=1,\n            message=\"Success\",\n        )\n\n\nclass ACEAccuracy(MetricBase):\n    \"\"\"The ace benchmark metric\"\"\"\n\n    def __init__(\n        self,\n        state: list[dict],\n    ) -> None:\n        \"\"\"Initialize the _metric object.\"\"\"\n        super().__init__(\n            \"accuracy\",\n            MetricType.NUMERICAL,\n            \"The AceBench Agent eval accuracy metric.\",\n        )\n        self.state = state\n\n    async def __call__(\n        self,\n        solution: SolutionOutput,\n    ) -> MetricResult:\n        \"\"\"Calculate the metric result.\"\"\"\n        # Check if the solution matches the ground truth\n        if not isinstance(solution.output, list):\n            raise ValueError(\"Ground truth state must be a list.\")\n\n        # Handle the typos in ACEBench dataset\n        gathered_state = {}\n        for item in self.state:\n            for key, value in item.items():\n                if key.endswith(\"API\"):\n                    key = key.replace(\"API\", \"Api\")\n                elif key.endswith(\"rpi\"):\n                    key = key.replace(\"pi\", \"Api\")\n                gathered_state[key] = value\n\n        gathered_output = {}\n        for item in solution.output:\n            for key, value in item.items():\n                gathered_output[key] = value\n\n        if not set(gathered_state.keys()).issubset(gathered_output.keys()):\n            raise ValueError(\n                \"Missing keys in solution output compared to state, \"\n                f\"ground truth keys: {gathered_state.keys()}, \"\n                f\"solution keys: {gathered_output.keys()}\",\n            )\n\n        for key, value in gathered_state.items():\n            if value != gathered_output.get(key):\n                return MetricResult(\n                    name=self.name,\n                    result=0,\n                    message=(\n                        f\"Error: Mismatch in key '{key}':\"\n                        f\"\\n{value}\\n{gathered_output.get(key)}\"\n                    ),\n                )\n\n        return MetricResult(\n            name=self.name,\n            result=1,\n            message=\"Success: All keys match\",\n        )\n"
  },
  {
    "path": "src/agentscope/evaluate/_ace_benchmark/_ace_tools_api/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The ACEBench simulation tools in AgentScope.\"\"\"\n\nfrom ._message_api import MessageApi\nfrom ._travel_api import TravelApi\nfrom ._reminder_api import ReminderApi\nfrom ._food_platform_api import FoodPlatformApi\n\n__all__ = [\n    \"MessageApi\",\n    \"TravelApi\",\n    \"ReminderApi\",\n    \"FoodPlatformApi\",\n]\n"
  },
  {
    "path": "src/agentscope/evaluate/_ace_benchmark/_ace_tools_api/_food_platform_api.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The food platform API in the ACEBench evaluation.\"\"\"\n\nfrom ._shared_state import SharedState\n\n\nclass FoodPlatformApi(SharedState):\n    \"\"\"The food platform Api in the ACEBench evaluation.\"\"\"\n\n    tool_functions: list[str] = [\n        \"login_food_platform\",\n        \"view_logged_in_users\",\n        \"check_balance\",\n        \"add_food_delivery_order\",\n        \"get_products\",\n        \"view_orders\",\n        \"search_orders\",\n    ]\n\n    def __init__(self, shared_state: dict) -> None:\n        super().__init__(shared_state)\n\n        # 设置用户和初始金额\n        self.users: dict = {\n            \"Eve\": {\n                \"user_id\": \"U100\",\n                \"password\": \"password123\",\n                \"balance\": 500.0,\n            },\n            \"Frank\": {\n                \"user_id\": \"U101\",\n                \"password\": \"password456\",\n                \"balance\": 300.0,\n            },\n            \"Grace\": {\n                \"user_id\": \"U102\",\n                \"password\": \"password789\",\n                \"balance\": 150.0,\n            },\n            \"Helen\": {\n                \"user_id\": \"U103\",\n                \"password\": \"password321\",\n                \"balance\": 800.0,\n            },\n            \"Isaac\": {\n                \"user_id\": \"U104\",\n                \"password\": \"password654\",\n                \"balance\": 400.0,\n            },\n            \"Jack\": {\n                \"user_id\": \"U105\",\n                \"password\": \"password654\",\n                \"balance\": 120.0,\n            },\n        }\n\n        # 设置六个商家及其菜单\n        self.merchant_list: dict[str, dict] = {\n            \"达美乐\": {\n                \"merchant_id\": \"M100\",\n                \"service_type\": \"Pizza\",\n                \"menu\": [\n                    {\"product\": \"玛格丽特披萨\", \"price\": 68.0},\n                    {\"product\": \"超级至尊披萨\", \"price\": 88.0},\n                ],\n            },\n            \"米村拌饭\": {\n                \"merchant_id\": \"M101\",\n                \"service_type\": \"Bibimbap\",\n                \"menu\": [\n                    {\"product\": \"石锅拌饭\", \"price\": 35.0},\n                    {\"product\": \"韩式牛肉拌饭\", \"price\": 45.0},\n                ],\n            },\n            \"海底捞\": {\n                \"merchant_id\": \"M102\",\n                \"service_type\": \"Hotpot\",\n                \"menu\": [\n                    {\"product\": \"牛肉卷\", \"price\": 68.0},\n                    {\"product\": \"海鲜拼盘\", \"price\": 88.0},\n                ],\n            },\n            \"喜茶\": {\n                \"merchant_id\": \"M103\",\n                \"service_type\": \"Milk Tea\",\n                \"menu\": [\n                    {\"product\": \"芝士奶茶\", \"price\": 25.0},\n                    {\"product\": \"四季春奶茶\", \"price\": 22.0},\n                ],\n            },\n            \"盒马生鲜\": {\n                \"merchant_id\": \"M104\",\n                \"service_type\": \"Fresh Grocery\",\n                \"menu\": [\n                    {\"product\": \"有机蔬菜包\", \"price\": 15.0},\n                    {\"product\": \"生鲜大礼包\", \"price\": 99.0},\n                ],\n            },\n            \"九田家烤肉\": {\n                \"merchant_id\": \"M105\",\n                \"service_type\": \"BBQ\",\n                \"menu\": [\n                    {\"product\": \"韩式烤牛肉\", \"price\": 128.0},\n                    {\"product\": \"烤五花肉\", \"price\": 78.0},\n                ],\n            },\n        }\n\n        # 设置已登录用户列表\n        self.logged_in_users: list[str] = []\n        # 订单列表\n        self.orders: list = []\n\n    def get_state_dict(self) -> dict:\n        \"\"\"Get the current state dict of the FoodPlatformApi.\"\"\"\n        return {\n            \"FoodPlatform\": {\n                \"logged_in_users\": self.logged_in_users,\n                \"orders\": self.orders,\n                \"users\": self.users,\n            },\n        }\n\n    def login_food_platform(\n        self,\n        username: str,\n        password: str,\n    ) -> dict[str, bool | str]:\n        \"\"\"使用用户名和密码登录外卖平台。\n\n        Args:\n            username (`str`):\n                用户的用户名。\n            password (`str`):\n                用户的密码。\n        \"\"\"\n        if not self.wifi:\n            return {\"status\": False, \"message\": \"wifi未打开，无法登录\"}\n        if username not in self.users:\n            return {\"status\": False, \"message\": \"用户不存在\"}\n        if self.users[username][\"password\"] != password:\n            return {\"status\": False, \"message\": \"密码错误\"}\n\n        # 检查是否已经有用户登录\n        if username in self.logged_in_users:\n            return {\"status\": False, \"message\": f\"{username} 已经登录\"}\n\n        # 记录已登录用户\n        self.logged_in_users.append(username)\n        return {\"status\": True, \"message\": f\"用户{username}登陆成功！\"}\n\n    def view_logged_in_users(self) -> dict:\n        \"\"\"查看当前所有登录的用户。\"\"\"\n        if not self.logged_in_users:\n            return {\n                \"status\": False,\n                \"message\": \"当前没有登录food platform\",\n            }\n\n        return {\"status\": True, \"logged_in_users\": self.logged_in_users}\n\n    def check_balance(self, user_name: str) -> float:\n        \"\"\"查询指定用户的余额。\n\n        Args:\n            user_name (`str`):\n                用户的用户名。\n        \"\"\"\n        if user_name in self.users:\n            return self.users[user_name][\"balance\"]\n        else:\n            return 0.0\n\n    def add_food_delivery_order(\n        self,\n        username: str,\n        merchant_name: str,\n        items: list[dict[str, str | int]],\n    ) -> dict[str, bool | str]:\n        \"\"\"订外卖\n\n        Args:\n            username (`str`):\n                下订单的用户姓名。\n            merchant_name (`str`):\n                下订单的商家名称。\n            items (`list[dict[str, str | int]]`):\n                订单中商品的列表，每个商品包含名称和数量。\n        \"\"\"\n        if username not in self.logged_in_users:\n            return {\n                \"status\": False,\n                \"message\": f\"用户 {username} 未登录food platform\",\n            }\n\n        if merchant_name not in self.merchant_list:\n            return {\"status\": False, \"message\": \"商家不存在\"}\n\n        total_price = 0.0\n        order_items = []\n\n        for item in items:\n            product_name = item.get(\"product\")\n            quantity = item.get(\"quantity\", 1)\n\n            if not isinstance(quantity, int) or quantity <= 0:\n                return {\n                    \"status\": False,\n                    \"message\": f\"无效的数量 {quantity} 对于商品 {product_name}\",\n                }\n\n            # 查找商品价格\n            product_found = False\n            for product in self.merchant_list[merchant_name][\"menu\"]:\n                if product[\"product\"] == product_name:\n                    total_price += product[\"price\"] * quantity\n                    order_items.append(\n                        {\n                            \"product\": product_name,\n                            \"quantity\": quantity,\n                            \"price_per_unit\": product[\"price\"],\n                        },\n                    )\n                    product_found = True\n                    break\n            if not product_found:\n                return {\n                    \"status\": False,\n                    \"message\": f\"商品 {product_name} 不存在于 \"\n                    f\"{merchant_name} 的菜单中\",\n                }\n\n        # 检查余额是否足够\n        if total_price >= self.users[username][\"balance\"]:\n            return {\"status\": False, \"message\": \"余额不足，无法下单\"}\n\n        # 扣除余额并创建订单\n        self.users[username][\"balance\"] -= total_price\n        order = {\n            \"user_name\": username,\n            \"merchant_name\": merchant_name,\n            \"items\": order_items,\n            \"total_price\": total_price,\n        }\n        self.orders.append(order)\n        return {\n            \"status\": True,\n            \"message\": f\"外卖订单成功下单给 {merchant_name}，\" f\"总金额为 {total_price} 元\",\n        }\n\n    def get_products(\n        self,\n        merchant_name: str,\n    ) -> list[dict[str, str | float]] | dict[str, bool | str]:\n        \"\"\"获取特定商家的商品列表。\n\n        Args:\n            merchant_name (`str`):\n                要获取商品的商家名称。\n        \"\"\"\n        merchant = self.merchant_list.get(merchant_name)\n        if merchant:\n            return merchant[\"menu\"]\n        else:\n            return {\n                \"status\": False,\n                \"message\": f\"商家 '{merchant_name}' 不存在\",\n            }\n\n    def view_orders(\n        self,\n        user_name: str,\n    ) -> dict[str, bool | str | list[dict[str, str | int | float]]]:\n        \"\"\"查看用户的所有订单\"\"\"\n        user_orders = [\n            order for order in self.orders if order[\"user_name\"] == user_name\n        ]\n\n        if not user_orders:\n            return {\"status\": False, \"message\": \"用户没有订单记录\"}\n\n        return {\"status\": True, \"orders\": user_orders}\n\n    def search_orders(\n        self,\n        keyword: str,\n    ) -> dict[str, bool | str | list[dict[str, str | float]]]:\n        \"\"\"根据关键字搜索订单。\"\"\"\n        matched_orders = [\n            order\n            for order in self.orders\n            if keyword.lower() in order[\"merchant_name\"].lower()\n            or any(\n                keyword.lower() in item.lower()\n                for item in order.get(\"items\", [])\n            )\n        ]\n\n        if not matched_orders:\n            return {\"status\": False, \"message\": \"没有找到匹配的订单\"}\n\n        return {\"status\": True, \"orders\": matched_orders}\n"
  },
  {
    "path": "src/agentscope/evaluate/_ace_benchmark/_ace_tools_api/_message_api.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The Message API in the ACEBench evaluation.\"\"\"\nfrom datetime import datetime\n\nfrom ._shared_state import SharedState\n\n\nclass MessageApi(SharedState):\n    \"\"\"The message Api in the ACEBench evaluation.\"\"\"\n\n    tool_functions: list[str] = [\n        \"send_message\",\n        \"delete_message\",\n        \"view_messages_between_users\",\n        \"search_messages\",\n        \"get_all_message_times_with_ids\",\n        \"get_latest_message_id\",\n        \"get_earliest_message_id\",\n    ]\n\n    def __init__(self, share_state: dict) -> None:\n        \"\"\"Initialize the MessageApi with shared state.\"\"\"\n        super().__init__(share_state)\n\n        # 设置六个用户\n        self.max_capacity = 6\n        self.user_list: dict[str, dict[str, str | int]] = {\n            \"Eve\": {\n                \"user_id\": \"USR100\",\n                \"phone_number\": \"123-456-7890\",\n                \"occupation\": \"Software Engineer\",\n            },\n            \"Frank\": {\n                \"user_id\": \"USR101\",\n                \"phone_number\": \"234-567-8901\",\n                \"occupation\": \"Data Scientist\",\n            },\n            \"Grace\": {\n                \"user_id\": \"USR102\",\n                \"phone_number\": \"345-678-9012\",\n                \"occupation\": \"Product Manager\",\n            },\n            \"Helen\": {\n                \"user_id\": \"USR103\",\n                \"phone_number\": \"456-789-0123\",\n                \"occupation\": \"UX Designer\",\n            },\n            \"Isaac\": {\n                \"user_id\": \"USR104\",\n                \"phone_number\": \"567-890-1234\",\n                \"occupation\": \"DevOps Engineer\",\n            },\n            \"Jack\": {\n                \"user_id\": \"USR105\",\n                \"phone_number\": \"678-901-2345\",\n                \"occupation\": \"Marketing Specialist\",\n            },\n        }\n\n        # 设置六个用户之间的短信记录\n        # 信息1和reminder配合  信息2和food配合\n        self.inbox: dict[int, dict[str, str | int]] = {\n            1: {\n                \"sender_id\": \"USR100\",\n                \"receiver_id\": \"USR101\",\n                \"message\": \"Hey Frank, don't forget about our meeting on \"\n                \"2024-06-11 at 4 PM in Conference Room 1.\",\n                \"time\": \"2024-06-09\",\n            },\n            2: {\n                \"sender_id\": \"USR101\",\n                \"receiver_id\": \"USR102\",\n                \"message\": \"\"\"你能帮我点一个\\\"玛格丽特披萨\\\"的外卖吗,商家是达美乐。\"\"\",\n                \"time\": \"2024-03-09\",\n            },\n            3: {\n                \"sender_id\": \"USR102\",\n                \"receiver_id\": \"USR103\",\n                \"message\": \"帮我查一些喜茶有哪些奶茶外卖，买一杯便宜些的奶茶。\"\n                \"买完以后记得回复我,回复的内容是（已经买好了）\",\n                \"time\": \"2023-12-05\",\n            },\n            4: {\n                \"sender_id\": \"USR103\",\n                \"receiver_id\": \"USR102\",\n                \"message\": \"No problem Helen, I can assist you.\",\n                \"time\": \"2024-09-09\",\n            },\n            5: {\n                \"sender_id\": \"USR104\",\n                \"receiver_id\": \"USR105\",\n                \"message\": \"Isaac, are you available for a call?\",\n                \"time\": \"2024-06-06\",\n            },\n            6: {\n                \"sender_id\": \"USR105\",\n                \"receiver_id\": \"USR104\",\n                \"message\": \"Yes Jack, let's do it in 30 minutes.\",\n                \"time\": \"2024-01-15\",\n            },\n        }\n\n        self.message_id_counter: int = 6\n\n    def get_state_dict(self) -> dict:\n        \"\"\"Get the current state dict of the MessageApi.\"\"\"\n\n        # To avoid the error in ACEBench dataset\n        inbox_state = {}\n        for key, value in self.inbox.items():\n            inbox_state[str(key)] = value\n\n        return {\n            \"MessageApi\": {\n                \"inbox\": inbox_state,\n            },\n        }\n\n    def send_message(\n        self,\n        sender_name: str,\n        receiver_name: str,\n        message: str,\n    ) -> dict[str, bool | str]:\n        \"\"\"将一条消息从一个用户发送给另一个用户。\n\n        Args:\n            sender_name (`str`):\n                发送消息的用户姓名。\n            receiver_name (`str`):\n                接收消息的用户姓名。\n            message (`str`):\n                要发送的消息内容。\n        \"\"\"\n        if not self.logged_in:\n            return {\"status\": False, \"message\": \"device未登录，无法发送短信\"}\n\n        if not self.wifi:\n            return {\"status\": False, \"message\": \"wifi关闭，此时不能发送信息\"}\n\n        if len(self.inbox) >= self.max_capacity:\n            return {\n                \"status\": False,\n                \"message\": \"内存容量不够了，你需要询问user删除哪一条短信。\",\n            }\n\n        # 验证发送者和接收者是否存在\n        if (\n            sender_name not in self.user_list\n            or receiver_name not in self.user_list\n        ):\n            return {\"status\": False, \"message\": \"发送者或接收者不存在\"}\n\n        sender_id = self.user_list[sender_name][\"user_id\"]\n        receiver_id = self.user_list[receiver_name][\"user_id\"]\n\n        # 将短信添加到inbox\n        self.message_id_counter += 1\n        self.inbox[self.message_id_counter] = {\n            \"sender_id\": sender_id,\n            \"receiver_id\": receiver_id,\n            \"message\": message,\n        }\n\n        return {\"status\": True, \"message\": f\"短信成功发送给{receiver_name}。\"}\n\n    def delete_message(self, message_id: int) -> dict[str, bool | str]:\n        \"\"\"根据消息 ID 删除一条消息。\n\n        Args:\n            message_id (`int`):\n                要删除的消息的 ID。\n        \"\"\"\n        if not self.logged_in:\n            return {\"status\": False, \"message\": \"device未登录，无法删除短信\"}\n        if message_id not in self.inbox:\n            return {\"status\": False, \"message\": \"短信ID不存在\"}\n\n        del self.inbox[message_id]\n        return {\"status\": True, \"message\": f\"短信ID {message_id} 已成功删除。\"}\n\n    def view_messages_between_users(\n        self,\n        sender_name: str,\n        receiver_name: str,\n    ) -> dict:\n        \"\"\"获取特定用户发送给另一个用户的所有消息。\n\n        Args:\n            sender_name (`str`):\n                发送消息的用户姓名。\n            receiver_name (`str`):\n                接收消息的用户姓名。\n        \"\"\"\n        if not self.logged_in:\n            return {\n                \"status\": False,\n                \"message\": \"device未登录，无法查看短信信息\",\n            }\n\n        if sender_name not in self.user_list:\n            return {\"status\": False, \"message\": \"发送者不存在\"}\n\n        if receiver_name not in self.user_list:\n            return {\"status\": False, \"message\": \"接收者不存在\"}\n\n        sender_id = self.user_list[sender_name][\"user_id\"]\n        receiver_id = self.user_list[receiver_name][\"user_id\"]\n        messages_between_users = []\n\n        # 遍历 inbox，找出 sender_id 发送给 receiver_id 的短信\n        for msg_id, msg_data in self.inbox.items():\n            if (\n                msg_data[\"sender_id\"] == sender_id\n                and msg_data[\"receiver_id\"] == receiver_id\n            ):\n                messages_between_users.append(\n                    {\n                        \"id\": msg_id,\n                        \"sender\": sender_name,\n                        \"receiver\": receiver_name,\n                        \"message\": msg_data[\"message\"],\n                    },\n                )\n\n        if not messages_between_users:\n            return {\"status\": False, \"message\": \"没有找到相关的短信记录\"}\n\n        return {\"status\": True, \"messages\": messages_between_users}\n\n    def search_messages(\n        self,\n        user_name: str,\n        keyword: str,\n    ) -> dict:\n        \"\"\"搜索特定用户消息中包含特定关键字的消息。\n\n        Args:\n            user_name (`str`):\n                要搜索消息的用户姓名。\n            keyword (`str`):\n                要在消息中搜索的关键字。\n        \"\"\"\n        if user_name not in self.user_list:\n            return {\"status\": False, \"message\": \"用户不存在\"}\n\n        user_id = self.user_list[user_name][\"user_id\"]\n        matched_messages = []\n\n        # 遍历 inbox，找到发送或接收中包含关键词的消息\n        for msg_id, msg_data in self.inbox.items():\n            if (\n                user_id in (msg_data[\"sender_id\"], msg_data[\"receiver_id\"])\n                and keyword.lower() in msg_data[\"message\"].lower()\n            ):\n                matched_messages.append(\n                    {\n                        \"id\": msg_id,\n                        \"sender_id\": msg_data[\"sender_id\"],\n                        \"receiver_id\": msg_data[\"receiver_id\"],\n                        \"message\": msg_data[\"message\"],\n                    },\n                )\n\n        if not matched_messages:\n            return {\"status\": False, \"message\": \"没有找到包含关键词的短信\"}\n\n        return {\"status\": True, \"messages\": matched_messages}\n\n    def get_all_message_times_with_ids(\n        self,\n    ) -> dict:\n        \"\"\"获取所有短信的时间以及对应的短信编号。\"\"\"\n        if not self.logged_in:\n            return {\n                \"status\": False,\n                \"message\": \"device未登录，获取所有短信的时间以及对应的短信编号。\",\n            }\n        message_times_with_ids = {\n            msg_id: msg_data[\"time\"] for msg_id, msg_data in self.inbox.items()\n        }\n        return message_times_with_ids\n\n    def get_latest_message_id(self) -> dict:\n        \"\"\"获取最近发送的消息的 ID。\"\"\"\n        if not self.logged_in:\n            return {\n                \"status\": False,\n                \"message\": \"device未登录，无法获取最新发送的短信ID。\",\n            }\n        if not self.inbox:\n            return {\"status\": False, \"message\": \"短信记录为空\"}\n\n        # 遍历所有短信，找出时间最新的短信\n        latest_message_id = None\n        latest_time = None\n\n        for message_id, message_data in self.inbox.items():\n            message_time = datetime.strptime(\n                str(message_data[\"time\"]),\n                \"%Y-%m-%d\",\n            )\n            if latest_time is None or message_time > latest_time:\n                latest_time = message_time\n                latest_message_id = message_id\n\n        return {\n            \"status\": True,\n            \"message\": f\"最新的短信ID是 {latest_message_id}\",\n            \"message_id\": latest_message_id,\n        }\n\n    def get_earliest_message_id(self) -> dict:\n        \"\"\"获取最早发送的消息的 ID。\"\"\"\n        if not self.logged_in:\n            return {\n                \"status\": False,\n                \"message\": \"device未登录，无法获取最早发送的短信ID\",\n            }\n        if not self.inbox:\n            return {\"status\": False, \"message\": \"短信记录为空\"}\n\n        # 遍历所有短信，找出时间最早的短信\n        earliest_message_id = None\n        earliest_time = None\n\n        for message_id, message_data in self.inbox.items():\n            message_time = datetime.strptime(\n                str(message_data[\"time\"]),\n                \"%Y-%m-%d\",\n            )\n            if earliest_time is None or message_time < earliest_time:\n                earliest_time = message_time\n                earliest_message_id = message_id\n\n        return {\n            \"status\": True,\n            \"message\": f\"最早的短信ID是 {earliest_message_id}\",\n            \"message_id\": earliest_message_id,\n        }\n"
  },
  {
    "path": "src/agentscope/evaluate/_ace_benchmark/_ace_tools_api/_reminder_api.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The reminder API in ACEBench simulation tools.\"\"\"\nfrom datetime import datetime\n\nfrom ._shared_state import SharedState\n\n\nclass ReminderApi(SharedState):\n    \"\"\"The reminder Api in the ACEBench evaluation.\"\"\"\n\n    tool_functions: list[str] = [\n        \"view_reminder_by_title\",\n        \"add_reminder\",\n        \"delete_reminder\",\n        \"view_all_reminders\",\n        \"mark_as_notified\",\n        \"search_reminders\",\n    ]\n\n    def __init__(self, share_state: dict) -> None:\n        \"\"\"Initialize the Reminder Api in the ACEBench evaluation.\"\"\"\n        super().__init__(share_state)\n\n        self.max_capacity = 6\n        self.reminder_list: dict[\n            int,\n            dict,\n        ] = {\n            1: {\n                \"reminder_id\": 1001,\n                \"title\": \"Doctor's Appointment\",\n                \"description\": \"Visit Dr. Smith for a checkup.\",\n                \"time\": \"2024-07-15 09:30\",\n                \"notified\": False,\n            },\n            2: {\n                \"reminder_id\": 1002,\n                \"title\": \"Team Meeting\",\n                \"description\": \"Monthly project review with the team.\",\n                \"time\": \"2024-07-17 11:00\",\n                \"notified\": False,\n            },\n            3: {\n                \"reminder_id\": 1003,\n                \"title\": \"To-do list\",\n                \"description\": '首先帮Frank在\"盒马生鲜\"点外卖，'\n                '需要定两个\"生鲜大礼包\"，再发短信告诉Frank：'\n                '\"购买商品的价格是()元\"。要把括号换成实际金额，'\n                \"保留一位小数。\",\n                \"time\": \"2024-07-16 11:00\",\n                \"notified\": False,\n            },\n        }\n        self.reminder_id_counter: int = 3\n\n    def get_state_dict(self) -> dict:\n        \"\"\"Get the current state dict of the ReminderApi.\"\"\"\n        return {\n            \"ReminderApi\": {\n                \"reminder_list\": self.reminder_list,\n            },\n        }\n\n    def _check_capacity(self) -> bool:\n        \"\"\"检查备忘录容量是否已满。\"\"\"\n        return len(self.reminder_list) >= self.max_capacity\n\n    def view_reminder_by_title(\n        self,\n        title: str,\n    ) -> dict[str, str | bool | dict[str, str | bool | datetime]]:\n        \"\"\"根据提醒的标题查看特定的提醒。\n\n        Args:\n            title (str): 提醒的标题。\n\n        Returns:\n            dict[str, str | bool | dict[str, str | bool | datetime]]:\n                包含查找状态和提醒详情的字典。\n        \"\"\"\n        if not self.logged_in:\n            return {\"status\": False, \"message\": \"device未登录，无法查看提醒\"}\n        for reminder in self.reminder_list.values():\n            if reminder[\"title\"] == title:\n                return {\"status\": True, \"reminder\": reminder}\n\n        return {\"status\": False, \"message\": f\"没有找到标题为 '{title}' 的提醒\"}\n\n    def add_reminder(\n        self,\n        title: str,\n        description: str,\n        time: datetime,\n    ) -> dict[str, bool | str]:\n        \"\"\"添加一个新的提醒。\n\n        Args:\n            title (str): 提醒标题。\n            description (str): 提醒描述。\n            time (datetime): 提醒时间, 一定遵循格式\"YYYY-MM-DD HH:MM\"。\n\n        Returns:\n            dict[str, bool | str]: 包含添加状态和结果的字典。\n        \"\"\"\n        if not self.logged_in:\n            return {\n                \"status\": False,\n                \"message\": \"device未登录，无法添加一个新的提醒\",\n            }\n        if self._check_capacity():\n            return {\"status\": False, \"message\": \"提醒容量已满，无法添加新的提醒\"}\n\n        self.reminder_id_counter += 1\n        reminder_id = self.reminder_id_counter\n        self.reminder_list[reminder_id] = {\n            \"reminder_id\": reminder_id,\n            \"title\": title,\n            \"description\": description,\n            \"time\": time,\n            \"notified\": False,\n        }\n        return {\"status\": True, \"message\": f\"提醒 '{title}' 已成功添加\"}\n\n    def delete_reminder(self, reminder_id: int) -> dict[str, bool | str]:\n        \"\"\"删除指定的提醒。\n\n        Args:\n            reminder_id (int): 要删除的提醒ID。\n\n        Returns:\n            dict[str, bool | str]: 包含删除状态和结果的字典。\n        \"\"\"\n        if not self.logged_in:\n            return {\"status\": False, \"message\": \"device未登录，无法删除指定的提醒\"}\n        if reminder_id not in self.reminder_list:\n            return {\"status\": False, \"message\": \"提醒ID不存在\"}\n\n        del self.reminder_list[reminder_id]\n        return {\"status\": True, \"message\": f\"提醒ID {reminder_id} 已成功删除\"}\n\n    def view_all_reminders(\n        self,\n    ) -> dict:\n        \"\"\"查看所有的提醒。\n\n        Returns:\n            dict:\n                包含所有提醒的字典列表。\n        \"\"\"\n        if not self.reminder_list:\n            return {\"status\": False, \"message\": \"没有任何提醒\"}\n\n        reminders = []\n        for reminder in self.reminder_list.values():\n            reminders.append(\n                {\n                    \"title\": reminder[\"title\"],\n                    \"description\": reminder[\"description\"],\n                    \"time\": reminder[\"time\"],\n                    \"notified\": reminder[\"notified\"],\n                },\n            )\n        return {\"status\": True, \"reminders\": reminders}\n\n    def mark_as_notified(\n        self,\n        reminder_id: int,\n    ) -> dict[str, bool | str]:\n        \"\"\"标记提醒为已通知。\n\n        Args:\n            reminder_id (int): 要标记为已通知的提醒ID。\n\n        Returns:\n            dict[str, bool | str]:: 包含操作结果的字典。\n        \"\"\"\n        if reminder_id not in self.reminder_list:\n            return {\"status\": False, \"message\": \"提醒ID不存在\"}\n\n        self.reminder_list[reminder_id][\"notified\"] = True\n        return {\"status\": True, \"message\": f\"提醒ID {reminder_id} 已标记为已通知\"}\n\n    def search_reminders(\n        self,\n        keyword: str,\n    ) -> dict:\n        \"\"\"根据关键词搜索提醒。\n\n        Args:\n            keyword (str): 搜索关键词。\n\n        Returns:\n            `dict`:\n                包含匹配提醒的字典列表。\n        \"\"\"\n        matched_reminders = []\n\n        for reminder in self.reminder_list.values():\n            if (\n                keyword.lower() in reminder[\"title\"].lower()\n                or keyword.lower() in reminder[\"description\"].lower()\n            ):\n                matched_reminders.append(\n                    {\n                        \"title\": reminder[\"title\"],\n                        \"description\": reminder[\"description\"],\n                        \"time\": reminder[\"time\"].strftime(\"%Y-%m-%d %H:%M\"),\n                    },\n                )\n\n        if not matched_reminders:\n            return {\"status\": False, \"message\": \"没有找到包含该关键词的提醒\"}\n\n        return {\"status\": True, \"reminders\": matched_reminders}\n"
  },
  {
    "path": "src/agentscope/evaluate/_ace_benchmark/_ace_tools_api/_shared_state.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The shared state class for ACEBench simulation tools.\"\"\"\n\n\nclass SharedState:\n    \"\"\"The sharing state class for ACEBench simulation tools.\"\"\"\n\n    def __init__(self, shared_state: dict) -> None:\n        \"\"\"Initialize the shared state\"\"\"\n        self._shared_state = shared_state\n\n    @property\n    def wifi(self) -> bool:\n        \"\"\"The WI-FI state\"\"\"\n        return self._shared_state[\"wifi\"]\n\n    @property\n    def logged_in(self) -> bool:\n        \"\"\"The logged in state\"\"\"\n        return self._shared_state[\"logged_in\"]\n"
  },
  {
    "path": "src/agentscope/evaluate/_ace_benchmark/_ace_tools_api/_travel_api.py",
    "content": "# -*- coding: utf-8 -*-\n# type: ignore\n# pylint: disable=too-many-lines\n# pylint: disable=too-many-statements\n# pylint: disable=too-many-branches\n# pylint: disable=too-many-statements\n# pylint: disable=too-many-return-statements\n\"\"\"The travel API for the ACEBench simulation tools in AgentScope.\"\"\"\n\nfrom datetime import datetime, timedelta\n\n\nclass TravelApi:\n    \"\"\"旅行预订系统类。\n\n    提供航班查询、用户认证、预订管理等功能的旅行系统。\n    支持直飞和中转航班查询、航班预订、预订修改和取消等功能。\n    \"\"\"\n\n    tool_functions: list[str] = [\n        \"get_user_details\",\n        \"get_flight_details\",\n        \"get_reservation_details\",\n        \"reserve_flight\",\n        \"cancel_reservation\",\n        \"modify_flight\",\n    ]\n\n    def __init__(self) -> None:\n        \"\"\"初始化旅行系统。\n\n        设置用户档案和航班信息，包含用户信息、航班数据和预订记录。\n        \"\"\"\n        # 初始化用户信息\n        self.users = {\n            \"user1\": {\n                \"user_name\": \"Eve\",\n                \"password\": \"password123\",\n                \"cash_balance\": 2000.0,\n                \"bank_balance\": 50000.0,\n                \"membership_level\": \"regular\",\n            },\n            \"user2\": {\n                \"user_name\": \"Frank\",\n                \"password\": \"password456\",\n                \"cash_balance\": 8000.0,\n                \"bank_balance\": 8000.0,\n                \"membership_level\": \"silver\",\n            },\n            \"user3\": {\n                \"user_name\": \"Grace\",\n                \"password\": \"password789\",\n                \"cash_balance\": 1000.0,\n                \"bank_balance\": 5000.0,\n                \"membership_level\": \"gold\",\n            },\n        }\n\n        # 初始化航班信息\n        self.flights = [\n            {\n                \"flight_no\": \"CA1234\",\n                \"origin\": \"北京\",\n                \"destination\": \"上海\",\n                \"depart_time\": \"2024-07-15 08:00:00\",\n                \"arrival_time\": \"2024-07-15 10:30:00\",\n                \"status\": \"available\",\n                \"seats_available\": 5,\n                \"economy_price\": 1200,\n                \"business_price\": 3000,\n            },\n            {\n                \"flight_no\": \"MU5678\",\n                \"origin\": \"上海\",\n                \"destination\": \"北京\",\n                \"depart_time\": \"2024-07-16 09:00:00\",\n                \"arrival_time\": \"2024-07-16 11:30:00\",\n                \"status\": \"available\",\n                \"seats_available\": 3,\n                \"economy_price\": 1900,\n                \"business_price\": 3000,\n            },\n            {\n                \"flight_no\": \"CZ4321\",\n                \"origin\": \"上海\",\n                \"destination\": \"北京\",\n                \"depart_time\": \"2024-07-16 20:00:00\",\n                \"arrival_time\": \"2024-07-16 22:00:00\",\n                \"status\": \"available\",\n                \"seats_available\": 8,\n                \"economy_price\": 2500,\n                \"business_price\": 4000,\n            },\n            {\n                \"flight_no\": \"CZ4352\",\n                \"origin\": \"上海\",\n                \"destination\": \"北京\",\n                \"depart_time\": \"2024-07-17 20:00:00\",\n                \"arrival_time\": \"2024-07-17 22:00:00\",\n                \"status\": \"available\",\n                \"seats_available\": 8,\n                \"economy_price\": 1600,\n                \"business_price\": 2500,\n            },\n            {\n                \"flight_no\": \"MU3561\",\n                \"origin\": \"北京\",\n                \"destination\": \"南京\",\n                \"depart_time\": \"2024-07-18 08:00:00\",\n                \"arrival_time\": \"2024-07-18 10:00:00\",\n                \"status\": \"available\",\n                \"seats_available\": 8,\n                \"economy_price\": 1500,\n                \"business_price\": 4000,\n            },\n            {\n                \"flight_no\": \"MU1566\",\n                \"origin\": \"北京\",\n                \"destination\": \"南京\",\n                \"depart_time\": \"2024-07-18 20:00:00\",\n                \"arrival_time\": \"2024-07-18 22:00:00\",\n                \"status\": \"available\",\n                \"seats_available\": 8,\n                \"economy_price\": 1500,\n                \"business_price\": 4000,\n            },\n            {\n                \"flight_no\": \"CZ1765\",\n                \"origin\": \"南京\",\n                \"destination\": \"深圳\",\n                \"depart_time\": \"2024-07-17 20:30:00\",\n                \"arrival_time\": \"2024-07-17 22:00:00\",\n                \"status\": \"available\",\n                \"seats_available\": 8,\n                \"economy_price\": 1500,\n                \"business_price\": 2500,\n            },\n            {\n                \"flight_no\": \"CZ1765\",\n                \"origin\": \"南京\",\n                \"destination\": \"深圳\",\n                \"depart_time\": \"2024-07-18 12:30:00\",\n                \"arrival_time\": \"2024-07-18 15:00:00\",\n                \"status\": \"available\",\n                \"seats_available\": 8,\n                \"economy_price\": 1500,\n                \"business_price\": 2500,\n            },\n            {\n                \"flight_no\": \"MH1765\",\n                \"origin\": \"厦门\",\n                \"destination\": \"成都\",\n                \"depart_time\": \"2024-07-17 12:30:00\",\n                \"arrival_time\": \"2024-07-17 15:00:00\",\n                \"status\": \"available\",\n                \"seats_available\": 8,\n                \"economy_price\": 1500,\n                \"business_price\": 2500,\n            },\n            {\n                \"flight_no\": \"MH2616\",\n                \"origin\": \"成都\",\n                \"destination\": \"厦门\",\n                \"depart_time\": \"2024-07-18 18:30:00\",\n                \"arrival_time\": \"2024-07-18 21:00:00\",\n                \"status\": \"available\",\n                \"seats_available\": 8,\n                \"economy_price\": 1500,\n                \"business_price\": 2500,\n            },\n            {\n                \"flight_no\": \"MH2616\",\n                \"origin\": \"成都\",\n                \"destination\": \"福州\",\n                \"depart_time\": \"2024-07-16 18:30:00\",\n                \"arrival_time\": \"2024-07-16 21:00:00\",\n                \"status\": \"available\",\n                \"seats_available\": 8,\n                \"economy_price\": 1500,\n                \"business_price\": 2500,\n            },\n        ]\n\n        # 初始化预订列表\n        self.reservations = [\n            {\n                \"reservation_id\": \"res_1\",\n                \"user_id\": \"user1\",\n                \"flight_no\": \"CA1234\",\n                \"payment_method\": \"bank\",\n                \"cabin\": \"经济舱\",\n                \"baggage\": 1,\n                \"origin\": \"北京\",\n                \"destination\": \"上海\",\n            },\n            {\n                \"reservation_id\": \"res_2\",\n                \"user_id\": \"user1\",\n                \"flight_no\": \"MU5678\",\n                \"payment_method\": \"bank\",\n                \"cabin\": \"商务舱\",\n                \"baggage\": 1,\n                \"origin\": \"上海\",\n                \"destination\": \"北京\",\n            },\n            {\n                \"reservation_id\": \"res_3\",\n                \"user_id\": \"user2\",\n                \"flight_no\": \"MH1765\",\n                \"payment_method\": \"bank\",\n                \"cabin\": \"商务舱\",\n                \"baggage\": 1,\n                \"origin\": \"厦门\",\n                \"destination\": \"成都\",\n            },\n            {\n                \"reservation_id\": \"res_4\",\n                \"user_id\": \"user2\",\n                \"flight_no\": \"MU2616\",\n                \"payment_method\": \"bank\",\n                \"cabin\": \"商务舱\",\n                \"baggage\": 1,\n                \"origin\": \"成都\",\n                \"destination\": \"厦门\",\n            },\n        ]\n\n    def get_state_dict(self) -> dict:\n        \"\"\"Get the current state dict of the TravelApi.\"\"\"\n        return {\n            \"Travel\": {\n                \"users\": self.users,\n                \"reservations\": self.reservations,\n            },\n        }\n\n    # 根据出发地和到达地查询航班\n\n    def get_flight_details(\n        self,\n        origin: str = None,\n        destination: str = None,\n    ) -> list[dict] | str:\n        \"\"\"根据出发地和到达地查询航班的基本信息。\n\n        Args:\n            origin (str, optional): 出发地城市名称。默认为None。\n            destination (str, optional): 目的地城市名称。默认为None。\n\n        Returns:\n            list[dict] | str: 符合条件的航班列表或无航班的提示信息。\n        \"\"\"\n        flights = self.flights\n\n        # 过滤出发地\n        if origin:\n            flights = [\n                flight for flight in flights if flight[\"origin\"] == origin\n            ]\n\n        # 过滤到达地\n        if destination:\n            flights = [\n                flight\n                for flight in flights\n                if flight[\"destination\"] == destination\n            ]\n        if len(flights) == 0:\n            return \"没有符合条件的直达航班\"\n        # 返回查询结果\n        return [\n            {\n                \"flight_no\": flight[\"flight_no\"],\n                \"origin\": flight[\"origin\"],\n                \"destination\": flight[\"destination\"],\n                \"depart_time\": flight[\"depart_time\"],\n                \"arrival_time\": flight[\"arrival_time\"],\n                \"status\": flight[\"status\"],\n                \"seats_available\": flight[\"seats_available\"],\n                \"economy_price\": flight[\"economy_price\"],\n                \"business_price\": flight[\"business_price\"],\n            }\n            for flight in flights\n        ]\n\n    def get_user_details(self, user_id: str, password: str) -> dict:\n        \"\"\"根据用户名和密码查询用户信息。\n\n        Args:\n            user_id (str): 用户ID。\n            password (str): 用户密码。\n\n        Returns:\n            dict: 用户信息字典（不包含密码）或错误信息。\n        \"\"\"\n        user = self.users.get(user_id)\n        if user and user[\"password\"] == password:\n            return {\n                key: value for key, value in user.items() if key != \"password\"\n            }\n        return {\"status\": \"error\", \"message\": \"用户名或密码不正确\"}\n\n    def get_reservation_details(\n        self,\n        reservation_id: str = None,\n        user_id: str = None,\n    ) -> list[dict] | dict:\n        \"\"\"根据预订ID或用户ID查询预订信息，包括对应航班的基本信息。\n\n        Args:\n            reservation_id (str, optional): 预订ID。默认为None。\n            user_id (str, optional): 用户ID。默认为None。\n\n        Returns:\n            `list[dict] | dict`:\n                详细预订信息列表或错误信息字典。\n        \"\"\"\n        # 根据预订ID或用户ID筛选预订信息\n        if reservation_id:\n            reservations = [\n                reservation\n                for reservation in self.reservations\n                if reservation[\"reservation_id\"] == reservation_id\n            ]\n        elif user_id:\n            reservations = [\n                reservation\n                for reservation in self.reservations\n                if reservation[\"user_id\"] == user_id\n            ]\n        else:\n            return {\"status\": \"error\", \"message\": \"请提供有效的预订ID或用户ID\"}\n\n        # 对每个预订，附加航班信息\n        detailed_reservations = []\n        for reservation in reservations:\n            flight_info = next(\n                (\n                    flight\n                    for flight in self.flights\n                    if flight[\"flight_no\"] == reservation[\"flight_no\"]\n                ),\n                None,\n            )\n            detailed_reservation = {**reservation, \"flight_info\": flight_info}\n            detailed_reservations.append(detailed_reservation)\n\n        return detailed_reservations\n\n    def authenticate_user(self, user_id: str, password: str) -> dict:\n        \"\"\"验证用户身份。\n\n        Args:\n            user_id (str): 用户ID。\n            password (str): 用户密码。\n\n        Returns:\n            `dict`:\n                用户信息字典或错误信息字典。\n        \"\"\"\n        user = self.users.get(user_id)\n        if user and user[\"password\"] == password:\n            return user\n        return {\"status\": \"error\", \"message\": \"用户名或密码不正确\"}\n\n    def get_baggage_allowance(\n        self,\n        membership_level: str,\n        cabin_class: str,\n    ) -> int:\n        \"\"\"获取用户基于会员等级和舱位的免费托运行李限额。\n\n        Args:\n            membership_level (str): 会员等级 (\"regular\", \"silver\", \"gold\")。\n            cabin_class (str): 舱位 (\"基础经济舱\", \"经济舱\", \"商务舱\")。\n\n        Returns:\n            int: 免费托运行李数量。\n        \"\"\"\n        allowance = {\n            \"regular\": {\"经济舱\": 1, \"商务舱\": 2},\n            \"silver\": {\"经济舱\": 2, \"商务舱\": 3},\n            \"gold\": {\"经济舱\": 3, \"商务舱\": 3},\n        }\n        return allowance.get(membership_level, {}).get(cabin_class, 0)\n\n    def find_transfer_flights(\n        self,\n        origin_city: str,\n        transfer_city: str,\n        destination_city: str,\n    ) -> list[dict] | str:\n        \"\"\"查找从出发城市到目的地城市的中转航班。\n\n        确保第一班航班降落时间早于第二班航班起飞时间。\n\n        Args:\n            origin_city (str): 出发城市。\n            transfer_city (str): 中转城市。\n            destination_city (str): 到达城市。\n\n        Returns:\n            list[dict] | str:\n                满足条件的中转航班列表，每个航班包含两段航程的信息，或无航班提示。\n        \"\"\"\n        # 获取从出发城市到中转城市的航班\n        first_leg_flights: list[dict] = [\n            flight\n            for flight in self.flights\n            if flight[\"origin\"] == origin_city\n            and flight[\"destination\"] == transfer_city\n            and flight[\"status\"] == \"available\"\n        ]\n\n        # 获取从中转城市到目的地城市的航班\n        second_leg_flights = [\n            flight\n            for flight in self.flights\n            if flight[\"origin\"] == transfer_city\n            and flight[\"destination\"] == destination_city\n            and flight[\"status\"] == \"available\"\n        ]\n\n        # 存储符合条件的中转航班\n        transfer_flights = []\n\n        # 遍历第一段航班和第二段航班，查找符合时间条件的组合\n        for first_flight in first_leg_flights:\n            first_arrival = datetime.strptime(\n                first_flight[\"arrival_time\"],\n                \"%Y-%m-%d %H:%M:%S\",\n            )\n\n            for second_flight in second_leg_flights:\n                second_departure = datetime.strptime(\n                    str(second_flight[\"depart_time\"]),\n                    \"%Y-%m-%d %H:%M:%S\",\n                )\n\n                # 检查第一班航班降落时间早于第二班航班起飞时间\n                if first_arrival < second_departure:\n                    transfer_flights.append(\n                        {\n                            \"first_leg\": first_flight,\n                            \"second_leg\": second_flight,\n                        },\n                    )\n\n        # 返回符合条件的中转航班列表\n        if transfer_flights:\n            return transfer_flights\n        else:\n            return \"未找到符合条件的中转航班。\"\n\n    def calculate_baggage_fee(\n        self,\n        membership_level: str,\n        cabin_class: str,\n        baggage_count: int,\n    ) -> float:\n        \"\"\"计算行李费用。\n\n        Args:\n            membership_level (str): 会员等级。\n            cabin_class (str): 舱位等级。\n            baggage_count (int): 行李数量。\n\n        Returns:\n            float: 额外行李费用。\n        \"\"\"\n        free_baggage = {\n            \"regular\": {\"经济舱\": 1, \"商务舱\": 2},\n            \"silver\": {\"经济舱\": 2, \"商务舱\": 3},\n            \"gold\": {\"经济舱\": 3, \"商务舱\": 3},\n        }\n        free_limit = free_baggage[membership_level][cabin_class]\n        additional_baggage = max(baggage_count - free_limit, 0)\n        return additional_baggage * 50\n\n    def update_balance(\n        self,\n        user: dict,\n        payment_method: str,\n        amount: float,\n    ) -> bool:\n        \"\"\"更新用户的余额。\n\n        Args:\n            user (dict): 用户信息字典。\n            payment_method (str): 支付方式（\"cash\" 或 \"bank\"）。\n            amount (float): 更新金额（正数表示增加，负数表示减少）。\n\n        Returns:\n            bool: 如果余额充足且更新成功，返回 True，否则返回 False。\n        \"\"\"\n        if payment_method == \"cash\":\n            if user[\"cash_balance\"] + amount < 0:\n                return False  # 余额不足\n            user[\"cash_balance\"] += amount\n        elif payment_method == \"bank\":\n            if user[\"bank_balance\"] + amount < 0:\n                return False  # 余额不足\n            user[\"bank_balance\"] += amount\n        return True\n\n    def reserve_flight(\n        self,\n        user_id: str,\n        password: str,\n        flight_no: str,\n        cabin: str,\n        payment_method: str,\n        baggage_count: int,\n    ) -> str:\n        \"\"\"预订航班。\n\n        Args:\n            user_id (str): 用户ID。\n            password (str): 用户密码。\n            flight_no (str): 航班号。\n            cabin (str): 舱位等级。\n            payment_method (str): 支付方式。\n            baggage_count (int): 行李数量。\n\n        Returns:\n            str: 预订结果信息。\n        \"\"\"\n        user = self.authenticate_user(user_id, password)\n        if not user:\n            return \"认证失败，请检查用户ID和密码。\"\n\n        # 检查航班和座位\n        flight = next(\n            (\n                f\n                for f in self.flights\n                if f[\"flight_no\"] == flight_no and f[\"status\"] == \"available\"\n            ),\n            None,\n        )\n\n        # 计算航班价格\n        price: int = (\n            flight[\"economy_price\"]\n            if cabin == \"经济舱\"\n            else flight[\"business_price\"]\n        )\n        total_cost = price\n\n        # 计算行李费用\n        baggage_fee = self.calculate_baggage_fee(\n            user[\"membership_level\"],\n            cabin,\n            baggage_count,\n        )\n        total_cost += baggage_fee\n\n        # 检查支付方式\n        if payment_method not in [\"cash\", \"bank\"]:\n            return \"支付方式无效\"\n\n        # 更新预定后的余额\n        if payment_method == \"cash\":\n            if total_cost > self.users.get(user_id)[\"cash_balance\"]:\n                return \"cash余额不足，请考虑换一种支付方式\"\n            self.users.get(user_id)[\"cash_balance\"] -= total_cost\n        else:\n            if total_cost > self.users.get(user_id)[\"bank_balance\"]:\n                return \"bank余额不足，请考虑换一种支付方式\"\n            self.users.get(user_id)[\"bank_balance\"] -= total_cost\n\n        # 更新航班信息并生成预订\n        flight[\"seats_available\"] -= 1\n        reservation_id = f\"res_{len(self.reservations) + 1}\"\n        reservation = {\n            \"reservation_id\": reservation_id,\n            \"user_id\": user_id,\n            \"flight_no\": flight_no,\n            \"payment_method\": payment_method,\n            \"cabin\": cabin,\n            \"baggage\": baggage_count,\n        }\n        self.reservations.append(reservation)\n\n        return f\"预订成功，预订号：{reservation_id}，\" f\"总费用：{total_cost}元（包含行李费用）。\"\n\n    def modify_flight(\n        self,\n        user_id: str,\n        reservation_id: str,\n        new_flight_no: str = None,\n        new_cabin: str = None,\n        add_baggage: int = 0,\n        new_payment_method: str = None,\n    ) -> str:\n        \"\"\"修改航班预订，包括更改航班、舱位和行李。\n\n        Args:\n            user_id (str): 用户ID。\n            reservation_id (str): 预订ID。\n            new_flight_no (str, optional): 新的航班号。默认为None。\n            new_cabin (str, optional): 新的舱位。默认为None。\n            add_baggage (int, optional): 新增托运行李的数量。默认为0。\n            new_payment_method (str, optional): 新的付款方式。默认为None。\n\n        Returns:\n            str: 修改结果信息。\n        \"\"\"\n        # 获取对应的预订\n        reservation = next(\n            (\n                r\n                for r in self.reservations\n                if r[\"reservation_id\"] == reservation_id\n                and r[\"user_id\"] == user_id\n            ),\n            None,\n        )\n        if not reservation:\n            return \"预订未找到或用户ID不匹配。\"\n\n        # 检查当前预订的航班信息\n        current_flight = next(\n            (\n                f\n                for f in self.flights\n                if f[\"flight_no\"] == reservation[\"flight_no\"]\n            ),\n            None,\n        )\n        if not current_flight:\n            return \"航班信息未找到。\"\n\n        # 获取原始支付方式或新提供的支付方式\n        payment_method = (\n            new_payment_method\n            if new_payment_method\n            else reservation[\"payment_method\"]\n        )\n        user = self.users[user_id]\n        if not user:\n            return \"用户信息未找到。\"\n\n        # 存储处理结果\n        result_messages = []\n\n        if new_flight_no and new_flight_no != reservation[\"flight_no\"]:\n            # 更新航班号（若提供）但必须匹配出发地和目的地\n            new_flight = next(\n                (f for f in self.flights if f[\"flight_no\"] == new_flight_no),\n                None,\n            )\n            if (\n                new_flight\n                and new_flight[\"origin\"] == current_flight[\"origin\"]\n                and new_flight[\"destination\"] == current_flight[\"destination\"]\n            ):\n                reservation[\"flight_no\"] = new_flight_no\n                result_messages.append(\"航班号已更改。\")\n            else:\n                return \"航班更改失败：新的航班号无效或目的地不匹配。\"\n\n        # 更新舱位（若提供）并计算价格差价\n        if new_cabin and new_cabin != reservation.get(\"cabin\"):\n            price_difference = self.calculate_price_difference(\n                current_flight,\n                reservation[\"cabin\"],\n                new_cabin,\n            )\n            reservation[\"cabin\"] = new_cabin\n            if price_difference > 0:\n                # 扣除差价\n                if self.update_balance(\n                    user,\n                    payment_method,\n                    -price_difference,\n                ):\n                    result_messages.append(\n                        f\"舱位更改成功。已支付差价: {price_difference}。\",\n                    )\n                else:\n                    result_messages.append(\"余额不足，无法支付舱位差价。\")\n            elif price_difference < 0:\n                # 退款\n                self.update_balance(user, payment_method, -price_difference)\n                result_messages.append(f\"舱位更改成功。已退款差价: {-price_difference}。\")\n\n        # 增加托运行李，检查免费限额和计算费用\n        if add_baggage > 0:\n            membership = user[\"membership_level\"]\n            max_free_baggage = self.get_baggage_allowance(\n                membership,\n                reservation[\"cabin\"],\n            )\n            current_baggage = reservation.get(\"baggage\", 0)\n            total_baggage = current_baggage + add_baggage\n            extra_baggage = max(0, total_baggage - max_free_baggage)\n            baggage_cost = extra_baggage * 50\n            if baggage_cost > 0:\n                # 扣除行李费用\n                if self.update_balance(user, payment_method, -baggage_cost):\n                    result_messages.append(\n                        f\"行李已增加。需支付额外费用: {baggage_cost}。\",\n                    )\n                else:\n                    result_messages.append(\"余额不足，无法支付额外行李费用。\")\n            reservation[\"baggage\"] = total_baggage\n\n        # 返回最终结果\n        if not result_messages:\n            result_messages.append(\"修改完成，无需额外费用。\")\n        return \" \".join(result_messages)\n\n    def cancel_reservation(\n        self,\n        user_id: str,\n        reservation_id: str,\n        reason: str,\n    ) -> str:\n        \"\"\"取消预订。\n\n        Args:\n            user_id (str): 用户ID。\n            reservation_id (str): 预订ID。\n            reason (str): 取消原因。\n\n        Returns:\n            str: 取消结果信息。\n        \"\"\"\n        # 设置默认当前时间为 2024年7月14日早上6点\n        current_time = datetime(2024, 7, 14, 6, 0, 0)\n\n        # 验证用户和预订是否存在\n        user = self.users.get(user_id, None)\n        if not user:\n            return \"用户ID无效。\"\n\n        reservation = next(\n            (\n                r\n                for r in self.reservations\n                if r[\"reservation_id\"] == reservation_id\n                and r[\"user_id\"] == user_id\n            ),\n            None,\n        )\n        if not reservation:\n            return \"预订ID无效或与该用户无关。\"\n\n        # 检查航班信息是否存在\n        flight = next(\n            (\n                f\n                for f in self.flights\n                if f[\"flight_no\"] == reservation[\"flight_no\"]\n            ),\n            None,\n        )\n        if not flight:\n            return \"航班信息无效。\"\n\n        # 检查航班是否已起飞\n        depart_time = datetime.strptime(\n            flight[\"depart_time\"],\n            \"%Y-%m-%d %H:%M:%S\",\n        )\n        if current_time > depart_time:\n            return \"航段已使用，无法取消。\"\n\n        # 计算距离出发时间\n        time_until_departure = depart_time - current_time\n        cancel_fee = 0\n        refund_amount = 0\n\n        # 获取航班价格\n        flight_price = (\n            flight[\"economy_price\"]\n            if reservation[\"cabin\"] == \"经济舱\"\n            else flight[\"business_price\"]\n        )\n\n        # 取消政策及退款计算\n        if reason == \"航空公司取消航班\":\n            # 航空公司取消航班，全额退款\n            refund_amount = flight_price\n            self.process_refund(user, refund_amount)\n            return f\"航班已取消，您的预订将被免费取消，已退款{refund_amount}元。\"\n\n        elif time_until_departure > timedelta(days=1):\n            # 离出发时间超过24小时免费取消\n            refund_amount = flight_price\n            self.process_refund(user, refund_amount)\n            return f\"距离出发时间超过24小时，免费取消成功，已退款{refund_amount}元。\"\n\n        else:\n            # 若不符合免费取消条件，可根据需求设置取消费\n            cancel_fee = flight_price * 0.1  # 假设取消费为票价的10%\n            refund_amount = flight_price - cancel_fee\n            self.process_refund(user, refund_amount)\n            return f\"距离出发时间不足24小时，已扣除取消费{cancel_fee}元，退款{refund_amount}元。\"\n\n    def process_refund(self, user: dict, amount: float) -> str:\n        \"\"\"将退款金额添加到用户的现金余额中。\n\n        Args:\n            user (dict): 用户信息字典。\n            amount (float): 退款金额。\n        \"\"\"\n        user[\"cash_balance\"] += amount\n        return f\"已成功处理退款，{user['user_name']}的现金余额增加了{amount}元。\"\n\n    def calculate_price_difference(\n        self,\n        flight: dict,\n        old_cabin: str,\n        new_cabin: str,\n    ) -> float:\n        \"\"\"计算舱位价格差异。\n\n        Args:\n            flight (dict): 航班信息字典。\n            old_cabin (str): 原舱位等级。\n            new_cabin (str): 新舱位等级。\n\n        Returns:\n            float: 价格差异（正数表示需支付差价，负数表示退款）。\n        \"\"\"\n        cabin_prices = {\n            \"经济舱\": flight[\"economy_price\"],\n            \"商务舱\": flight[\"business_price\"],\n        }\n        old_price = cabin_prices.get(old_cabin, 0)\n        new_price = cabin_prices.get(new_cabin, 0)\n        return new_price - old_price\n"
  },
  {
    "path": "src/agentscope/evaluate/_ace_benchmark/_ace_tools_zh.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The Chinese tools for ACEBench evaluation.\"\"\"\nfrom functools import wraps\nfrom typing import Callable, Any\n\nfrom ._ace_tools_api import (\n    ReminderApi,\n    FoodPlatformApi,\n    TravelApi,\n    MessageApi,\n)\nfrom ...message import TextBlock\nfrom ...tool import ToolResponse\n\n\ndef _tool_function_wrapper(get_tool_function: Callable) -> Callable:\n    \"\"\"Wrap the tool function result to be ToolResponse.\"\"\"\n\n    @wraps(get_tool_function)\n    def wrapper(self: \"ACEPhone\", name: str) -> Callable:\n        \"\"\"Wrap the tool function to return ToolResponse.\"\"\"\n        tool_function = get_tool_function(self, name)\n\n        @wraps(tool_function)\n        def wrapper_tool_function(*args: Any, **kwargs: Any) -> ToolResponse:\n            \"\"\"The wrapped tool function\"\"\"\n            res = tool_function(*args, **kwargs)\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=str(res),\n                    ),\n                ],\n            )\n\n        return wrapper_tool_function\n\n    return wrapper\n\n\nclass ACEPhone:\n    \"\"\"Simulate a user phone with various apps and functionalities in\n    ACEBench. The code is implemented with reference to the\n    `ACEBench <https://github.com/ACEBench/ACEBench>`_.\n    \"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"Initialize the shared state and apps for the ACEPhone.\"\"\"\n        self._state = {\n            \"wifi\": False,\n            \"logged_in\": False,\n        }\n        self._message_app = MessageApi(self._state)\n        self._reminder_app = ReminderApi(self._state)\n        self._food_platform_app = FoodPlatformApi(self._state)\n        self._travel = TravelApi()\n\n    def turn_on_wifi(self) -> dict[str, bool | str]:\n        \"\"\"开启WiFi连接。\"\"\"\n        self._state[\"wifi\"] = True\n        return {\"status\": True, \"message\": \"wifi已经打开\"}\n\n    def login_device(self) -> dict[str, bool | str]:\n        \"\"\"登录设备。\"\"\"\n        self._state[\"logged_in\"] = True\n        return {\"status\": True, \"message\": \"设备已经登录\"}\n\n    def load_initial_config(self, initial_config: dict) -> None:\n        \"\"\"Load the initial config from the application configuration.\"\"\"\n        # Empty initial config\n        if len(initial_config) == 0:\n            return\n\n        # Fix the typo in ACEBench by renaming \"Baspi\" to \"BaseApi\"\n        if \"Baspi\" in initial_config:\n            initial_config[\"BaseApi\"] = initial_config.pop(\"Baspi\")\n\n        # Verify state\n        assert (\n            \"BaseApi\" in initial_config\n            and \"wifi\" in initial_config[\"BaseApi\"]\n            and \"logged_in\" in initial_config[\"BaseApi\"]\n        ), f\"Invalid initial config: {initial_config}\"\n\n        self._state[\"wifi\"] = initial_config[\"BaseApi\"][\"wifi\"]\n        self._state[\"logged_in\"] = initial_config[\"BaseApi\"][\"logged_in\"]\n\n    def get_current_state(self) -> list[dict]:\n        \"\"\"Follow ACEBench to get the current state of the ACEPhone.\"\"\"\n        return [\n            {\"BaseApi\": self._state},\n            self._message_app.get_state_dict(),\n            self._reminder_app.get_state_dict(),\n            self._food_platform_app.get_state_dict(),\n            self._travel.get_state_dict(),\n        ]\n\n    @_tool_function_wrapper\n    def get_tool_function(self, name: str) -> Callable:\n        \"\"\"Get a tool function by name.\"\"\"\n        if name in [\n            \"turn_on_wifi\",\n            \"login_device\",\n        ]:\n            return getattr(self, name)\n\n        if name in self._message_app.tool_functions:\n            return getattr(self._message_app, name)\n\n        if name in self._food_platform_app.tool_functions:\n            return getattr(self._food_platform_app, name)\n\n        if name in self._reminder_app.tool_functions:\n            return getattr(self._reminder_app, name)\n\n        if name in self._travel.tool_functions:\n            return getattr(self._travel, name)\n\n        raise ValueError(\n            f\"Tool function '{name}' not found in ACEPhone.\",\n        )\n"
  },
  {
    "path": "src/agentscope/evaluate/_benchmark_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The base class for benchmark evaluation.\"\"\"\nfrom abc import ABC, abstractmethod\nfrom typing import Generator\n\nfrom ._task import Task\n\n\nclass BenchmarkBase(ABC):\n    \"\"\"The base class for benchmark evaluation.\"\"\"\n\n    name: str\n    \"\"\"The name of the benchmark.\"\"\"\n\n    description: str\n    \"\"\"The description of the benchmark.\"\"\"\n\n    def __init__(self, name: str, description: str) -> None:\n        \"\"\"Initialize the benchmark.\n\n        Args:\n            name (`str`):\n                The name of the benchmark.\n            description (`str`):\n                A brief description of the benchmark.\n        \"\"\"\n        self.name = name\n        self.description = description\n\n    @abstractmethod\n    def __iter__(self) -> Generator[Task, None, None]:\n        \"\"\"Iterate over the benchmark.\"\"\"\n        raise NotImplementedError(\"Subclasses must implement this method.\")\n\n    @abstractmethod\n    def __len__(self) -> int:\n        \"\"\"Get the length of the benchmark.\"\"\"\n        raise NotImplementedError(\"Subclasses must implement this method.\")\n\n    @abstractmethod\n    def __getitem__(self, index: int) -> Task:\n        \"\"\"Get the task at the given index.\"\"\"\n        raise NotImplementedError(\"Subclasses must implement this method.\")\n"
  },
  {
    "path": "src/agentscope/evaluate/_evaluator/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The evaluator module in AgentScope.\"\"\"\n\nfrom ._evaluator_base import EvaluatorBase\nfrom ._ray_evaluator import RayEvaluator\nfrom ._general_evaluator import GeneralEvaluator\n\n__all__ = [\n    \"EvaluatorBase\",\n    \"RayEvaluator\",\n    \"GeneralEvaluator\",\n]\n"
  },
  {
    "path": "src/agentscope/evaluate/_evaluator/_evaluator_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The base class for evaluator in evaluation.\"\"\"\nimport collections\nimport json\nfrom abc import abstractmethod\nfrom dataclasses import asdict\nfrom typing import Callable, Coroutine, Any\nfrom collections import defaultdict\n\nfrom .._solution import SolutionOutput\nfrom .._task import Task\nfrom .._benchmark_base import BenchmarkBase\nfrom .._evaluator_storage import EvaluatorStorageBase\nfrom .._metric_base import MetricType\nfrom ..._utils._common import _get_timestamp\n\n\nclass EvaluatorBase:\n    \"\"\"The class that runs the evaluation process.\"\"\"\n\n    def __init__(\n        self,\n        name: str,\n        benchmark: BenchmarkBase,\n        n_repeat: int,\n        storage: EvaluatorStorageBase,\n    ) -> None:\n        \"\"\"Initialize the evaluator.\n\n        Args:\n            name (`str`):\n                The name of this evaluator.\n            benchmark: (`BenchmarkBase`):\n                A benchmark instance inheriting from `BenchmarkBase` that\n                defines the evaluation dataset.\n            n_repeat (`int`):\n                How many times to repeat the evaluation for each task.\n            storage (`EvaluatorStorageBase`):\n                A instance inheriting from the child class of\n                `EvaluatorStorageBase` that supports storing and loading\n                solution output and evaluation results.\n        \"\"\"\n        self.name = name\n        self.benchmark = benchmark\n        self.n_repeat = n_repeat\n        self.storage = storage\n\n    @abstractmethod\n    async def run(\n        self,\n        solution: Callable[\n            [Task, Callable],\n            Coroutine[Any, Any, SolutionOutput],\n        ],\n    ) -> None:\n        \"\"\"Run the evaluation and return the results.\n\n        Args:\n            solution (`Callable[[Task, Callable], Coroutine[Any, Any, \\\n            SolutionOutput]]`):\n                A async function that takes a `Task` instance and a pre-hook\n                as input and returns a `SolutionOutput` instance.\n        \"\"\"\n\n    async def _save_evaluation_meta(self) -> None:\n        \"\"\"Save the evaluation meta information.\"\"\"\n        self.storage.save_evaluation_meta(\n            {\n                \"evaluation_name\": self.name,\n                \"created_at\": _get_timestamp(),\n                \"total_repeats\": self.n_repeat,\n                \"benchmark\": {\n                    \"name\": self.benchmark.name,\n                    \"description\": self.benchmark.description,\n                    \"total_tasks\": len(self.benchmark),\n                },\n                \"schema_version\": 1,\n            },\n        )\n\n    async def _save_task_meta(self, task: Task) -> None:\n        \"\"\"Save the task meta information.\n\n        Args:\n            task (`Task`):\n                The task instance.\n        \"\"\"\n        meta_info = asdict(task)\n        meta_info.pop(\"metadata\")\n        self.storage.save_task_meta(\n            task.id,\n            meta_info,\n        )\n\n    # pylint: disable=too-many-branches, too-many-statements\n    async def aggregate(self) -> None:\n        \"\"\"Aggregate the evaluation results and save an overall result.\"\"\"\n        meta_info: dict = {\n            \"total_tasks\": len(self.benchmark),\n            \"total_repeats\": self.n_repeat,\n            \"total_stats\": {\n                \"llm\": defaultdict(int),\n                \"agent\": 0,\n                \"tool\": defaultdict(int),\n                \"embedding\": defaultdict(int),\n                \"chat_usage\": {},\n            },\n            \"repeats\": {},\n            \"schema_version\": 1,\n        }\n\n        for repeat_index in range(self.n_repeat):\n            repeat_id = str(repeat_index)\n            current_repeat: dict = {\n                \"completed_tasks\": 0,\n                \"incomplete_tasks\": 0,\n                \"metrics\": {},\n                \"completed_ids\": [],\n                \"incomplete_ids\": [],\n                \"stats\": {\n                    \"llm\": defaultdict(int),\n                    \"agent\": 0,\n                    \"tool\": defaultdict(int),\n                    \"embedding\": defaultdict(int),\n                    \"chat_usage\": {},\n                },\n            }\n            for task in self.benchmark:\n                current_stats = self.storage.get_solution_stats(\n                    task.id,\n                    repeat_id,\n                )\n\n                # llm\n                for model_name, cnt in current_stats.get(\"llm\", {}).items():\n                    current_repeat[\"stats\"][\"llm\"][model_name] += cnt\n\n                # agent\n                current_repeat[\"stats\"][\"agent\"] += current_stats.get(\n                    \"agent\",\n                    0,\n                )\n\n                # tool\n                for tool_name, cnt in current_stats.get(\"tool\", {}).items():\n                    current_repeat[\"stats\"][\"tool\"][tool_name] += cnt\n\n                # embedding\n                for embedding_model, cnt in current_stats.get(\n                    \"embedding\",\n                    {},\n                ).items():\n                    current_repeat[\"stats\"][\"embedding\"][\n                        embedding_model\n                    ] += cnt\n\n                # chat usage\n                for model_name, usage in current_stats.get(\n                    \"chat_usage\",\n                    {},\n                ).items():\n                    if model_name not in current_repeat[\"stats\"][\"chat_usage\"]:\n                        current_repeat[\"stats\"][\"chat_usage\"][\n                            model_name\n                        ] = defaultdict(int)\n                    current_repeat[\"stats\"][\"chat_usage\"][model_name][\n                        \"input_tokens\"\n                    ] += usage.get(\"input_tokens\", 0)\n                    current_repeat[\"stats\"][\"chat_usage\"][model_name][\n                        \"output_tokens\"\n                    ] += usage.get(\"output_tokens\", 0)\n\n                for metric in task.metrics:\n                    # Create a new dict in aggregated_result\n                    if metric.name not in current_repeat[\"metrics\"]:\n                        current_repeat[\"metrics\"][metric.name] = {\n                            \"type\": metric.metric_type,\n                            \"involved_tasks\": 0,\n                            \"completed_tasks\": 0,\n                            \"incomplete_tasks\": 0,\n                            \"aggregation\": {},\n                            \"distribution\": collections.defaultdict(list),\n                        }\n\n                    # Record the submitted task\n                    current_repeat[\"metrics\"][metric.name][\n                        \"involved_tasks\"\n                    ] += 1\n\n                    # Not finished\n                    if not self.storage.evaluation_result_exists(\n                        task.id,\n                        repeat_id,\n                        metric.name,\n                    ):\n                        if task.id not in current_repeat[\"incomplete_ids\"]:\n                            current_repeat[\"incomplete_tasks\"] += 1\n                            current_repeat[\"incomplete_ids\"].append(task.id)\n                        current_repeat[\"metrics\"][metric.name][\n                            \"incomplete_tasks\"\n                        ] += 1\n                        continue\n\n                    if task.id not in current_repeat[\"completed_ids\"]:\n                        current_repeat[\"completed_tasks\"] += 1\n                        current_repeat[\"completed_ids\"].append(task.id)\n                    current_repeat[\"metrics\"][metric.name][\n                        \"completed_tasks\"\n                    ] += 1\n\n                    # Get the evaluation result\n                    eval_result = self.storage.get_evaluation_result(\n                        task.id,\n                        repeat_id,\n                        metric.name,\n                    )\n\n                    # Record the metric result\n                    if metric.metric_type == MetricType.CATEGORY:\n                        current_repeat[\"metrics\"][metric.name][\"distribution\"][\n                            eval_result.result\n                        ].append(\n                            task.id,\n                        )\n\n                    elif metric.metric_type == MetricType.NUMERICAL:\n                        current_repeat[\"metrics\"][metric.name][\"distribution\"][\n                            task.id\n                        ] = eval_result.result\n\n            print(\"Repeat ID:\", repeat_id)\n\n            for metric, value in current_repeat[\"metrics\"].items():\n                print(\"\\tMetric:\", metric)\n                print(\"\\t\\tType:\", value[\"type\"])\n                print(\"\\t\\tInvolved tasks:\", value[\"involved_tasks\"])\n                print(\"\\t\\tCompleted tasks:\", value[\"completed_tasks\"])\n                print(\"\\t\\tIncomplete tasks:\", value[\"incomplete_tasks\"])\n\n                if value[\"type\"] == MetricType.CATEGORY:\n                    # Count the distribution\n                    for category, task_ids in value[\"distribution\"].items():\n                        value[\"aggregation\"][category] = (\n                            len(task_ids) * 1.0 / value[\"involved_tasks\"]\n                        )\n\n                elif value[\"type\"] == MetricType.NUMERICAL:\n                    scores = list(value[\"distribution\"].values())\n                    value[\"aggregation\"] = {\n                        \"mean\": sum(scores) / value[\"involved_tasks\"],\n                        \"max\": max(scores),\n                        \"min\": min(scores),\n                    }\n\n                print(\n                    \"\\t\\tAggregation:\",\n                    json.dumps(\n                        value[\"aggregation\"],\n                        indent=4,\n                        ensure_ascii=False,\n                    ).replace(\"\\n\", \"\\n\\t\\t\"),\n                )\n\n            meta_info[\"repeats\"][repeat_id] = current_repeat\n\n            # Aggregate total stats\n            repeat_stats = current_repeat[\"stats\"]\n\n            # llm\n            for model_name, cnt in repeat_stats.get(\"llm\", {}).items():\n                meta_info[\"total_stats\"][\"llm\"][model_name] += cnt\n\n            # agent\n            meta_info[\"total_stats\"][\"agent\"] += repeat_stats.get(\"agent\", 0)\n\n            # tool\n            for tool_name, cnt in repeat_stats.get(\"tool\", {}).items():\n                meta_info[\"total_stats\"][\"tool\"][tool_name] += cnt\n\n            # embedding\n            for embedding_model, cnt in repeat_stats.get(\n                \"embedding\",\n                {},\n            ).items():\n                meta_info[\"total_stats\"][\"embedding\"][embedding_model] += cnt\n\n            # chat usage\n            for model_name, usage in repeat_stats.get(\n                \"chat_usage\",\n                {},\n            ).items():\n                if model_name not in meta_info[\"total_stats\"][\"chat_usage\"]:\n                    meta_info[\"total_stats\"][\"chat_usage\"][\n                        model_name\n                    ] = defaultdict(int)\n                meta_info[\"total_stats\"][\"chat_usage\"][model_name][\n                    \"input_tokens\"\n                ] += usage.get(\"input_tokens\", 0)\n                meta_info[\"total_stats\"][\"chat_usage\"][model_name][\n                    \"output_tokens\"\n                ] += usage.get(\"output_tokens\", 0)\n\n        # save\n        self.storage.save_aggregation_result(meta_info)\n"
  },
  {
    "path": "src/agentscope/evaluate/_evaluator/_general_evaluator.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"General evaluator implementation in AgentScope, which is easy to debug\ncompared to the RayEvaluator.\"\"\"\nfrom typing import Callable, Awaitable, Coroutine, Any\n\nfrom ._evaluator_base import EvaluatorBase\nfrom ._in_memory_exporter import _InMemoryExporter\nfrom .._evaluator_storage import EvaluatorStorageBase\nfrom .._task import Task\nfrom .._solution import SolutionOutput\nfrom .._benchmark_base import BenchmarkBase\n\n\nclass GeneralEvaluator(EvaluatorBase):\n    \"\"\"The general evaluator that support users to debug their evaluation\"\"\"\n\n    def __init__(\n        self,\n        name: str,\n        benchmark: BenchmarkBase,\n        n_repeat: int,\n        storage: EvaluatorStorageBase,\n        n_workers: int,\n    ) -> None:\n        \"\"\"Initialize the evaluator.\"\"\"\n        super().__init__(\n            name=name,\n            benchmark=benchmark,\n            n_repeat=n_repeat,\n            storage=storage,\n        )\n\n        assert isinstance(benchmark, BenchmarkBase)\n\n        assert n_repeat >= 1, \"n_repeat must be at least 1\"\n\n        assert n_workers >= 1, \"n_workers must be at least 1\"\n\n        self.benchmark = benchmark\n        self.n_repeat = n_repeat\n        self.n_workers = n_workers\n\n    async def run_evaluation(\n        self,\n        task: Task,\n        repeat_id: str,\n        solution_output: SolutionOutput,\n    ) -> None:\n        \"\"\"Run the evaluation for a task and solution result.\"\"\"\n        evaluation_results = await task.evaluate(solution_output)\n        # store the evaluation result\n        for result in evaluation_results:\n            self.storage.save_evaluation_result(\n                task_id=task.id,\n                repeat_id=repeat_id,\n                evaluation=result,\n            )\n\n    async def run_solution(\n        self,\n        repeat_id: str,\n        task: Task,\n        solution: Callable[[Task, Callable], Awaitable[SolutionOutput]],\n    ) -> None:\n        \"\"\"Generate a solution to a task and evaluate.\"\"\"\n        if self.storage.solution_result_exists(task.id, repeat_id):\n            # Obtain from storage\n            solution_result = self.storage.get_solution_result(\n                task.id,\n                repeat_id,\n            )\n\n        else:\n            from opentelemetry import trace\n            from opentelemetry.context import attach, detach\n            from opentelemetry import baggage\n\n            tracer = trace.get_tracer(__name__)\n\n            # Set baggage\n            ctx = baggage.set_baggage(\"task_id\", task.id)\n            ctx = baggage.set_baggage(\"repeat_id\", repeat_id, context=ctx)\n\n            # Activate the context\n            token = attach(ctx)\n\n            try:\n                with tracer.start_as_current_span(\n                    name=f\"Solution_{task.id}_{repeat_id}\",\n                ):\n                    from ... import _config\n\n                    _config.trace_enabled = True\n\n                    # Run the solution\n                    solution_result = await solution(\n                        task,\n                        self.storage.get_agent_pre_print_hook(\n                            task.id,\n                            repeat_id,\n                        ),\n                    )\n                    self.storage.save_solution_result(\n                        task.id,\n                        repeat_id,\n                        solution_result,\n                    )\n            finally:\n                detach(token)\n\n        # Evaluate the solution with the\n        for metric in task.metrics:\n            if not self.storage.evaluation_result_exists(\n                task.id,\n                repeat_id,\n                metric.name,\n            ):\n                await self.run_evaluation(\n                    task,\n                    repeat_id,\n                    solution_result,\n                )\n\n    async def run(\n        self,\n        solution: Callable[\n            [Task, Callable],\n            Coroutine[Any, Any, SolutionOutput],\n        ],\n    ) -> None:\n        \"\"\"Run the ray-based distributed and parallel evaluation, and get the\n        results.\n\n        Args:\n            solution (`Callable[[Task, Callable], Coroutine[Any, Any, \\\n            SolutionOutput]]`):\n                A async function that takes a `Task` instance and a pre-print\n                hook function as input, returns a `SolutionOutput` instance.\n        \"\"\"\n\n        from opentelemetry import trace\n        from opentelemetry.sdk.trace import TracerProvider\n        from opentelemetry.sdk.trace.export import SimpleSpanProcessor\n\n        exporter = _InMemoryExporter()\n        span_processor = SimpleSpanProcessor(exporter)\n\n        tracer_provider: TracerProvider = trace.get_tracer_provider()\n        if not isinstance(tracer_provider, TracerProvider):\n            # Create a new tracer provider if not exists\n            tracer_provider = TracerProvider()\n        tracer_provider.add_span_processor(span_processor)\n        trace.set_tracer_provider(tracer_provider)\n\n        await self._save_evaluation_meta()\n\n        for task in self.benchmark:\n            await self._save_task_meta(task)\n\n            for repeat_id in range(self.n_repeat):\n                await self.run_solution(\n                    str(repeat_id),\n                    task,\n                    solution,\n                )\n\n                # Save the exporter data\n                if (\n                    task.id in exporter.cnt\n                    and str(repeat_id) in exporter.cnt[task.id]\n                ):\n                    self.storage.save_solution_stats(\n                        task.id,\n                        str(repeat_id),\n                        exporter.cnt[task.id][str(repeat_id)],\n                    )\n\n        await self.aggregate()\n"
  },
  {
    "path": "src/agentscope/evaluate/_evaluator/_in_memory_exporter.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"An in memory exporter of OpenTelemetry traces for AgentScope evaluator, used\nto record the token usage during evaluation.\"\"\"\nfrom collections import defaultdict\nfrom typing import Sequence\n\nfrom opentelemetry import baggage\nfrom opentelemetry.sdk.trace import ReadableSpan\nfrom opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult\n\nfrom ...tracing._attributes import SpanAttributes, OperationNameValues\n\n\nclass _InMemoryExporter(SpanExporter):\n    \"\"\"An in memory exporter to store the token usage from the ChatModel spans\n    in OpenTelemetry traces.\"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"Initialize the in memory exporter.\"\"\"\n        # Initialize the counter\n        self.cnt: dict = {}\n        self._stopped = False\n\n    def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:\n        \"\"\"Exports a batch of telemetry data.\n\n        Args:\n            spans (`Sequence[ReadableSpan]`):\n                The list of `opentelemetry.trace.Span` objects to be exported\n\n        Returns:\n            `SpanExportResult`:\n                The result of the export\n        \"\"\"\n        for span in spans:\n            task_id = baggage.get_baggage(\"task_id\")\n            repeat_id = baggage.get_baggage(\"repeat_id\")\n\n            if task_id is None or repeat_id is None:\n                continue\n\n            if task_id not in self.cnt:\n                self.cnt[task_id] = {}\n\n            if repeat_id not in self.cnt[task_id]:\n                self.cnt[task_id][repeat_id] = {\n                    \"llm\": defaultdict(int),\n                    \"agent\": 0,\n                    \"tool\": defaultdict(int),\n                    \"embedding\": defaultdict(int),\n                    \"chat_usage\": {},\n                }\n\n            span_kind = span.attributes.get(\n                SpanAttributes.GEN_AI_OPERATION_NAME,\n            )\n            if span_kind == OperationNameValues.CHAT:\n                model_name = span.attributes.get(\n                    SpanAttributes.GEN_AI_REQUEST_MODEL,\n                    \"unknown\",\n                )\n                self.cnt[task_id][repeat_id][\"llm\"][model_name] += 1\n                if (\n                    model_name\n                    not in self.cnt[task_id][repeat_id][\"chat_usage\"]\n                ):\n                    self.cnt[task_id][repeat_id][\"chat_usage\"][\n                        model_name\n                    ] = defaultdict(int)\n\n                self.cnt[task_id][repeat_id][\"chat_usage\"][model_name][\n                    \"input_tokens\"\n                ] += span.attributes.get(\n                    SpanAttributes.GEN_AI_USAGE_INPUT_TOKENS,\n                    0,\n                )\n\n                self.cnt[task_id][repeat_id][\"chat_usage\"][model_name][\n                    \"output_tokens\"\n                ] += span.attributes.get(\n                    SpanAttributes.GEN_AI_USAGE_OUTPUT_TOKENS,\n                    0,\n                )\n\n            elif span_kind == OperationNameValues.INVOKE_AGENT:\n                self.cnt[task_id][repeat_id][\"agent\"] += 1\n\n            elif span_kind == OperationNameValues.EXECUTE_TOOL:\n                tool_name = span.attributes.get(\n                    SpanAttributes.GEN_AI_TOOL_NAME,\n                    \"unknown\",\n                )\n                self.cnt[task_id][repeat_id][\"tool\"][tool_name] += 1\n\n            elif span_kind == OperationNameValues.EMBEDDINGS:\n                embedding_model = span.attributes.get(\n                    SpanAttributes.GEN_AI_REQUEST_MODEL,\n                    \"unknown\",\n                )\n                self.cnt[task_id][repeat_id][\"embedding\"][embedding_model] += 1\n\n        return SpanExportResult.SUCCESS\n\n    def shutdown(self) -> None:\n        \"\"\"Shuts down the exporter.\"\"\"\n        self._stopped = True\n"
  },
  {
    "path": "src/agentscope/evaluate/_evaluator/_ray_evaluator.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The evaluator base class in agentscope.\"\"\"\nimport asyncio\nfrom typing import Callable, Awaitable, Coroutine, Any\n\nfrom ._in_memory_exporter import _InMemoryExporter\nfrom .._benchmark_base import BenchmarkBase\nfrom .._evaluator._evaluator_base import EvaluatorBase\nfrom .._solution import SolutionOutput\nfrom .._task import Task\nfrom .._evaluator_storage import EvaluatorStorageBase\n\n\ndef _check_ray_available() -> None:\n    \"\"\"Check if ray is available and raise ImportError if not.\"\"\"\n    try:\n        import ray  # noqa  # pylint: disable=unused-import\n    except ImportError as e:\n        raise ImportError(\n            \"Ray is not installed. Please install it with `pip install ray` \"\n            \"to use the RayEvaluator.\",\n        ) from e\n\n\n# Create a conditional decorator for ray.remote\ndef _ray_remote_decorator(cls: Any) -> Any:\n    \"\"\"\n    Conditional ray.remote decorator that only applies when ray is available.\n    \"\"\"\n    try:\n        import ray\n\n        return ray.remote(cls)\n    except ImportError:\n        return cls\n\n\n@_ray_remote_decorator\nclass RayEvaluationActor:\n    \"\"\"\n    Actor class for running evaluation with ray remote.\n    \"\"\"\n\n    @staticmethod\n    async def run(\n        storage: EvaluatorStorageBase,\n        task: Task,\n        repeat_id: str,\n        solution_output: SolutionOutput,\n    ) -> None:\n        \"\"\"\n        Run the evaluation for a task and solution result.\n\n        Args:\n            storage (EvaluatorStorageBase): Evaluator storage.\n            task (Task): Task to be evaluated.\n            repeat_id (str): Repeat ID\n            solution_output (SolutionOutput): output data after execute agents.\n        \"\"\"\n        evaluation_results = await task.evaluate(solution_output)\n        # store the evaluation result\n        for result in evaluation_results:\n            storage.save_evaluation_result(\n                task_id=task.id,\n                repeat_id=repeat_id,\n                evaluation=result,\n            )\n\n\n@_ray_remote_decorator\nclass RaySolutionActor:\n    \"\"\"\n    Actor class for running agent solutions with ray remote.\n    \"\"\"\n\n    def __init__(self, n_workers: int = 1):\n        self.eval_actor = RayEvaluationActor.options(\n            max_concurrency=n_workers,\n        ).remote()\n\n        # Set up global exporter for this Actor\n        self.exporter = _InMemoryExporter()\n\n        from opentelemetry import trace\n        from opentelemetry.sdk.trace import TracerProvider\n        from opentelemetry.sdk.trace.export import SimpleSpanProcessor\n\n        span_processor = SimpleSpanProcessor(self.exporter)\n        tracer_provider: TracerProvider = trace.get_tracer_provider()\n        if not isinstance(tracer_provider, TracerProvider):\n            # Create a new tracer provider if not exists\n            tracer_provider = TracerProvider()\n        tracer_provider.add_span_processor(span_processor)\n        trace.set_tracer_provider(tracer_provider)\n\n    async def run(\n        self,\n        storage: EvaluatorStorageBase,\n        repeat_id: str,\n        task: Task,\n        solution: Callable[\n            [Task, Callable],\n            Coroutine[Any, Any, SolutionOutput],\n        ],\n    ) -> None:\n        \"\"\"Generate a solution to a task and evaluate.\n\n        Args:\n            storage (EvaluatorStorageBase): Evaluator storage.\n            repeat_id (str): Repeat ID.\n            task (Task): Task to be evaluated.\n            solution\n                (Callable[[Task, Callable], Awaitable[SolutionOutput, Any]]):\n                callable function to execute agents and generate results.\n        \"\"\"\n        if storage.solution_result_exists(task.id, repeat_id):\n            # Obtain from storage\n            solution_result = storage.get_solution_result(\n                task.id,\n                repeat_id,\n            )\n\n        else:\n            from opentelemetry import trace, baggage\n            from opentelemetry.context import attach, detach\n\n            tracer = trace.get_tracer(__name__)\n\n            # Set baggage items\n            ctx = baggage.set_baggage(\"task_id\", task.id)\n            ctx = baggage.set_baggage(\"repeat_id\", repeat_id, context=ctx)\n\n            # Attach the context with baggage\n            token = attach(ctx)\n\n            try:\n                with tracer.start_as_current_span(\n                    name=f\"Solution_{task.id}_{repeat_id}\",\n                ):\n                    from ... import _config\n\n                    _config.trace_enabled = True\n\n                    # Run the solution\n                    solution_result = await solution(\n                        task,\n                        storage.get_agent_pre_print_hook(\n                            task.id,\n                            repeat_id,\n                        ),\n                    )\n            finally:\n                detach(token)\n                # Ensure all spans are flushed\n                trace.get_tracer_provider().force_flush()\n\n            storage.save_solution_stats(\n                task.id,\n                repeat_id,\n                self.exporter.cnt.get(task.id, {}).get(repeat_id, {}),\n            )\n\n            storage.save_solution_result(\n                task.id,\n                repeat_id,\n                solution_result,\n            )\n\n        # Evaluate the solution with the metrics\n        futures = []\n        for metric in task.metrics:\n            if not storage.evaluation_result_exists(\n                task.id,\n                repeat_id,\n                metric.name,\n            ):\n                futures.append(\n                    self.eval_actor.run.remote(\n                        storage,\n                        task,\n                        repeat_id,\n                        solution_result,\n                    ),\n                )\n        if futures:\n            await asyncio.gather(*futures)\n\n\nclass RayEvaluator(EvaluatorBase):\n    \"\"\"The ray-based evaluator that supports distributed and parallel\n    evaluation.\"\"\"\n\n    def __init__(\n        self,\n        name: str,\n        benchmark: BenchmarkBase,\n        n_repeat: int,\n        storage: EvaluatorStorageBase,\n        n_workers: int,\n    ) -> None:\n        \"\"\"Initialize the evaluator.\"\"\"\n        super().__init__(\n            name=name,\n            benchmark=benchmark,\n            n_repeat=n_repeat,\n            storage=storage,\n        )\n\n        # Check ray availability early\n        _check_ray_available()\n\n        assert isinstance(benchmark, BenchmarkBase)\n\n        assert n_repeat >= 1, \"n_repeat must be at least 1\"\n\n        assert n_workers >= 1, \"n_workers must be at least 1\"\n\n        self.benchmark = benchmark\n        self.n_repeat = n_repeat\n        self.n_workers = n_workers\n\n    async def run(\n        self,\n        solution: Callable[\n            [Task, Callable],\n            Awaitable[SolutionOutput] | SolutionOutput,\n        ],\n    ) -> None:\n        \"\"\"Run the ray-based distributed and parallel evaluation, and get the\n        results.\n\n        Args:\n            solution (`Callable[[Task], SolutionOutput]`):\n                A sync or async function that takes a `Task` instance as input\n                and returns a `SolutionOutput` instance.\n        \"\"\"\n\n        await self._save_evaluation_meta()\n\n        # Create solution actors\n        futures = []\n        solution_actor = RaySolutionActor.options(\n            max_concurrency=self.n_workers,\n        ).remote(n_workers=self.n_workers)\n\n        # Iterate over all tasks in the benchmark\n        for task in self.benchmark:\n            # Save the task meta information\n            await self._save_task_meta(task)\n\n            # Run n_repeat times\n            for repeat_id in range(self.n_repeat):\n                futures.append(\n                    solution_actor.run.remote(\n                        self.storage,\n                        str(repeat_id),\n                        task,\n                        solution,\n                    ),\n                )\n\n        # Await all the futures\n        if futures:\n            await asyncio.gather(*futures)\n\n        # Aggregate the results\n        await self.aggregate()\n"
  },
  {
    "path": "src/agentscope/evaluate/_evaluator_storage/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The evaluator storage module in AgentScope.\"\"\"\n\nfrom ._evaluator_storage_base import EvaluatorStorageBase\nfrom ._file_evaluator_storage import FileEvaluatorStorage\n\n__all__ = [\n    \"EvaluatorStorageBase\",\n    \"FileEvaluatorStorage\",\n]\n"
  },
  {
    "path": "src/agentscope/evaluate/_evaluator_storage/_evaluator_storage_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The evaluator storage base class for storing solution and evaluation\nresults.\"\"\"\nfrom abc import abstractmethod\nfrom typing import Any, Callable\n\nfrom .._metric_base import MetricResult\nfrom .._solution import SolutionOutput\nfrom ...agent import AgentBase\nfrom ...types import JSONSerializableObject\n\n\nclass EvaluatorStorageBase:\n    \"\"\"Used to store the solution results and evaluation results to support\n    resuming the evaluation process\"\"\"\n\n    @abstractmethod\n    def save_solution_result(\n        self,\n        task_id: str,\n        repeat_id: str,\n        output: SolutionOutput,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Save the solution result.\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            repeat_id (`str`):\n                The repeat ID for the task, usually the index of the repeat\n                evaluation.\n            output (`SolutionOutput`):\n                The solution output to be saved.\n        \"\"\"\n\n    @abstractmethod\n    def get_evaluation_result(\n        self,\n        task_id: str,\n        repeat_id: str,\n        metric_name: str,\n    ) -> MetricResult:\n        \"\"\"Get the evaluation result by the given task id and repeat id\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            repeat_id (`str`):\n                The repeat ID for the task, usually the index of the repeat\n                evaluation.\n            metric_name (`str`):\n                The metric name.\n\n        Returns:\n            `MetricResult`:\n                The evaluation result for the given task and repeat ID.\n        \"\"\"\n\n    @abstractmethod\n    def save_evaluation_result(\n        self,\n        task_id: str,\n        repeat_id: str,\n        evaluation: MetricResult,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Save the evaluation result.\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            repeat_id (`str`):\n                The repeat ID for the task, usually the index of the repeat\n                evaluation.\n            evaluation (`MetricResult`):\n                The evaluation result to be saved.\n        \"\"\"\n\n    @abstractmethod\n    def get_solution_result(\n        self,\n        task_id: str,\n        repeat_id: str,\n        **kwargs: Any,\n    ) -> SolutionOutput:\n        \"\"\"Get the solution result for the given task and repeat id.\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            repeat_id (`str`):\n                The repeat ID for the task, usually the index of the repeat\n                evaluation.\n\n        Returns:\n            `SolutionOutput`:\n                The solution output for the given task and repeat ID.\n        \"\"\"\n\n    @abstractmethod\n    def solution_result_exists(self, task_id: str, repeat_id: str) -> bool:\n        \"\"\"Check if the solution for the given task and repeat is finished.\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            repeat_id (`str`):\n                The repeat ID for the task, usually the index of the repeat\n                evaluation.\n\n        Returns:\n            `bool`:\n                True if the solution result file exists, False otherwise.\n        \"\"\"\n\n    @abstractmethod\n    def evaluation_result_exists(\n        self,\n        task_id: str,\n        repeat_id: str,\n        metric_name: str,\n    ) -> bool:\n        \"\"\"Check if the evaluation result for the given solution and metric\n        is finished.\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            repeat_id (`str`):\n                The repeat ID for the task, usually the index of the repeat\n                evaluation.\n            metric_name (`str`):\n                The name of the metric.\n\n        Returns:\n            `bool`:\n                True if the evaluation result file exists, False otherwise.\n        \"\"\"\n\n    @abstractmethod\n    def save_aggregation_result(\n        self,\n        aggregation_result: dict,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Save the aggregation result.\n\n        Args:\n            aggregation_result (`dict`):\n                A dictionary containing the aggregation result.\n        \"\"\"\n\n    @abstractmethod\n    def aggregation_result_exists(\n        self,\n        **kwargs: Any,\n    ) -> bool:\n        \"\"\"Check if the aggregation result exists\n\n        Returns:\n            `bool`:\n                `True` if the aggregation result file exists.\n        \"\"\"\n\n    @abstractmethod\n    def save_evaluation_meta(self, meta_info: dict) -> None:\n        \"\"\"Save the evaluation meta information.\n\n        Args:\n            meta_info (`dict`):\n                A dictionary containing the meta information.\n        \"\"\"\n\n    @abstractmethod\n    def save_task_meta(\n        self,\n        task_id: str,\n        meta_info: dict[str, JSONSerializableObject],\n    ) -> None:\n        \"\"\"Save the task meta information.\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            meta_info (`dict[str, JSONSerializableObject]`):\n                The task meta information to be saved, which should be JSON\n                serializable.\n        \"\"\"\n\n    @abstractmethod\n    def save_solution_stats(\n        self,\n        task_id: str,\n        repeat_id: str,\n        stats: dict,\n    ) -> None:\n        \"\"\"Save the solution statistics information for a given task and\n        repeat ID.\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            repeat_id (`str`):\n                The repeat ID for the task, usually the index of the repeat\n                evaluation.\n            stats (`dict`):\n                A dictionary containing the solution statistics to be saved.\n        \"\"\"\n\n    @abstractmethod\n    def get_solution_stats(\n        self,\n        task_id: str,\n        repeat_id: str,\n    ) -> dict:\n        \"\"\"Get the solution statistics information for a given task and\n        repeat ID.\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            repeat_id (`str`):\n                The repeat ID for the task, usually the index of the repeat\n                evaluation.\n\n        Returns:\n            `dict`:\n                A dictionary containing the solution statistics for the given\n                task and repeat ID.\n        \"\"\"\n\n    @abstractmethod\n    def get_agent_pre_print_hook(\n        self,\n        task_id: str,\n        repeat_id: str,\n    ) -> Callable[[AgentBase, dict], None]:\n        \"\"\"Get a pre-print hook function for the agent to save the agent\n        printing in the evaluation storage.\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            repeat_id (`str`):\n                The repeat ID for the task, usually the index of the repeat\n                evaluation.\n\n        Returns:\n            `Callable[[AgentBase, dict], None]`:\n                A hook function that takes an `AgentBase` instance and a\n                keyword arguments dictionary as input, saving the agent's\n                printing Msg into the evaluation storage.\n        \"\"\"\n"
  },
  {
    "path": "src/agentscope/evaluate/_evaluator_storage/_file_evaluator_storage.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"A file system based evaluator storage.\"\"\"\nimport json\nimport os\nfrom json import JSONDecodeError\nfrom typing import Any, Callable\n\nfrom ._evaluator_storage_base import EvaluatorStorageBase\nfrom .._solution import SolutionOutput\nfrom .._metric_base import MetricResult\nfrom ...agent import AgentBase\nfrom ...message import Msg\nfrom ...types import JSONSerializableObject\n\n\nclass FileEvaluatorStorage(EvaluatorStorageBase):\n    \"\"\"File system based evaluator storage, providing methods to save and\n    retrieve evaluation results. So that the evaluation process can be resumed\n    from the last saved state.\n\n    The files are organized in a directory structure:\n    - save_dir/\n        - evaluation_result.json\n        - evaluation_meta.json\n        - {repeat_id}/\n            - {task_id}/\n                - solution.json\n                - evaluation/\n                    - {metric_name}.json\n    \"\"\"\n\n    SOLUTION_FILE_NAME = \"solution.json\"\n    SOLUTION_STATS_FILE_NAME = \"stats.json\"\n    EVALUATION_DIR_NAME = \"evaluation\"\n    EVALUATION_RESULT_FILE = \"evaluation_result.json\"\n    EVALUATION_META_FILE = \"evaluation_meta.json\"\n    TASK_META_FILE = \"task_meta.json\"\n    AGENT_PRINTING_LOG = \"logging.txt\"\n\n    def __init__(self, save_dir: str) -> None:\n        \"\"\"Initialize the file evaluator storage.\"\"\"\n        self.save_dir = os.path.abspath(save_dir)\n\n    def _get_save_path(\n        self,\n        task_id: str,\n        repeat_id: str | None,\n        *args: str,\n    ) -> str:\n        \"\"\"Get the save path for a given task, repeat ID, and additional path\n        components.\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            repeat_id (`str | None`):\n                The repeat ID for the task, usually the index of the repeat\n                evaluation. If None, it will be ignored in the path.\n            *args (`str`):\n                Additional path components to be appended.\n        \"\"\"\n        path_components = [\n            task_id,\n            repeat_id,\n            *args,\n        ]\n\n        path = os.path.join(self.save_dir, *[_ for _ in path_components if _])\n        os.makedirs(os.path.dirname(path), exist_ok=True)\n        return path\n\n    def save_solution_result(\n        self,\n        task_id: str,\n        repeat_id: str,\n        output: SolutionOutput,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Save the solution result.\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            repeat_id (`str`):\n                The repeat ID for the task, usually the index of the repeat\n                evaluation.\n            output (`SolutionOutput`):\n                The solution output to be saved.\n        \"\"\"\n        path_file = self._get_save_path(\n            task_id,\n            repeat_id,\n            self.SOLUTION_FILE_NAME,\n        )\n        with open(path_file, \"w\", encoding=\"utf-8\") as f:\n            json.dump(output, f, ensure_ascii=False, indent=4)\n\n    def save_evaluation_result(\n        self,\n        task_id: str,\n        repeat_id: str,\n        evaluation: MetricResult,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Save the evaluation result.\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            repeat_id (`str`):\n                The repeat ID for the task, usually the index of the repeat\n                evaluation.\n            evaluation (`MetricResult`):\n                The evaluation result to be saved.\n        \"\"\"\n        path_file = self._get_save_path(\n            task_id,\n            repeat_id,\n            self.EVALUATION_DIR_NAME,\n            f\"{evaluation.name}.json\",\n        )\n        with open(path_file, \"w\", encoding=\"utf-8\") as f:\n            json.dump(evaluation, f, ensure_ascii=False, indent=4)\n\n    def get_evaluation_result(\n        self,\n        task_id: str,\n        repeat_id: str,\n        metric_name: str,\n    ) -> MetricResult:\n        \"\"\"Get the evaluation result by the given task id and repeat id\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            repeat_id (`str`):\n                The repeat ID for the task, usually the index of the repeat\n                evaluation.\n            metric_name (`str`):\n                The metric name.\n\n        Returns:\n            `MetricResult`:\n                The evaluation result for the given task and repeat ID.\n        \"\"\"\n        path_file = self._get_save_path(\n            task_id,\n            repeat_id,\n            self.EVALUATION_DIR_NAME,\n            f\"{metric_name}.json\",\n        )\n        if not os.path.exists(path_file):\n            raise FileNotFoundError(path_file)\n        with open(path_file, \"r\", encoding=\"utf-8\") as f:\n            evaluation = json.load(f)\n        return MetricResult(**evaluation)\n\n    def get_solution_result(\n        self,\n        task_id: str,\n        repeat_id: str,\n        **kwargs: Any,\n    ) -> SolutionOutput:\n        \"\"\"Get the solution result for the given task and repeat id from the\n        file system.\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            repeat_id (`str`):\n                The repeat ID for the task, usually the index of the repeat\n                evaluation.\n\n        Raises:\n            `FileNotFoundError`:\n                If the solution result file does not exist for the given task\n                and repeat ID.\n\n        Returns:\n            `SolutionOutput`:\n                The solution output for the given task and repeat ID.\n        \"\"\"\n        path_file = self._get_save_path(\n            task_id,\n            repeat_id,\n            self.SOLUTION_FILE_NAME,\n        )\n        if not os.path.exists(path_file):\n            raise FileNotFoundError(\n                f\"Solution result for task {task_id} and repeat {repeat_id} \"\n                \"not found.\",\n            )\n\n        try:\n            with open(path_file, \"r\", encoding=\"utf-8\") as f:\n                solution_data = json.load(f)\n        except JSONDecodeError as e:\n            raise JSONDecodeError(\n                f\"Failed to load JSON from {path_file}: {e.msg}\",\n                e.doc,\n                e.pos,\n            ) from e\n\n        return SolutionOutput(**solution_data)\n\n    def solution_result_exists(self, task_id: str, repeat_id: str) -> bool:\n        \"\"\"Check if the solution for the given task and repeat is finished.\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            repeat_id (`str`):\n                The repeat ID for the task, usually the index of the repeat\n                evaluation.\n\n        Returns:\n            `bool`:\n                True if the solution result file exists, False otherwise.\n        \"\"\"\n        path_file = self._get_save_path(\n            task_id,\n            repeat_id,\n            self.SOLUTION_FILE_NAME,\n        )\n\n        return os.path.exists(path_file) and os.path.getsize(path_file) > 0\n\n    def evaluation_result_exists(\n        self,\n        task_id: str,\n        repeat_id: str,\n        metric_name: str,\n    ) -> bool:\n        \"\"\"Check if the evaluation result for the given solution and metric\n        is finished.\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            repeat_id (`str`):\n                The repeat ID for the task, usually the index of the repeat\n                evaluation.\n            metric_name (`str`):\n                The name of the metric.\n\n        Returns:\n            `bool`:\n                True if the evaluation result file exists, False otherwise.\n        \"\"\"\n        path_file = self._get_save_path(\n            task_id,\n            repeat_id,\n            self.EVALUATION_DIR_NAME,\n            f\"{metric_name}.json\",\n        )\n        return os.path.exists(path_file) and os.path.getsize(path_file) > 0\n\n    def save_aggregation_result(\n        self,\n        aggregation_result: dict,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Save the aggregation result.\n\n        Args:\n            aggregation_result (`dict`):\n                A dictionary containing the aggregation result.\n        \"\"\"\n        path_file = os.path.join(\n            self.save_dir,\n            self.EVALUATION_RESULT_FILE,\n        )\n        os.makedirs(os.path.dirname(path_file), exist_ok=True)\n        with open(path_file, \"w\", encoding=\"utf-8\") as f:\n            json.dump(aggregation_result, f, ensure_ascii=False, indent=4)\n\n    def aggregation_result_exists(\n        self,\n        **kwargs: Any,\n    ) -> bool:\n        \"\"\"Check if the aggregation result exists\n\n        Returns:\n            `bool`:\n                `True` if the aggregation result file exists.\n        \"\"\"\n        path_file = os.path.join(\n            self.save_dir,\n            self.EVALUATION_RESULT_FILE,\n        )\n        return os.path.exists(path_file) and os.path.getsize(path_file) > 0\n\n    def save_evaluation_meta(self, meta_info: dict) -> None:\n        \"\"\"Save the evaluation meta information.\n\n        Args:\n            meta_info (`dict`):\n                A dictionary containing the meta information.\n        \"\"\"\n        path_file = os.path.join(\n            self.save_dir,\n            self.EVALUATION_META_FILE,\n        )\n        os.makedirs(os.path.dirname(path_file), exist_ok=True)\n        with open(path_file, \"w\", encoding=\"utf-8\") as f:\n            json.dump(meta_info, f, ensure_ascii=False, indent=4)\n\n    def save_task_meta(\n        self,\n        task_id: str,\n        meta_info: dict[str, JSONSerializableObject],\n    ) -> None:\n        \"\"\"Save the task meta information.\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            meta_info (`dict[str, JSONSerializableObject]`):\n                The task meta information to be saved, which should be JSON\n                serializable.\n        \"\"\"\n        path_file = self._get_save_path(\n            task_id,\n            None,\n            self.TASK_META_FILE,\n        )\n        with open(path_file, \"w\", encoding=\"utf-8\") as f:\n            json.dump(meta_info, f, ensure_ascii=False, indent=4)\n\n    def save_solution_stats(\n        self,\n        task_id: str,\n        repeat_id: str,\n        stats: dict,\n    ) -> None:\n        \"\"\"Save the solution statistics information for a given task and\n        repeat ID.\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            repeat_id (`str`):\n                The repeat ID for the task, usually the index of the repeat\n                evaluation.\n            stats (`dict`):\n                A dictionary containing the solution statistics to be saved.\n        \"\"\"\n        path_file = self._get_save_path(\n            task_id,\n            repeat_id,\n            self.SOLUTION_STATS_FILE_NAME,\n        )\n        if not os.path.exists(path_file):\n            with open(path_file, \"w\", encoding=\"utf-8\") as f:\n                json.dump(stats, f, ensure_ascii=False, indent=4)\n\n    def get_solution_stats(\n        self,\n        task_id: str,\n        repeat_id: str,\n    ) -> dict:\n        \"\"\"Get the solution statistics information for a given task and\n        repeat ID.\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            repeat_id (`str`):\n                The repeat ID for the task, usually the index of the repeat\n                evaluation.\n\n        Returns:\n            `dict`:\n                A dictionary containing the solution statistics for the given\n                task and repeat ID.\n        \"\"\"\n        path_file = self._get_save_path(\n            task_id,\n            repeat_id,\n            self.SOLUTION_STATS_FILE_NAME,\n        )\n\n        if not os.path.exists(path_file):\n            raise FileNotFoundError(\n                f\"Solution statistics for task {task_id} and repeat \"\n                f\"{repeat_id} not found.\",\n            )\n\n        try:\n            with open(path_file, \"r\", encoding=\"utf-8\") as f:\n                return json.load(f)\n        except JSONDecodeError as e:\n            raise JSONDecodeError(\n                f\"Failed to load JSON from {path_file}: {e.msg}\",\n                e.doc,\n                e.pos,\n            ) from e\n\n    def get_agent_pre_print_hook(\n        self,\n        task_id: str,\n        repeat_id: str,\n    ) -> Callable[[AgentBase, dict], None]:\n        \"\"\"Get a pre-print hook function for the agent to save the agent\n        printing in the evaluation storage.\n\n        Args:\n            task_id (`str`):\n                The task ID.\n            repeat_id (`str`):\n                The repeat ID for the task, usually the index of the repeat\n                evaluation.\n\n        Returns:\n            `Callable[[AgentBase, dict], None]`:\n                A hook function that takes an `AgentBase` instance and a\n                keyword arguments dictionary as input, saving the agent's\n                printing Msg into the evaluation storage.\n        \"\"\"\n\n        def pre_print_hook(_agent: AgentBase, kwargs: dict) -> None:\n            \"\"\"Hook function to save agent's printing.\"\"\"\n            msg: Msg | None = kwargs.get(\"msg\", None)\n            last: bool = kwargs.get(\"last\", False)\n\n            if msg is None or not last:\n                return\n\n            # Only save the last message\n            printing_str = []\n            for block in msg.get_content_blocks():\n                match block[\"type\"]:\n                    case \"text\":\n                        printing_str.append(\n                            f\"{msg.name}: {block['text']}\",\n                        )\n                    case \"thinking\":\n                        printing_str.append(\n                            f\"{msg.name} (thinking): {block['text']}\",\n                        )\n                    case _:\n                        block_str = json.dumps(\n                            block,\n                            ensure_ascii=False,\n                            indent=4,\n                        )\n                        if printing_str:\n                            printing_str.append(block_str)\n                        else:\n                            printing_str.append(f\"{msg.name}: {block_str}\")\n\n            path_file = self._get_save_path(\n                task_id,\n                repeat_id,\n                self.AGENT_PRINTING_LOG,\n            )\n            with open(path_file, \"a\", encoding=\"utf-8\") as f:\n                f.write(\"\\n\".join(printing_str) + \"\\n\")\n\n        return pre_print_hook\n"
  },
  {
    "path": "src/agentscope/evaluate/_metric_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The base class for _metric in evaluation.\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom dataclasses import dataclass, field\nfrom enum import Enum\nfrom typing import Any\n\nfrom .._utils._common import _get_timestamp\nfrom .._utils._mixin import DictMixin\nfrom ..types import JSONSerializableObject\n\n\n@dataclass\nclass MetricResult(DictMixin):\n    \"\"\"The result of a _metric.\"\"\"\n\n    name: str\n    \"\"\"The metric name.\"\"\"\n\n    result: str | float | int\n    \"\"\"The metric result.\"\"\"\n\n    created_at: str = field(default_factory=_get_timestamp)\n    \"\"\"The timestamp when the metric result was created.\"\"\"\n\n    message: str | None = field(default_factory=lambda: None)\n    \"\"\"An optional message for the metric result, can be used to provide\n    additional information or context about the result.\"\"\"\n\n    metadata: dict[str, JSONSerializableObject] | None = field(default=None)\n    \"\"\"Optional metadata for the metric result, can be used to store\n    additional information related to the metric result.\"\"\"\n\n\nclass MetricType(str, Enum):\n    \"\"\"The metric type enum.\"\"\"\n\n    CATEGORY = \"category\"\n    \"\"\"The metric result is a category, e.g. \"pass\" or \"fail\".\"\"\"\n\n    NUMERICAL = \"numerical\"\n    \"\"\"The metric result is a numerical value, e.g. 0.95 or 100.\"\"\"\n\n\n@dataclass\nclass MetricBase(ABC):\n    \"\"\"The base class for _metric in evaluation.\"\"\"\n\n    name: str\n    \"\"\"The name of the Metric\"\"\"\n\n    metric_type: MetricType\n    \"\"\"The metric type\"\"\"\n\n    description: str | None\n    \"\"\"The description of the metric\"\"\"\n\n    categories: list[str] | None\n    \"\"\"The candidate categories. If `metric_type` is \"category\", the\n    categories must be provided, otherwise it should be `None`.\"\"\"\n\n    def __init__(\n        self,\n        name: str,\n        metric_type: MetricType,\n        description: str | None = None,\n        categories: list[str] | None = None,\n    ) -> None:\n        \"\"\"Initialize the _metric object.\n\n        Args:\n            name (`str`):\n                The name of the metric.\n            metric_type (`MetricType`):\n                The type of the metric, can be either \"category\" or\n                \"numerical\", which will determine how to display the result.\n            description (`str`):\n                The description of the metric.\n            categories (`list[str] | None`, optional):\n                The candidate categories. If `metric_type` is \"category\", the\n                categories must be provided, otherwise it should be `None`.\n        \"\"\"\n        self.name = name\n        self.metric_type = metric_type\n        self.description = description\n\n        if metric_type == MetricType.CATEGORY and categories is None:\n            raise ValueError(\n                \"Categories must be provided for category metrics.\",\n            )\n\n        self.categories = categories\n\n    @abstractmethod\n    async def __call__(\n        self,\n        *args: Any,\n        **kwargs: Any,\n    ) -> MetricResult:\n        \"\"\"The call function to calculate the _metric result\"\"\"\n"
  },
  {
    "path": "src/agentscope/evaluate/_solution.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Solution class for evaluation tasks.\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import Any\n\nfrom ..message import (\n    ToolResultBlock,\n    ToolUseBlock,\n    TextBlock,\n)\nfrom ..types._json import JSONSerializableObject\nfrom .._utils._mixin import DictMixin\n\n\n@dataclass\nclass SolutionOutput(DictMixin):\n    \"\"\"The output of a solution in evaluation task\"\"\"\n\n    success: bool\n    \"\"\"Indicates whether the solution is executed successfully. When the\n    solution raise exception, this should be set to False.\"\"\"\n    output: JSONSerializableObject\n    \"\"\"The final output of the solution.\"\"\"\n    trajectory: list[ToolUseBlock | ToolResultBlock | TextBlock]\n    \"\"\"The tool calls and results trajectory\"\"\"\n    meta: dict[str, Any] | None = field(default_factory=lambda: None)\n    \"\"\"Additional metadata for the solution\"\"\"\n\n    def __getstate__(self) -> dict[str, Any]:\n        \"\"\"Custom pickling to handle dataclass + DictMixin inheritance.\"\"\"\n        return self.__dict__.copy()\n\n    def __setstate__(self, state: dict[str, Any]) -> None:\n        \"\"\"Custom unpickling to handle dataclass + DictMixin inheritance.\"\"\"\n        self.__dict__.update(state)\n"
  },
  {
    "path": "src/agentscope/evaluate/_task.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The base class for task in evaluation.\"\"\"\nfrom dataclasses import dataclass, field\nfrom typing import Any\n\nfrom ._solution import SolutionOutput\nfrom ._metric_base import MetricBase, MetricResult\nfrom ..types._json import JSONSerializableObject\n\n\n@dataclass\nclass Task:\n    \"\"\"The base class for task in evaluation.\"\"\"\n\n    id: str\n    \"\"\"The unique identifier for the task.\"\"\"\n\n    input: JSONSerializableObject\n    \"\"\"The task input, which should be a JSON serializable object.\"\"\"\n\n    ground_truth: JSONSerializableObject\n    \"\"\"The task ground truth if exists, which should be a JSON serializable\n    object.\"\"\"\n\n    metrics: list[MetricBase]\n    \"\"\"The metrics to evaluate the task, which should be a list of\n    `MetricBase` objects.\"\"\"\n\n    tags: dict[str, str] | None = field(default_factory=lambda: None)\n    \"\"\"Tags to categorize the task, e.g. `{\"difficulty\": \"easy\",\n    \"cate\": \"math\"}`.\"\"\"\n\n    metadata: dict[str, Any] | None = field(\n        default_factory=lambda: None,\n    )\n    \"\"\"Additional metadata for the task.\"\"\"\n\n    async def evaluate(self, solution: SolutionOutput) -> list[MetricResult]:\n        \"\"\"Evaluate the task with the given solution.\n\n        Args:\n            solution (`SolutionOutput`):\n                The solution to evaluate the task with.\n\n        Returns:\n            `MetricResult`:\n                The result of the evaluation.\n        \"\"\"\n        evaluations = []\n        for metric in self.metrics:\n            result = await metric(solution)\n            evaluations.append(result)\n        return evaluations\n"
  },
  {
    "path": "src/agentscope/exception/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The exception module in agentscope.\"\"\"\n\nfrom ._exception_base import AgentOrientedExceptionBase\nfrom ._tool import (\n    ToolInterruptedError,\n    ToolNotFoundError,\n    ToolInvalidArgumentsError,\n)\n\n__all__ = [\n    \"AgentOrientedExceptionBase\",\n    \"ToolInterruptedError\",\n    \"ToolNotFoundError\",\n    \"ToolInvalidArgumentsError\",\n]\n"
  },
  {
    "path": "src/agentscope/exception/_exception_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The base exception class in agentscope.\"\"\"\n\n\nclass AgentOrientedExceptionBase(Exception):\n    \"\"\"The base class for all agent-oriented exceptions. These exceptions are\n    expect to the captured and exposed to the agent during runtime, so that\n    agents can handle the error appropriately during the runtime.\n    \"\"\"\n\n    def __init__(self, message: str):\n        \"\"\"Initialize the exception with a message.\"\"\"\n        super().__init__(message)\n        self.message = message\n\n    def __str__(self) -> str:\n        \"\"\"Return the string representation of the exception.\"\"\"\n        return f\"{self.__class__.__name__}: {self.message}\"\n"
  },
  {
    "path": "src/agentscope/exception/_tool.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The tool-related exceptions in agentscope.\"\"\"\n\nfrom ._exception_base import AgentOrientedExceptionBase\n\n\nclass ToolNotFoundError(AgentOrientedExceptionBase):\n    \"\"\"Exception raised when a tool was not found.\"\"\"\n\n\nclass ToolInterruptedError(AgentOrientedExceptionBase):\n    \"\"\"Exception raised when a tool calling was interrupted by the user.\"\"\"\n\n\nclass ToolInvalidArgumentsError(AgentOrientedExceptionBase):\n    \"\"\"Exception raised when the arguments passed to a tool are invalid.\"\"\"\n"
  },
  {
    "path": "src/agentscope/formatter/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The formatter module in agentscope.\"\"\"\n\nfrom ._formatter_base import FormatterBase\nfrom ._truncated_formatter_base import TruncatedFormatterBase\nfrom ._dashscope_formatter import (\n    DashScopeChatFormatter,\n    DashScopeMultiAgentFormatter,\n)\nfrom ._anthropic_formatter import (\n    AnthropicChatFormatter,\n    AnthropicMultiAgentFormatter,\n)\nfrom ._openai_formatter import (\n    OpenAIChatFormatter,\n    OpenAIMultiAgentFormatter,\n)\nfrom ._gemini_formatter import (\n    GeminiChatFormatter,\n    GeminiMultiAgentFormatter,\n)\nfrom ._ollama_formatter import (\n    OllamaChatFormatter,\n    OllamaMultiAgentFormatter,\n)\nfrom ._deepseek_formatter import (\n    DeepSeekChatFormatter,\n    DeepSeekMultiAgentFormatter,\n)\nfrom ._a2a_formatter import A2AChatFormatter\n\n__all__ = [\n    \"FormatterBase\",\n    \"TruncatedFormatterBase\",\n    \"DashScopeChatFormatter\",\n    \"DashScopeMultiAgentFormatter\",\n    \"OpenAIChatFormatter\",\n    \"OpenAIMultiAgentFormatter\",\n    \"AnthropicChatFormatter\",\n    \"AnthropicMultiAgentFormatter\",\n    \"GeminiChatFormatter\",\n    \"GeminiMultiAgentFormatter\",\n    \"OllamaChatFormatter\",\n    \"OllamaMultiAgentFormatter\",\n    \"DeepSeekChatFormatter\",\n    \"DeepSeekMultiAgentFormatter\",\n    \"A2AChatFormatter\",\n]\n"
  },
  {
    "path": "src/agentscope/formatter/_a2a_formatter.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The A2A message formatter class.\"\"\"\nimport mimetypes\nimport uuid\nfrom typing import Literal, TYPE_CHECKING\n\n\nfrom .._logging import logger\nfrom ._formatter_base import FormatterBase\nfrom ..message import (\n    Msg,\n    TextBlock,\n    URLSource,\n    Base64Source,\n    ContentBlock,\n)\n\n\nif TYPE_CHECKING:\n    from a2a.types import (\n        Message,\n        Task,\n        Part,\n    )\nelse:\n    Message = \"a2a.types.Message\"\n    Task = \"a2a.types.Task\"\n    Part = \"a2a.types.Part\"\n\n\nclass A2AChatFormatter(FormatterBase):\n    \"\"\"A2A message formatter class, which convert AgentScope messages into\n    A2A message format.\"\"\"\n\n    async def format(self, msgs: list[Msg]) -> Message:\n        \"\"\"Convert AgentScope messages into a A2A message object. Note that\n        A2A server only supports single request message, so the input msgs\n        list will be merged into a single A2A Message.\n\n        .. note:: Note the A2A protocol receives a single message per request,\n         so multi-message inputs will be merged into one A2A Message with role\n         'user'.\n\n        Args:\n            msgs (`list[Msg]`):\n                List of AgentScope Msg objects to be converted.\n\n        Returns:\n            `Message`:\n                The converted A2A Message object.\n        \"\"\"\n\n        from a2a.types import (\n            Part,\n            TextPart,\n            FilePart,\n            FileWithUri,\n            FileWithBytes,\n            DataPart,\n            Role,\n            Message,\n        )\n\n        self.assert_list_of_msgs(msgs)\n\n        parts = []\n        for msg in msgs:\n            for block in msg.get_content_blocks():\n                block_type = block.get(\"type\")\n                if block_type == \"text\" and block.get(\"text\"):\n                    parts.append(\n                        Part(\n                            root=TextPart(\n                                text=block.get(\"text\"),\n                            ),\n                        ),\n                    )\n\n                elif block_type == \"thinking\" and block.get(\"thinking\"):\n                    parts.append(\n                        Part(\n                            root=TextPart(\n                                text=block.get(\"thinking\"),\n                            ),\n                        ),\n                    )\n\n                elif block_type in [\n                    \"image\",\n                    \"video\",\n                    \"audio\",\n                ] and block.get(\"source\"):\n                    source = block.get(\"source\", {})\n                    source_type = source.get(\"type\")\n\n                    if source_type == \"url\":\n                        parts.append(\n                            Part(\n                                root=FilePart(\n                                    file=FileWithUri(\n                                        uri=source.get(\"url\"),\n                                    ),\n                                ),\n                            ),\n                        )\n\n                    elif source_type == \"base64\":\n                        parts.append(\n                            Part(\n                                root=FilePart(\n                                    file=FileWithBytes(\n                                        bytes=source.get(\"data\"),\n                                        mime_type=source.get(\"media_type\"),\n                                    ),\n                                ),\n                            ),\n                        )\n\n                    else:\n                        raise ValueError(\n                            f\"Unsupported source type: {source_type}\",\n                        )\n\n                elif block_type in [\"tool_use\", \"tool_result\"]:\n                    parts.append(\n                        Part(\n                            root=DataPart(\n                                data=block,\n                            ),\n                        ),\n                    )\n\n                else:\n                    logger.error(\n                        \"Unsupported block type %s in A2AFormatter.\",\n                        block_type,\n                    )\n\n        a2a_message = Message(\n            message_id=str(uuid.uuid4()),\n            role=Role.user,\n            parts=parts,\n        )\n\n        return a2a_message\n\n    async def format_a2a_message(self, name: str, message: Message) -> Msg:\n        \"\"\"Convert A2A Message object back to AgentScope Msg format.\n\n        Args:\n            name (`str`):\n                The name of the message sender.\n            message (`Message`):\n                The A2A Message object to be converted.\n\n        Returns:\n            `list[Msg]`:\n                List of converted AgentScope Msg objects.\n        \"\"\"\n\n        from a2a.types import Role\n\n        content = []\n        metadata = None\n        for part in message.parts:\n            content.append(\n                await self._format_a2a_part(part),\n            )\n\n        if message.role == Role.user:\n            role: Literal[\"user\", \"assistant\"] = \"user\"\n        elif message.role == Role.agent:\n            role = \"assistant\"\n        else:\n            raise ValueError(\n                f\"Unsupported role: {message.role} in A2A message.\",\n            )\n\n        return Msg(\n            name=name,\n            role=role,\n            content=content,\n            metadata=metadata,\n        )\n\n    @staticmethod\n    def _guess_type(\n        uri: str | None = None,\n        mime_type: str | None = None,\n    ) -> Literal[\"image\", \"video\", \"audio\", \"unknown\"]:\n        \"\"\"Guess the content type from the uri or mime type.\n\n        Args:\n            uri (`str | None`, optional):\n                The uri of the content.\n            mime_type (`str | None`, optional):\n                The mime type of the content.\n\n        Returns:\n            `Literal[\"image\", \"video\", \"audio\", \"unknown\"]`:\n                The guessed content type.\n        \"\"\"\n        if mime_type is None and uri is None:\n            raise ValueError(\n                \"Either uri or mime_type must be provided to guess the\"\n                \" content type.\",\n            )\n\n        if mime_type is None:\n            mime_type, _encoding = mimetypes.guess_type(uri or \"\")\n\n        if isinstance(mime_type, str):\n            if mime_type.startswith(\"image/\"):\n                return \"image\"\n\n            if mime_type.startswith(\"video/\"):\n                return \"video\"\n\n            if mime_type.startswith(\"audio/\"):\n                return \"audio\"\n\n        return \"unknown\"\n\n    async def format_a2a_task(self, name: str, task: Task) -> list[Msg]:\n        \"\"\"Convert A2A Task object back to AgentScope Msg format.\n\n        Args:\n            name (`str`):\n                The name of the message sender.\n            task (`Task`):\n                The A2A Task object to be converted.\n\n        Returns:\n            `list[Msg]`:\n                Converted AgentScope Msg objects.\n        \"\"\"\n        msgs = []\n        if task.status and task.status.message:\n            msgs.append(\n                await self.format_a2a_message(name, task.status.message),\n            )\n\n        merged_msgs = []\n        for msg in msgs:\n            if merged_msgs and merged_msgs[-1].role == msg.role:\n                merged_msgs[-1].content.extend(msg.content)\n\n            else:\n                merged_msgs.append(msg)\n\n        if task.artifacts:\n            for artifact in task.artifacts:\n                artifact_content = [\n                    await self._format_a2a_part(_) for _ in artifact.parts\n                ]\n\n                if merged_msgs and merged_msgs[-1].role == \"assistant\":\n                    merged_msgs[-1].content.extend(artifact_content)\n                    merged_msgs[-1].metadata = artifact.metadata\n\n                else:\n                    merged_msgs.append(\n                        Msg(\n                            name=name,\n                            role=\"assistant\",\n                            content=artifact_content,\n                            metadata=artifact.metadata,\n                        ),\n                    )\n\n        return merged_msgs\n\n    async def _format_a2a_part(self, part: Part) -> ContentBlock:\n        \"\"\"Convert a single A2A Part object into AgentScope ContentBlock.\n\n        .. note:: We will try to convert the `DataPart` into tool use and tool\n         result blocks if possible.\n\n        Args:\n            part (`Part`):\n                The A2A Part object to be converted.\n\n        Returns:\n            `ContentBlock`:\n                The converted AgentScope ContentBlock.\n        \"\"\"\n\n        from a2a.types import (\n            TextPart,\n            FilePart,\n            FileWithUri,\n            FileWithBytes,\n            DataPart,\n        )\n\n        if isinstance(part.root, TextPart):\n            return TextBlock(\n                type=\"text\",\n                text=part.root.text,\n            )\n\n        if isinstance(part.root, FilePart):\n            if isinstance(part.root.file, FileWithUri):\n                return {  # type: ignore[return-value, misc]\n                    \"type\": self._guess_type(\n                        part.root.file.uri,\n                        part.root.file.mime_type,\n                    ),\n                    \"source\": URLSource(\n                        type=\"url\",\n                        url=part.root.file.uri,\n                    ),\n                }\n\n            if isinstance(part.root.file, FileWithBytes):\n                return {  # type: ignore[return-value, misc]\n                    \"type\": self._guess_type(\n                        mime_type=part.root.file.mime_type,\n                    ),\n                    \"source\": Base64Source(\n                        type=\"base64\",\n                        media_type=part.root.file.mime_type\n                        or \"application/octet-stream\",\n                        data=part.root.file.bytes,\n                    ),\n                }\n\n            raise ValueError(\n                f\"Unsupported File type: {type(part.root.file)} in A2A\"\n                \"message.\",\n            )\n\n        if isinstance(part.root, DataPart):\n            # Maybe the tool use and tool result blocks\n            if {\n                \"type\",\n                \"name\",\n                \"input\",\n                \"id\",\n            } <= part.root.data.keys() and part.root.data[\n                \"type\"\n            ] == \"tool_use\":\n                return part.root.data\n\n            if {\n                \"type\",\n                \"name\",\n                \"output\",\n                \"id\",\n            } <= part.root.data.keys() and part.root.data[\n                \"type\"\n            ] == \"tool_result\":\n                return part.root.data\n\n            # TODO: what about the other data parts?\n            return TextBlock(\n                type=\"text\",\n                text=str(part.root.data),\n            )\n\n        raise ValueError(\n            f\"Unsupported Part type: {type(part.root)} in A2A message\"\n            f\": {part.root}\",\n        )\n"
  },
  {
    "path": "src/agentscope/formatter/_anthropic_formatter.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=too-many-branches\n\"\"\"The Anthropic formatter module.\"\"\"\n\nfrom typing import Any\n\nfrom ._truncated_formatter_base import TruncatedFormatterBase\nfrom .._logging import logger\nfrom ..message import Msg, TextBlock, ImageBlock, ToolUseBlock, ToolResultBlock\nfrom ..token import TokenCounterBase\n\n\nclass AnthropicChatFormatter(TruncatedFormatterBase):\n    \"\"\"The Anthropic formatter class for chatbot scenario, where only a user\n    and an agent are involved. We use the `role` field to identify different\n    entities in the conversation.\n    \"\"\"\n\n    support_tools_api: bool = True\n    \"\"\"Whether support tools API\"\"\"\n\n    support_multiagent: bool = False\n    \"\"\"Whether support multi-agent conversations\"\"\"\n\n    support_vision: bool = True\n    \"\"\"Whether support vision data\"\"\"\n\n    supported_blocks: list[type] = [\n        TextBlock,\n        # Multimodal\n        ImageBlock,\n        # Tool use\n        ToolUseBlock,\n        ToolResultBlock,\n    ]\n    \"\"\"The list of supported message blocks\"\"\"\n\n    async def _format(\n        self,\n        msgs: list[Msg],\n    ) -> list[dict[str, Any]]:\n        \"\"\"Format message objects into Anthropic API format.\n\n        Args:\n            msgs (`list[Msg]`):\n                The list of message objects to format.\n\n        Returns:\n            `list[dict[str, Any]]`:\n                The formatted messages as a list of dictionaries.\n\n        .. note:: Anthropic suggests always passing all previous thinking\n         blocks back to the API in subsequent calls to maintain reasoning\n         continuity. For more details, please refer to\n         `Anthropic's documentation\n         <https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#preserving-thinking-blocks>`_.\n        \"\"\"\n        self.assert_list_of_msgs(msgs)\n\n        messages: list[dict] = []\n        for index, msg in enumerate(msgs):\n            content_blocks = []\n\n            for block in msg.get_content_blocks():\n                typ = block.get(\"type\")\n                if typ in [\"thinking\", \"text\", \"image\"]:\n                    content_blocks.append({**block})\n\n                elif typ == \"tool_use\":\n                    content_blocks.append(\n                        {\n                            \"id\": block.get(\"id\"),\n                            \"type\": \"tool_use\",\n                            \"name\": block.get(\"name\"),\n                            \"input\": block.get(\"input\", {}),\n                        },\n                    )\n\n                elif typ == \"tool_result\":\n                    output = block.get(\"output\")\n                    if output is None:\n                        content_value = [{\"type\": \"text\", \"text\": None}]\n                    elif isinstance(output, list):\n                        content_value = output\n                    else:\n                        content_value = [{\"type\": \"text\", \"text\": str(output)}]\n                    messages.append(\n                        {\n                            \"role\": \"user\",\n                            \"content\": [\n                                {\n                                    \"type\": \"tool_result\",\n                                    \"tool_use_id\": block.get(\"id\"),\n                                    \"content\": content_value,\n                                },\n                            ],\n                        },\n                    )\n                else:\n                    logger.warning(\n                        \"Unsupported block type %s in the message, skipped.\",\n                        typ,\n                    )\n\n            # Claude only allow the first message to be system message\n            if msg.role == \"system\" and index != 0:\n                role = \"user\"\n            else:\n                role = msg.role\n\n            msg_anthropic = {\n                \"role\": role,\n                \"content\": content_blocks or None,\n            }\n\n            # When both content and tool_calls are None, skipped\n            if msg_anthropic[\"content\"] or msg_anthropic.get(\"tool_calls\"):\n                messages.append(msg_anthropic)\n\n        return messages\n\n\nclass AnthropicMultiAgentFormatter(TruncatedFormatterBase):\n    \"\"\"\n    Anthropic formatter for multi-agent conversations, where more than\n    a user and an agent are involved.\n    \"\"\"\n\n    support_tools_api: bool = True\n    \"\"\"Whether support tools API\"\"\"\n\n    support_multiagent: bool = True\n    \"\"\"Whether support multi-agent conversations\"\"\"\n\n    support_vision: bool = True\n    \"\"\"Whether support vision data\"\"\"\n\n    supported_blocks: list[type] = [\n        TextBlock,\n        # Multimodal\n        ImageBlock,\n        # Tool use\n        ToolUseBlock,\n        ToolResultBlock,\n    ]\n    \"\"\"The list of supported message blocks\"\"\"\n\n    def __init__(\n        self,\n        conversation_history_prompt: str = (\n            \"# Conversation History\\n\"\n            \"The content between <history></history> tags contains \"\n            \"your conversation history\\n\"\n        ),\n        token_counter: TokenCounterBase | None = None,\n        max_tokens: int | None = None,\n    ) -> None:\n        \"\"\"Initialize the DashScope multi-agent formatter.\n\n        Args:\n            conversation_history_prompt (`str`):\n                The prompt to use for the conversation history section.\n        \"\"\"\n        super().__init__(token_counter=token_counter, max_tokens=max_tokens)\n        self.conversation_history_prompt = conversation_history_prompt\n\n    async def _format_tool_sequence(\n        self,\n        msgs: list[Msg],\n    ) -> list[dict[str, Any]]:\n        \"\"\"Given a sequence of tool call/result messages, format them into\n        the required format for the Anthropic API.\"\"\"\n        return await AnthropicChatFormatter().format(msgs)\n\n    async def _format_agent_message(\n        self,\n        msgs: list[Msg],\n        is_first: bool = True,\n    ) -> list[dict[str, Any]]:\n        \"\"\"Given a sequence of messages without tool calls/results, format\n        them into the required format for the Anthropic API.\"\"\"\n\n        if is_first:\n            conversation_history_prompt = self.conversation_history_prompt\n        else:\n            conversation_history_prompt = \"\"\n\n        # Format into required Anthropic format\n        formatted_msgs: list[dict] = []\n\n        # Collect the multimodal files\n        conversation_blocks: list = []\n        accumulated_text = []\n        for msg in msgs:\n            for block in msg.get_content_blocks():\n                if block[\"type\"] == \"text\":\n                    accumulated_text.append(f\"{msg.name}: {block['text']}\")\n\n                elif block[\"type\"] == \"image\":\n                    # Handle the accumulated text as a single block\n                    if accumulated_text:\n                        conversation_blocks.append(\n                            {\n                                \"text\": \"\\n\".join(accumulated_text),\n                                \"type\": \"text\",\n                            },\n                        )\n                        accumulated_text.clear()\n\n                    conversation_blocks.append({**block})\n\n        if accumulated_text:\n            conversation_blocks.append(\n                {\n                    \"text\": \"\\n\".join(accumulated_text),\n                    \"type\": \"text\",\n                },\n            )\n\n        if conversation_blocks:\n            if conversation_blocks[0].get(\"text\"):\n                conversation_blocks[0][\"text\"] = (\n                    conversation_history_prompt\n                    + \"<history>\\n\"\n                    + conversation_blocks[0][\"text\"]\n                )\n\n            else:\n                conversation_blocks.insert(\n                    0,\n                    {\n                        \"type\": \"text\",\n                        \"text\": conversation_history_prompt + \"<history>\\n\",\n                    },\n                )\n\n            if conversation_blocks[-1].get(\"text\"):\n                conversation_blocks[-1][\"text\"] += \"\\n</history>\"\n\n            else:\n                conversation_blocks.append(\n                    {\"type\": \"text\", \"text\": \"</history>\"},\n                )\n\n        if conversation_blocks:\n            formatted_msgs.append(\n                {\n                    \"role\": \"user\",\n                    \"content\": conversation_blocks,\n                },\n            )\n\n        return formatted_msgs\n"
  },
  {
    "path": "src/agentscope/formatter/_dashscope_formatter.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=too-many-branches\n\"\"\"The dashscope formatter module.\"\"\"\n\nimport json\nimport os.path\nfrom typing import Any\n\nfrom ._truncated_formatter_base import TruncatedFormatterBase\nfrom .._logging import logger\nfrom .._utils._common import _is_accessible_local_file\nfrom ..message import (\n    Msg,\n    TextBlock,\n    ImageBlock,\n    AudioBlock,\n    VideoBlock,\n    ToolUseBlock,\n    ToolResultBlock,\n    URLSource,\n)\nfrom ..token import TokenCounterBase\n\n\ndef _format_dashscope_media_block(\n    block: ImageBlock | AudioBlock,\n) -> dict[str, str]:\n    \"\"\"Format an image or audio block for DashScope API.\n\n    Args:\n        block (`ImageBlock` | `AudioBlock`):\n            The image or audio block to format.\n\n    Returns:\n        `dict[str, str]`:\n            A dictionary with \"image\" or \"audio\" key and the formatted URL or\n            data URI as value.\n\n    Raises:\n        `NotImplementedError`:\n            If the source type is not supported.\n    \"\"\"\n    typ = block[\"type\"]\n    source = block[\"source\"]\n    if source[\"type\"] == \"url\":\n        url = source[\"url\"]\n        if _is_accessible_local_file(url):\n            return {typ: \"file://\" + os.path.abspath(url)}\n        else:\n            # treat as web url\n            return {typ: url}\n\n    elif source[\"type\"] == \"base64\":\n        media_type = source[\"media_type\"]\n        base64_data = source[\"data\"]\n        return {\n            typ: f\"data:{media_type};base64,{base64_data}\",\n        }\n\n    else:\n        raise NotImplementedError(\n            f\"Unsupported source type '{source.get('type')}' \"\n            f\"for {typ} block.\",\n        )\n\n\ndef _reformat_messages(\n    messages: list[dict[str, Any]],\n) -> list[dict[str, Any]]:\n    \"\"\"Reformat the content to be compatible with HuggingFaceTokenCounter.\n\n     This function processes a list of messages and converts multi-part\n     text content into single string content when all parts are plain text.\n     This is necessary for compatibility with HuggingFaceTokenCounter which\n     expects simple string content rather than structured content with\n     multiple parts.\n\n    Args:\n        messages (list[dict[str, Any]]):\n            A list of message dictionaries where each message may contain a\n            \"content\" field. The content can be either:\n            - A string (unchanged)\n            - A list of content items, where each item is a dict that may\n                contain \"text\", \"type\", and other fields\n\n    Returns:\n        list[dict[str, Any]]:\n            A list of reformatted messages. For messages where all content\n            items are plain text (have \"text\" field and either no \"type\"\n            field or \"type\" == \"text\"), the content list is converted to a\n            single newline-joined string. Other messages remain unchanged.\n\n    Example:\n        .. code-block:: python\n\n            # Case 1: All text content - will be converted\n            messages = [\n                {\n                    \"role\": \"user\",\n                    \"content\": [\n                        {\"text\": \"Hello\", \"type\": \"text\"},\n                        {\"text\": \"World\", \"type\": \"text\"}\n                    ]\n                }\n            ]\n            result = _reformat_messages(messages)\n            print(result[0][\"content\"])\n            # Output: \"Hello\\nWorld\"\n\n            # Case 2: Mixed content - will remain unchanged\n            messages = [\n                {\n                    \"role\": \"user\",\n                    \"content\": [\n                        {\"text\": \"Hello\", \"type\": \"text\"},\n                        {\"image_url\": \"...\", \"type\": \"image\"}\n                    ]\n                }\n            ]\n\n            result = _reformat_messages(messages)  # remain unchanged\n            print(type(result[0][\"content\"]))\n            # Output: <class 'list'>\n\n    \"\"\"\n    for message in messages:\n        content = message.get(\"content\", [])\n\n        is_all_text = True\n        texts = []\n        for item in content:\n            if not isinstance(item, dict) or \"text\" not in item:\n                is_all_text = False\n                break\n            if \"type\" in item and item[\"type\"] != \"text\":\n                is_all_text = False\n                break\n            if item[\"text\"]:\n                texts.append(item[\"text\"])\n\n        if is_all_text and texts:\n            message[\"content\"] = \"\\n\".join(texts)\n\n    return messages\n\n\nclass DashScopeChatFormatter(TruncatedFormatterBase):\n    \"\"\"The DashScope formatter class for chatbot scenario, where only a user\n    and an agent are involved. We use the `role` field to identify different\n    entities in the conversation.\n\n    .. warning::\n        Known Issues with DashScope API:\n\n        1. **Missing content field**: When messages lack the 'content' field,\n           qwen-vl-max models will raise ``KeyError: 'content'``.\n\n        2. **None content value**: When content is ``None``, qwen-vl-max models\n           will raise ``TypeError: 'NoneType' object is not iterable``.\n\n        3. **Empty text in content**: When content contains\n           ``[{\"text\": None}]``, qwen3-max may repeatedly invoke tools\n           multiple times. Note that when qwen3-max initiates tool calls,\n           the returned message contains ``\"content\": \"\"``.\n\n        To avoid these issues, this formatter assigns content as an empty\n        list ``[]`` for messages without valid content blocks.\n\n    \"\"\"\n\n    support_tools_api: bool = True\n    \"\"\"Whether support tools API\"\"\"\n\n    support_multiagent: bool = False\n    \"\"\"Whether support multi-agent conversations\"\"\"\n\n    support_vision: bool = True\n    \"\"\"Whether support vision data\"\"\"\n\n    supported_blocks: list[type] = [\n        TextBlock,\n        ImageBlock,\n        AudioBlock,\n        VideoBlock,\n        ToolUseBlock,\n        ToolResultBlock,\n    ]\n\n    def __init__(\n        self,\n        promote_tool_result_images: bool = False,\n        promote_tool_result_audios: bool = False,\n        promote_tool_result_videos: bool = False,\n        token_counter: TokenCounterBase | None = None,\n        max_tokens: int | None = None,\n    ) -> None:\n        \"\"\"Initialize the DashScope chat formatter.\n\n        Args:\n            promote_tool_result_images (`bool`, defaults to `False`):\n                Whether to promote images from tool results to user messages.\n                Most LLM APIs don't support images in tool result blocks, but\n                do support them in user message blocks. When `True`, images are\n                extracted and appended as a separate user message with\n                explanatory text indicating their source.\n            promote_tool_result_audios (`bool`, defaults to `False`):\n                Whether to promote audios from tool results to user messages.\n                Most LLM APIs don't support audios in tool result blocks, but\n                do support them in user message blocks. When `True`, audios are\n                extracted and appended as a separate user message with\n                explanatory text indicating their source.\n            promote_tool_result_videos (`bool`, defaults to `False`):\n                Whether to promote videos from tool results to user messages.\n                Most LLM APIs don't support videos in tool result blocks, but\n                do support them in user message blocks. When `True`, videos are\n                extracted and appended as a separate user message with\n                explanatory text indicating their source.\n            token_counter (`TokenCounterBase | None`, optional):\n                A token counter instance used to count tokens in the messages.\n                If not provided, the formatter will format the messages\n                without considering token limits.\n            max_tokens (`int | None`, optional):\n                The maximum number of tokens allowed in the formatted\n                messages. If not provided, the formatter will not truncate\n                the messages.\n        \"\"\"\n        super().__init__(token_counter, max_tokens)\n        self.promote_tool_result_images = promote_tool_result_images\n        self.promote_tool_result_audios = promote_tool_result_audios\n        self.promote_tool_result_videos = promote_tool_result_videos\n\n    async def _format(\n        self,\n        msgs: list[Msg],\n    ) -> list[dict[str, Any]]:\n        \"\"\"Format message objects into DashScope API format.\n\n        Args:\n            msgs (`list[Msg]`):\n                The list of message objects to format.\n\n        Returns:\n            `list[dict[str, Any]]`:\n                The formatted messages as a list of dictionaries.\n        \"\"\"\n        self.assert_list_of_msgs(msgs)\n\n        formatted_msgs: list[dict] = []\n\n        i = 0\n        while i < len(msgs):\n            msg = msgs[i]\n            content_blocks: list[dict[str, Any]] = []\n            tool_calls = []\n\n            for block in msg.get_content_blocks():\n                typ = block.get(\"type\")\n\n                if typ == \"text\":\n                    content_blocks.append(\n                        {\n                            \"text\": block.get(\"text\"),\n                        },\n                    )\n\n                elif typ in [\"image\", \"audio\", \"video\"]:\n                    content_blocks.append(\n                        _format_dashscope_media_block(\n                            block,  # type: ignore[arg-type]\n                        ),\n                    )\n\n                elif typ == \"tool_use\":\n                    tool_calls.append(\n                        {\n                            \"id\": block.get(\"id\"),\n                            \"type\": \"function\",\n                            \"function\": {\n                                \"name\": block.get(\"name\"),\n                                \"arguments\": json.dumps(\n                                    block.get(\"input\", {}),\n                                    ensure_ascii=False,\n                                ),\n                            },\n                        },\n                    )\n\n                elif typ == \"tool_result\":\n                    (\n                        textual_output,\n                        multimodal_data,\n                    ) = self.convert_tool_result_to_string(block[\"output\"])\n\n                    # First add the tool result message in DashScope API format\n                    formatted_msgs.append(\n                        {\n                            \"role\": \"tool\",\n                            \"tool_call_id\": block.get(\"id\"),\n                            \"content\": textual_output,\n                            \"name\": block.get(\"name\"),\n                        },\n                    )\n\n                    # Then, handle the multimodal data if any\n                    promoted_blocks: list = []\n                    for url, multimodal_block in multimodal_data:\n                        if (\n                            multimodal_block[\"type\"] == \"image\"\n                            and self.promote_tool_result_images\n                        ):\n                            promoted_blocks.extend(\n                                [\n                                    TextBlock(\n                                        type=\"text\",\n                                        text=f\"\\n- The image from '{url}': \",\n                                    ),\n                                    ImageBlock(\n                                        type=\"image\",\n                                        source=URLSource(\n                                            type=\"url\",\n                                            url=url,\n                                        ),\n                                    ),\n                                ],\n                            )\n                        elif (\n                            multimodal_block[\"type\"] == \"audio\"\n                            and self.promote_tool_result_audios\n                        ):\n                            promoted_blocks.extend(\n                                [\n                                    TextBlock(\n                                        type=\"text\",\n                                        text=f\"\\n- The audio from '{url}': \",\n                                    ),\n                                    AudioBlock(\n                                        type=\"audio\",\n                                        source=URLSource(\n                                            type=\"url\",\n                                            url=url,\n                                        ),\n                                    ),\n                                ],\n                            )\n                        elif (\n                            multimodal_block[\"type\"] == \"video\"\n                            and self.promote_tool_result_videos\n                        ):\n                            promoted_blocks.extend(\n                                [\n                                    TextBlock(\n                                        type=\"text\",\n                                        text=f\"\\n- The video from '{url}': \",\n                                    ),\n                                    VideoBlock(\n                                        type=\"video\",\n                                        source=URLSource(\n                                            type=\"url\",\n                                            url=url,\n                                        ),\n                                    ),\n                                ],\n                            )\n\n                    if promoted_blocks:\n                        # Insert promoted blocks as new user message(s)\n                        promoted_blocks = [\n                            TextBlock(\n                                type=\"text\",\n                                text=\"<system-info>The following are \"\n                                f\"the media contents from the tool \"\n                                f\"result of '{block['name']}':\",\n                            ),\n                            *promoted_blocks,\n                            TextBlock(\n                                type=\"text\",\n                                text=\"</system-info>\",\n                            ),\n                        ]\n\n                        msgs.insert(\n                            i + 1,\n                            Msg(\n                                name=\"user\",\n                                content=promoted_blocks,\n                                role=\"user\",\n                            ),\n                        )\n\n                else:\n                    logger.warning(\n                        \"Unsupported block type %s in the message, skipped.\",\n                        typ,\n                    )\n\n            msg_dashscope = {\n                \"role\": msg.role,\n                \"content\": content_blocks,\n            }\n\n            if tool_calls:\n                msg_dashscope[\"tool_calls\"] = tool_calls\n\n            if msg_dashscope[\"content\"] or msg_dashscope.get(\"tool_calls\"):\n                formatted_msgs.append(msg_dashscope)\n\n            # Move to next message\n            i += 1\n\n        return _reformat_messages(formatted_msgs)\n\n\nclass DashScopeMultiAgentFormatter(TruncatedFormatterBase):\n    \"\"\"DashScope formatter for multi-agent conversations, where more than\n    a user and an agent are involved.\n\n    .. note:: This formatter will combine previous messages (except tool\n     calls/results) into a history section in the first system message with\n     the conversation history prompt.\n\n    .. note:: For tool calls/results, they will be presented as separate\n     messages as required by the DashScope API. Therefore, the tool calls/\n     results messages are expected to be placed at the end of the input\n     messages.\n\n    .. tip:: Telling the assistant's name in the system prompt is very\n     important in multi-agent conversations. So that LLM can know who it\n     is playing as.\n\n    \"\"\"\n\n    support_tools_api: bool = True\n    \"\"\"Whether support tools API\"\"\"\n\n    support_multiagent: bool = True\n    \"\"\"Whether support multi-agent conversations\"\"\"\n\n    support_vision: bool = True\n    \"\"\"Whether support vision data\"\"\"\n\n    supported_blocks: list[type] = [\n        TextBlock,\n        # Multimodal\n        ImageBlock,\n        AudioBlock,\n        VideoBlock,\n        # Tool use\n        ToolUseBlock,\n        ToolResultBlock,\n    ]\n    \"\"\"The list of supported message blocks\"\"\"\n\n    def __init__(\n        self,\n        conversation_history_prompt: str = (\n            \"# Conversation History\\n\"\n            \"The content between <history></history> tags contains \"\n            \"your conversation history\\n\"\n        ),\n        promote_tool_result_images: bool = False,\n        promote_tool_result_audios: bool = False,\n        promote_tool_result_videos: bool = False,\n        token_counter: TokenCounterBase | None = None,\n        max_tokens: int | None = None,\n    ) -> None:\n        \"\"\"Initialize the DashScope multi-agent formatter.\n\n        Args:\n            conversation_history_prompt (`str`):\n                The prompt to use for the conversation history section.\n            promote_tool_result_images (`bool`, defaults to `False`):\n                Whether to promote images from tool results to user messages.\n                Most LLM APIs don't support images in tool result blocks, but\n                do support them in user message blocks. When `True`, images are\n                extracted and appended as a separate user message with\n                explanatory text indicating their source.\n            promote_tool_result_audios (`bool`, defaults to `False`):\n                Whether to promote audios from tool results to user messages.\n                Most LLM APIs don't support audios in tool result blocks, but\n                do support them in user message blocks. When `True`, audios are\n                extracted and appended as a separate user message with\n                explanatory text indicating their source.\n            promote_tool_result_videos (`bool`, defaults to `False`):\n                Whether to promote videos from tool results to user messages.\n                Most LLM APIs don't support videos in tool result blocks, but\n                do support them in user message blocks. When `True`, videos are\n                extracted and appended as a separate user message with\n                explanatory text indicating their source.\n            token_counter (`TokenCounterBase | None`, optional):\n                The token counter used for truncation.\n            max_tokens (`int | None`, optional):\n                The maximum number of tokens allowed in the formatted\n                messages. If `None`, no truncation will be applied.\n        \"\"\"\n        super().__init__(token_counter=token_counter, max_tokens=max_tokens)\n        self.conversation_history_prompt = conversation_history_prompt\n        self.promote_tool_result_images = promote_tool_result_images\n        self.promote_tool_result_audios = promote_tool_result_audios\n        self.promote_tool_result_videos = promote_tool_result_videos\n\n    async def _format_tool_sequence(\n        self,\n        msgs: list[Msg],\n    ) -> list[dict[str, Any]]:\n        \"\"\"Given a sequence of tool call/result messages, format them into\n        the required format for the DashScope API.\n\n        Args:\n            msgs (`list[Msg]`):\n                The list of messages containing tool calls/results to format.\n\n        Returns:\n            `list[dict[str, Any]]`:\n                A list of dictionaries formatted for the DashScope API.\n        \"\"\"\n        return await DashScopeChatFormatter(\n            promote_tool_result_images=self.promote_tool_result_images,\n            promote_tool_result_audios=self.promote_tool_result_audios,\n            promote_tool_result_videos=self.promote_tool_result_videos,\n        ).format(msgs)\n\n    async def _format_agent_message(\n        self,\n        msgs: list[Msg],\n        is_first: bool = True,\n    ) -> list[dict[str, Any]]:\n        \"\"\"Given a sequence of messages without tool calls/results, format\n        them into a user message with conversation history tags. For the\n        first agent message, it will include the conversation history prompt.\n\n        Args:\n            msgs (`list[Msg]`):\n                A list of Msg objects to be formatted.\n            is_first (`bool`, defaults to `True`):\n                Whether this is the first agent message in the conversation.\n                If `True`, the conversation history prompt will be included.\n\n        Returns:\n            `list[dict[str, Any]]`:\n                A list of dictionaries formatted for the DashScope API.\n        \"\"\"\n\n        if is_first:\n            conversation_history_prompt = self.conversation_history_prompt\n        else:\n            conversation_history_prompt = \"\"\n\n        # Format into required DashScope format\n        formatted_msgs: list[dict] = []\n\n        # Collect the multimodal files\n        conversation_blocks = []\n        accumulated_text = []\n        for msg in msgs:\n            for block in msg.get_content_blocks():\n                if block[\"type\"] == \"text\":\n                    accumulated_text.append(f\"{msg.name}: {block['text']}\")\n\n                elif block[\"type\"] in [\"image\", \"audio\", \"video\"]:\n                    # Handle the accumulated text as a single block\n                    if accumulated_text:\n                        conversation_blocks.append(\n                            {\"text\": \"\\n\".join(accumulated_text)},\n                        )\n                        accumulated_text.clear()\n\n                    if block[\"source\"][\"type\"] == \"url\":\n                        url = block[\"source\"][\"url\"]\n                        if _is_accessible_local_file(url):\n                            conversation_blocks.append(\n                                {\n                                    block[\"type\"]: \"file://\"\n                                    + os.path.abspath(url),\n                                },\n                            )\n                        else:\n                            conversation_blocks.append({block[\"type\"]: url})\n\n                    elif block[\"source\"][\"type\"] == \"base64\":\n                        media_type = block[\"source\"][\"media_type\"]\n                        base64_data = block[\"source\"][\"data\"]\n                        conversation_blocks.append(\n                            {\n                                block[\n                                    \"type\"\n                                ]: f\"data:{media_type};base64,{base64_data}\",\n                            },\n                        )\n\n                    else:\n                        logger.warning(\n                            \"Unsupported block type %s in the message, \"\n                            \"skipped.\",\n                            block[\"type\"],\n                        )\n\n        if accumulated_text:\n            conversation_blocks.append({\"text\": \"\\n\".join(accumulated_text)})\n\n        if conversation_blocks:\n            if conversation_blocks[0].get(\"text\"):\n                conversation_blocks[0][\"text\"] = (\n                    conversation_history_prompt\n                    + \"<history>\\n\"\n                    + conversation_blocks[0][\"text\"]\n                )\n\n            else:\n                conversation_blocks.insert(\n                    0,\n                    {\n                        \"text\": conversation_history_prompt + \"<history>\\n\",\n                    },\n                )\n\n            if conversation_blocks[-1].get(\"text\"):\n                conversation_blocks[-1][\"text\"] += \"\\n</history>\"\n\n            else:\n                conversation_blocks.append({\"text\": \"</history>\"})\n\n            formatted_msgs.append(\n                {\n                    \"role\": \"user\",\n                    \"content\": conversation_blocks,\n                },\n            )\n\n        return _reformat_messages(formatted_msgs)\n\n    async def _format_system_message(\n        self,\n        msg: Msg,\n    ) -> dict[str, Any]:\n        \"\"\"Format system message for DashScope API.\"\"\"\n        return {\n            \"role\": \"system\",\n            \"content\": msg.get_text_content(),\n        }\n"
  },
  {
    "path": "src/agentscope/formatter/_deepseek_formatter.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=too-many-branches\n\"\"\"The DeepSeek formatter module.\"\"\"\nimport json\nfrom typing import Any\n\nfrom ._truncated_formatter_base import TruncatedFormatterBase\nfrom .._logging import logger\nfrom ..message import Msg, TextBlock, ToolUseBlock, ToolResultBlock\nfrom ..token import TokenCounterBase\n\n\nclass DeepSeekChatFormatter(TruncatedFormatterBase):\n    \"\"\"The DeepSeek formatter class for chatbot scenario, where only a user\n    and an agent are involved. We use the `role` field to identify different\n    entities in the conversation.\n    \"\"\"\n\n    support_tools_api: bool = True\n    \"\"\"Whether support tools API\"\"\"\n\n    support_multiagent: bool = False\n    \"\"\"Whether support multi-agent conversations\"\"\"\n\n    support_vision: bool = False\n    \"\"\"Whether support vision data\"\"\"\n\n    supported_blocks: list[type] = [\n        TextBlock,\n        # Tool use\n        ToolUseBlock,\n        ToolResultBlock,\n    ]\n    \"\"\"The list of supported message blocks\"\"\"\n\n    async def _format(\n        self,\n        msgs: list[Msg],\n    ) -> list[dict[str, Any]]:\n        \"\"\"Format message objects into DeepSeek API format.\n\n        Args:\n            msgs (`list[Msg]`):\n                The list of message objects to format.\n\n        Returns:\n            `list[dict[str, Any]]`:\n                The formatted messages as a list of dictionaries.\n        \"\"\"\n        self.assert_list_of_msgs(msgs)\n\n        messages: list[dict] = []\n        for msg in msgs:\n            content_blocks: list = []\n            reasoning_content_blocks: list = []\n            tool_calls = []\n\n            for block in msg.get_content_blocks():\n                typ = block.get(\"type\")\n                if typ == \"text\":\n                    content_blocks.append({**block})\n                elif typ == \"thinking\":\n                    reasoning_content_blocks.append({**block})\n\n                elif typ == \"tool_use\":\n                    tool_calls.append(\n                        {\n                            \"id\": block.get(\"id\"),\n                            \"type\": \"function\",\n                            \"function\": {\n                                \"name\": block.get(\"name\"),\n                                \"arguments\": json.dumps(\n                                    block.get(\"input\", {}),\n                                    ensure_ascii=False,\n                                ),\n                            },\n                        },\n                    )\n\n                elif typ == \"tool_result\":\n                    textual_output, _ = self.convert_tool_result_to_string(\n                        block.get(\"output\"),  # type: ignore[arg-type]\n                    )\n                    messages.append(\n                        {\n                            \"role\": \"tool\",\n                            \"tool_call_id\": block.get(\"id\"),\n                            \"content\": textual_output,\n                            \"name\": block.get(\"name\"),\n                        },\n                    )\n\n                else:\n                    logger.warning(\n                        \"Unsupported block type %s in the message, skipped.\",\n                        typ,\n                    )\n            content_msg = \"\\n\".join(\n                content.get(\"text\", \"\") for content in content_blocks\n            )\n            reasoning_msg = \"\\n\".join(\n                reasoning.get(\"thinking\", \"\")\n                for reasoning in reasoning_content_blocks\n            )\n\n            msg_deepseek = {\n                \"role\": msg.role,\n                \"content\": content_msg or None,\n            }\n\n            if reasoning_msg:\n                msg_deepseek[\"reasoning_content\"] = reasoning_msg\n\n            if tool_calls:\n                msg_deepseek[\"tool_calls\"] = tool_calls\n\n            if msg_deepseek[\"content\"] or msg_deepseek.get(\"tool_calls\"):\n                messages.append(msg_deepseek)\n\n        return messages\n\n\nclass DeepSeekMultiAgentFormatter(TruncatedFormatterBase):\n    \"\"\"\n    DeepSeek formatter for multi-agent conversations, where more than\n    a user and an agent are involved.\n    \"\"\"\n\n    support_tools_api: bool = True\n    \"\"\"Whether support tools API\"\"\"\n\n    support_multiagent: bool = True\n    \"\"\"Whether support multi-agent conversations\"\"\"\n\n    support_vision: bool = False\n    \"\"\"Whether support vision data\"\"\"\n\n    supported_blocks: list[type] = [\n        TextBlock,\n        # Tool use\n        ToolUseBlock,\n        ToolResultBlock,\n    ]\n    \"\"\"The list of supported message blocks\"\"\"\n\n    def __init__(\n        self,\n        conversation_history_prompt: str = (\n            \"# Conversation History\\n\"\n            \"The content between <history></history> tags contains \"\n            \"your conversation history\\n\"\n        ),\n        token_counter: TokenCounterBase | None = None,\n        max_tokens: int | None = None,\n    ) -> None:\n        \"\"\"Initialize the DeepSeek multi-agent formatter.\n\n        Args:\n            conversation_history_prompt (`str`):\n                The prompt to use for the conversation history section.\n            token_counter (`TokenCounterBase | None`, optional):\n                A token counter instance used to count tokens in the messages.\n                If not provided, the formatter will format the messages\n                without considering token limits.\n            max_tokens (`int | None`, optional):\n                The maximum number of tokens allowed in the formatted\n                messages. If not provided, the formatter will not truncate\n                the messages.\n        \"\"\"\n        super().__init__(token_counter=token_counter, max_tokens=max_tokens)\n        self.conversation_history_prompt = conversation_history_prompt\n\n    async def _format_tool_sequence(\n        self,\n        msgs: list[Msg],\n    ) -> list[dict[str, Any]]:\n        \"\"\"Given a sequence of tool call/result messages, format them into\n        the required format for the DeepSeek API.\n\n        Args:\n            msgs (`list[Msg]`):\n                The list of messages containing tool calls/results to format.\n\n        Returns:\n            `list[dict[str, Any]]`:\n                A list of dictionaries formatted for the DeepSeek API.\n        \"\"\"\n        return await DeepSeekChatFormatter().format(msgs)\n\n    async def _format_agent_message(\n        self,\n        msgs: list[Msg],\n        is_first: bool = True,\n    ) -> list[dict[str, Any]]:\n        \"\"\"Given a sequence of messages without tool calls/results, format\n        them into the required format for the DeepSeek API.\n\n        Args:\n            msgs (`list[Msg]`):\n                A list of Msg objects to be formatted.\n            is_first (`bool`, defaults to `True`):\n                Whether this is the first agent message in the conversation.\n                If `True`, the conversation history prompt will be included.\n\n        Returns:\n            `list[dict[str, Any]]`:\n                A list of dictionaries formatted for the DeepSeek API.\n        \"\"\"\n\n        if is_first:\n            conversation_history_prompt = self.conversation_history_prompt\n        else:\n            conversation_history_prompt = \"\"\n\n        # Format into required DeepSeek format\n        formatted_msgs: list[dict] = []\n\n        conversation_blocks: list = []\n        accumulated_text = []\n        for msg in msgs:\n            for block in msg.get_content_blocks():\n                if block[\"type\"] == \"text\":\n                    accumulated_text.append(f\"{msg.name}: {block['text']}\")\n\n        if accumulated_text:\n            conversation_blocks.append(\n                {\"text\": \"\\n\".join(accumulated_text)},\n            )\n\n        if conversation_blocks:\n            if conversation_blocks[0].get(\"text\"):\n                conversation_blocks[0][\"text\"] = (\n                    conversation_history_prompt\n                    + \"<history>\\n\"\n                    + conversation_blocks[0][\"text\"]\n                )\n\n            else:\n                conversation_blocks.insert(\n                    0,\n                    {\n                        \"text\": conversation_history_prompt + \"<history>\\n\",\n                    },\n                )\n\n            if conversation_blocks[-1].get(\"text\"):\n                conversation_blocks[-1][\"text\"] += \"\\n</history>\"\n\n            else:\n                conversation_blocks.append({\"text\": \"</history>\"})\n\n        conversation_blocks_text = \"\\n\".join(\n            conversation_block.get(\"text\", \"\")\n            for conversation_block in conversation_blocks\n        )\n\n        user_message = {\n            \"role\": \"user\",\n            \"content\": conversation_blocks_text,\n        }\n\n        if conversation_blocks:\n            formatted_msgs.append(user_message)\n\n        return formatted_msgs\n"
  },
  {
    "path": "src/agentscope/formatter/_formatter_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The formatter module.\"\"\"\n\nfrom abc import abstractmethod\nfrom typing import Any, List, Tuple, Sequence\n\nfrom .._utils._common import _save_base64_data\nfrom ..message import Msg, AudioBlock, ImageBlock, TextBlock, VideoBlock\n\n\nclass FormatterBase:\n    \"\"\"The base class for formatters.\"\"\"\n\n    @abstractmethod\n    async def format(self, *args: Any, **kwargs: Any) -> list[dict[str, Any]]:\n        \"\"\"Format the Msg objects to a list of dictionaries that satisfy the\n        API requirements.\"\"\"\n\n    @staticmethod\n    def assert_list_of_msgs(msgs: list[Msg]) -> None:\n        \"\"\"Assert that the input is a list of Msg objects.\n\n        Args:\n            msgs (`list[Msg]`):\n                A list of Msg objects to be validated.\n        \"\"\"\n        if not isinstance(msgs, list):\n            raise TypeError(\"Input must be a list of Msg objects.\")\n\n        for msg in msgs:\n            if not isinstance(msg, Msg):\n                raise TypeError(\n                    f\"Expected Msg object, got {type(msg)} instead.\",\n                )\n\n    @staticmethod\n    def convert_tool_result_to_string(\n        output: str | List[TextBlock | ImageBlock | AudioBlock | VideoBlock],\n    ) -> tuple[\n        str,\n        Sequence[\n            Tuple[\n                str,\n                ImageBlock | AudioBlock | TextBlock | VideoBlock,\n            ]\n        ],\n    ]:\n        \"\"\"Turn the tool result list into a textual output to be compatible\n        with the LLM API that doesn't support multimodal data in the tool\n        result.\n\n        For URL-based images, the URL is included in the list. For\n        base64-encoded images, the local file path where the image is saved\n        is included in the returned list.\n\n        Args:\n            output (`str | List[TextBlock | ImageBlock | AudioBlock | \\\n            VideoBlock]`):\n                The output of the tool response, including text and multimodal\n                data like images and audio.\n\n        Returns:\n            `tuple[str, list[Tuple[str, ImageBlock | AudioBlock | VideoBlock \\\n            TextBlock]]]`:\n                A tuple containing the textual representation of the tool\n                result and a list of tuples. The first element of each tuple\n                is the local file path or URL of the multimodal data, and the\n                second element is the corresponding block.\n        \"\"\"\n\n        if isinstance(output, str):\n            return output, []\n\n        textual_output = []\n        multimodal_data = []\n        for block in output:\n            assert isinstance(block, dict) and \"type\" in block, (\n                f\"Invalid block: {block}, a TextBlock, ImageBlock, \"\n                f\"AudioBlock, or VideoBlock is expected.\"\n            )\n            if block[\"type\"] == \"text\":\n                textual_output.append(block[\"text\"])\n\n            elif block[\"type\"] in [\"image\", \"audio\", \"video\"]:\n                assert \"source\" in block, (\n                    f\"Invalid {block['type']} block: {block}, 'source' key \"\n                    \"is required.\"\n                )\n                source = block[\"source\"]\n                # Save the image locally and return the file path\n                if source[\"type\"] == \"url\":\n                    textual_output.append(\n                        f\"The returned {block['type']} can be found \"\n                        f\"at: {source['url']}\",\n                    )\n\n                    path_multimodal_file = source[\"url\"]\n\n                elif source[\"type\"] == \"base64\":\n                    path_multimodal_file = _save_base64_data(\n                        source[\"media_type\"],\n                        source[\"data\"],\n                    )\n                    textual_output.append(\n                        f\"The returned {block['type']} can be found \"\n                        f\"at: {path_multimodal_file}\",\n                    )\n\n                else:\n                    raise ValueError(\n                        f\"Invalid image source: {block['source']}, \"\n                        \"expected 'url' or 'base64'.\",\n                    )\n\n                multimodal_data.append(\n                    (path_multimodal_file, block),\n                )\n\n            else:\n                raise ValueError(\n                    f\"Unsupported block type: {block['type']}, \"\n                    \"expected 'text', 'image', 'audio', or 'video'.\",\n                )\n\n        if len(textual_output) == 1:\n            return textual_output[0], multimodal_data\n\n        else:\n            return \"\\n\".join(\"- \" + _ for _ in textual_output), multimodal_data\n"
  },
  {
    "path": "src/agentscope/formatter/_gemini_formatter.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=too-many-branches\n\"\"\"Google gemini API formatter in agentscope.\"\"\"\nimport base64\nimport os\nfrom typing import Any\nfrom urllib.parse import urlparse\n\nfrom ._truncated_formatter_base import TruncatedFormatterBase\nfrom .._utils._common import _get_bytes_from_web_url\nfrom ..message import (\n    Msg,\n    TextBlock,\n    ImageBlock,\n    AudioBlock,\n    ToolUseBlock,\n    ToolResultBlock,\n    VideoBlock,\n    URLSource,\n)\nfrom .._logging import logger\nfrom ..token import TokenCounterBase\n\n\ndef _format_gemini_media_block(\n    media_block: ImageBlock | AudioBlock | VideoBlock,\n) -> dict[str, Any]:\n    \"\"\"Format an image/audio/video block for Gemini API.\n\n    Args:\n        media_block (`ImageBlock | AudioBlock | VideoBlock`):\n            The media block to format.\n\n    Returns:\n        `dict[str, Any]`:\n            A dictionary with \"inline_data\" key in Gemini format.\n\n    Raises:\n        `ValueError`:\n            If the source type is not supported.\n    \"\"\"\n    source = media_block[\"source\"]\n    if source[\"type\"] == \"base64\":\n        return {\n            \"inline_data\": {\n                \"data\": source[\"data\"],\n                \"mime_type\": source[\"media_type\"],\n            },\n        }\n    elif source[\"type\"] == \"url\":\n        return {\n            \"inline_data\": _to_gemini_inline_data(source[\"url\"]),\n        }\n    else:\n        raise ValueError(\n            f\"Unsupported source type: {source['type']}\",\n        )\n\n\ndef _to_gemini_inline_data(url: str) -> dict:\n    \"\"\"Convert url into the Gemini API required format.\"\"\"\n    parsed_url = urlparse(url)\n    extension = url.split(\".\")[-1].lower()\n\n    # Pre-calculate media type from extension (image/audio/video).\n    typ = None\n    for k, v in GeminiChatFormatter.supported_extensions.items():\n        if extension in v:\n            typ = k\n            break\n\n    if not os.path.exists(url) and parsed_url.scheme != \"\":\n        # Web url\n        if typ is None:\n            raise TypeError(\n                f\"Unsupported file extension: {extension}, expected \"\n                f\"{GeminiChatFormatter.supported_extensions}\",\n            )\n\n        data = _get_bytes_from_web_url(url)\n        return {\n            \"data\": data,\n            \"mime_type\": f\"{typ}/{extension}\",\n        }\n\n    elif os.path.exists(url):\n        # Local file\n        if typ is None:\n            raise TypeError(\n                f\"Unsupported file extension: {extension}, expected \"\n                f\"{GeminiChatFormatter.supported_extensions}\",\n            )\n\n        with open(url, \"rb\") as f:\n            data = base64.b64encode(f.read()).decode(\"utf-8\")\n\n        return {\n            \"data\": data,\n            \"mime_type\": f\"{typ}/{extension}\",\n        }\n\n    raise ValueError(\n        f\"The URL `{url}` is not a valid image URL or local file.\",\n    )\n\n\nclass GeminiChatFormatter(TruncatedFormatterBase):\n    \"\"\"The Gemini formatter class for chatbot scenario, where only a user\n    and an agent are involved. We use the `role` field to identify different\n    entities in the conversation.\n    \"\"\"\n\n    support_tools_api: bool = True\n    \"\"\"Whether support tools API\"\"\"\n\n    support_multiagent: bool = False\n    \"\"\"Whether support multi-agent conversations\"\"\"\n\n    support_vision: bool = True\n    \"\"\"Whether support vision data\"\"\"\n\n    supported_blocks: list[type] = [\n        TextBlock,\n        # Multimodal\n        ImageBlock,\n        VideoBlock,\n        AudioBlock,\n        # Tool use\n        ToolUseBlock,\n        ToolResultBlock,\n    ]\n    \"\"\"The list of supported message blocks\"\"\"\n\n    supported_extensions: dict[str, list[str]] = {\n        \"image\": [\"png\", \"jpeg\", \"webp\", \"heic\", \"heif\"],\n        \"video\": [\n            \"mp4\",\n            \"mpeg\",\n            \"mov\",\n            \"avi\",\n            \"x-flv\",\n            \"mpg\",\n            \"webm\",\n            \"wmv\",\n            \"3gpp\",\n        ],\n        \"audio\": [\"mp3\", \"wav\", \"aiff\", \"aac\", \"ogg\", \"flac\"],\n    }\n\n    def __init__(\n        self,\n        promote_tool_result_images: bool = False,\n        token_counter: TokenCounterBase | None = None,\n        max_tokens: int | None = None,\n    ) -> None:\n        \"\"\"Initialize the Gemini chat formatter.\n\n        Args:\n            promote_tool_result_images (`bool`, defaults to `False`):\n                Whether to promote images from tool results to user messages.\n                Most LLM APIs don't support images in tool result blocks, but\n                do support them in user message blocks. When `True`, images are\n                extracted and appended as a separate user message with\n                explanatory text indicating their source.\n            token_counter (`TokenCounterBase | None`, optional):\n                A token counter instance used to count tokens in the messages.\n                If not provided, the formatter will format the messages\n                without considering token limits.\n            max_tokens (`int | None`, optional):\n                The maximum number of tokens allowed in the formatted\n                messages. If not provided, the formatter will not truncate\n                the messages.\n        \"\"\"\n        super().__init__(token_counter, max_tokens)\n        self.promote_tool_result_images = promote_tool_result_images\n\n    async def _format(\n        self,\n        msgs: list[Msg],\n    ) -> list[dict]:\n        \"\"\"Format message objects into Gemini API required format.\"\"\"\n        self.assert_list_of_msgs(msgs)\n\n        messages: list = []\n        i = 0\n        while i < len(msgs):\n            msg = msgs[i]\n            parts = []\n\n            for block in msg.get_content_blocks():\n                typ = block.get(\"type\")\n                if typ == \"text\":\n                    parts.append(\n                        {\n                            \"text\": block.get(\"text\"),\n                        },\n                    )\n\n                elif typ == \"tool_use\":\n                    parts.append(\n                        {\n                            \"function_call\": {\n                                \"id\": None,\n                                \"name\": block[\"name\"],\n                                \"args\": block[\"input\"],\n                            },\n                            \"thought_signature\": block.get(\"id\", None),\n                        },\n                    )\n\n                elif typ == \"tool_result\":\n                    (\n                        textual_output,\n                        multimodal_data,\n                    ) = self.convert_tool_result_to_string(block[\"output\"])\n\n                    # First add the tool result message in DashScope API format\n                    messages.append(\n                        {\n                            \"role\": \"user\",\n                            \"parts\": [\n                                {\n                                    \"function_response\": {\n                                        \"id\": block[\"id\"],\n                                        \"name\": block[\"name\"],\n                                        \"response\": {\n                                            \"output\": textual_output,\n                                        },\n                                    },\n                                },\n                            ],\n                        },\n                    )\n\n                    promoted_blocks: list = []\n                    for url, multimodal_block in multimodal_data:\n                        if (\n                            multimodal_block[\"type\"] == \"image\"\n                            and self.promote_tool_result_images\n                        ):\n                            promoted_blocks.extend(\n                                [\n                                    TextBlock(\n                                        type=\"text\",\n                                        text=f\"\\n- The image from '{url}': \",\n                                    ),\n                                    ImageBlock(\n                                        type=\"image\",\n                                        source=URLSource(\n                                            type=\"url\",\n                                            url=url,\n                                        ),\n                                    ),\n                                ],\n                            )\n\n                    if promoted_blocks:\n                        # Insert promoted blocks as new user message(s)\n                        promoted_blocks = [\n                            TextBlock(\n                                type=\"text\",\n                                text=\"<system-info>The following are \"\n                                \"the image contents from the tool \"\n                                f\"result of '{block['name']}':\",\n                            ),\n                            *promoted_blocks,\n                            TextBlock(\n                                type=\"text\",\n                                text=\"</system-info>\",\n                            ),\n                        ]\n\n                        msgs.insert(\n                            i + 1,\n                            Msg(\n                                name=\"user\",\n                                content=promoted_blocks,\n                                role=\"user\",\n                            ),\n                        )\n\n                elif typ in [\"image\", \"audio\", \"video\"]:\n                    parts.append(\n                        _format_gemini_media_block(\n                            block,  # type: ignore[arg-type]\n                        ),\n                    )\n\n                else:\n                    logger.warning(\n                        \"Unsupported block type: %s in the message, skipped. \",\n                        typ,\n                    )\n\n            role = \"model\" if msg.role == \"assistant\" else \"user\"\n\n            if parts:\n                messages.append(\n                    {\n                        \"role\": role,\n                        \"parts\": parts,\n                    },\n                )\n\n            # Move to next message (including inserted messages, which will\n            # be processed in subsequent iterations)\n            i += 1\n\n        return messages\n\n\nclass GeminiMultiAgentFormatter(TruncatedFormatterBase):\n    \"\"\"The multi-agent formatter for Google Gemini API, where more than a\n    user and an agent are involved.\n\n    .. note:: This formatter will combine previous messages (except tool\n     calls/results) into a history section in the first system message with\n     the conversation history prompt.\n\n    .. note:: For tool calls/results, they will be presented as separate\n     messages as required by the Gemini API. Therefore, the tool calls/\n     results messages are expected to be placed at the end of the input\n     messages.\n\n    .. tip:: Telling the assistant's name in the system prompt is very\n     important in multi-agent conversations. So that LLM can know who it\n     is playing as.\n\n    \"\"\"\n\n    support_tools_api: bool = True\n    \"\"\"Whether support tools API\"\"\"\n\n    support_multiagent: bool = True\n    \"\"\"Whether support multi-agent conversations\"\"\"\n\n    support_vision: bool = True\n    \"\"\"Whether support vision data\"\"\"\n\n    supported_blocks: list[type] = [\n        TextBlock,\n        # Multimodal\n        ImageBlock,\n        VideoBlock,\n        AudioBlock,\n        # Tool use\n        ToolUseBlock,\n        ToolResultBlock,\n    ]\n    \"\"\"The list of supported message blocks\"\"\"\n\n    def __init__(\n        self,\n        conversation_history_prompt: str = (\n            \"# Conversation History\\n\"\n            \"The content between <history></history> tags contains \"\n            \"your conversation history\\n\"\n        ),\n        promote_tool_result_images: bool = False,\n        token_counter: TokenCounterBase | None = None,\n        max_tokens: int | None = None,\n    ) -> None:\n        \"\"\"Initialize the Gemini multi-agent formatter.\n\n        Args:\n            conversation_history_prompt (`str`):\n                The prompt to be used for the conversation history section.\n            promote_tool_result_images (`bool`, defaults to `False`):\n                Whether to promote images from tool results to user messages.\n                Most LLM APIs don't support images in tool result blocks, but\n                do support them in user message blocks. When `True`, images are\n                extracted and appended as a separate user message with\n                explanatory text indicating their source.\n            token_counter (`TokenCounterBase | None`, optional):\n                The token counter used for truncation.\n            max_tokens (`int | None`, optional):\n                The maximum number of tokens allowed in the formatted\n                messages. If `None`, no truncation will be applied.\n        \"\"\"\n        super().__init__(token_counter=token_counter, max_tokens=max_tokens)\n        self.conversation_history_prompt = conversation_history_prompt\n        self.promote_tool_result_images = promote_tool_result_images\n\n    async def _format_system_message(\n        self,\n        msg: Msg,\n    ) -> dict[str, Any]:\n        \"\"\"Format system message for the Gemini API.\"\"\"\n        return {\n            \"role\": \"user\",\n            \"parts\": [\n                {\n                    \"text\": msg.get_text_content(),\n                },\n            ],\n        }\n\n    async def _format_tool_sequence(\n        self,\n        msgs: list[Msg],\n    ) -> list[dict[str, Any]]:\n        \"\"\"Given a sequence of tool call/result messages, format them into\n        the required format for the Gemini API.\n\n        Args:\n            msgs (`list[Msg]`):\n                The list of messages containing tool calls/results to format.\n\n        Returns:\n            `list[dict[str, Any]]`:\n                A list of dictionaries formatted for the Gemini API.\n        \"\"\"\n        return await GeminiChatFormatter(\n            promote_tool_result_images=self.promote_tool_result_images,\n        ).format(msgs)\n\n    async def _format_agent_message(\n        self,\n        msgs: list[Msg],\n        is_first: bool = True,\n    ) -> list[dict[str, Any]]:\n        \"\"\"Given a sequence of messages without tool calls/results, format\n        them into the required format for the Gemini API.\n\n        Args:\n            msgs (`list[Msg]`):\n                A list of Msg objects to be formatted.\n            is_first (`bool`, defaults to `True`):\n                Whether this is the first agent message in the conversation.\n                If `True`, the conversation history prompt will be included.\n\n        Returns:\n            `list[dict[str, Any]]`:\n                A list of dictionaries formatted for the Gemini API.\n        \"\"\"\n\n        if is_first:\n            conversation_history_prompt = self.conversation_history_prompt\n        else:\n            conversation_history_prompt = \"\"\n\n        # Format into Gemini API required format\n        formatted_msgs: list = []\n\n        # Collect the multimodal files\n        conversation_parts: list = []\n        accumulated_text = []\n        for msg in msgs:\n            for block in msg.get_content_blocks():\n                if block[\"type\"] == \"text\":\n                    accumulated_text.append(f\"{msg.name}: {block['text']}\")\n\n                elif block[\"type\"] in [\"image\", \"video\", \"audio\"]:\n                    # handle the accumulated text as a single part if exists\n                    if accumulated_text:\n                        conversation_parts.append(\n                            {\n                                \"text\": \"\\n\".join(accumulated_text),\n                            },\n                        )\n                        accumulated_text.clear()\n\n                    # handle the multimodal data\n                    conversation_parts.append(\n                        _format_gemini_media_block(\n                            block,  # type: ignore[arg-type]\n                        ),\n                    )\n\n        if accumulated_text:\n            conversation_parts.append(\n                {\n                    \"text\": \"\\n\".join(accumulated_text),\n                },\n            )\n\n        # Add prompt and <history></history> tags around conversation history\n        if conversation_parts:\n            if conversation_parts[0].get(\"text\"):\n                conversation_parts[0][\"text\"] = (\n                    conversation_history_prompt\n                    + \"<history>\"\n                    + conversation_parts[0][\"text\"]\n                )\n\n            else:\n                conversation_parts.insert(\n                    0,\n                    {\"text\": conversation_history_prompt + \"<history>\"},\n                )\n\n            if conversation_parts[-1].get(\"text\"):\n                conversation_parts[-1][\"text\"] += \"\\n</history>\"\n\n            else:\n                conversation_parts.append(\n                    {\"text\": \"</history>\"},\n                )\n\n            formatted_msgs.append(\n                {\n                    \"role\": \"user\",\n                    \"parts\": conversation_parts,\n                },\n            )\n\n        return formatted_msgs\n"
  },
  {
    "path": "src/agentscope/formatter/_ollama_formatter.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=too-many-branches\n\"\"\"The Ollama formatter module.\"\"\"\nimport base64\nimport os\nfrom typing import Any\nfrom urllib.parse import urlparse\n\nfrom ._truncated_formatter_base import TruncatedFormatterBase\nfrom .._logging import logger\nfrom .._utils._common import _get_bytes_from_web_url\nfrom ..message import (\n    Msg,\n    TextBlock,\n    ImageBlock,\n    ToolUseBlock,\n    ToolResultBlock,\n    URLSource,\n)\nfrom ..token import TokenCounterBase\n\n\ndef _format_ollama_image_block(\n    image_block: ImageBlock,\n) -> str:\n    \"\"\"Format an image block for Ollama API.\n\n    Args:\n        image_block (`ImageBlock`):\n            The image block to format.\n\n    Returns:\n        `str`:\n            Base64 encoded image data as a string.\n\n    Raises:\n        `ValueError`:\n            If the source type is not supported.\n    \"\"\"\n    source = image_block[\"source\"]\n    if source[\"type\"] == \"url\":\n        return _convert_ollama_image_url_to_base64_data(source[\"url\"])\n    elif source[\"type\"] == \"base64\":\n        return source[\"data\"]\n    else:\n        raise ValueError(\n            f\"Unsupported image source type: {source['type']}\",\n        )\n\n\ndef _convert_ollama_image_url_to_base64_data(url: str) -> str:\n    \"\"\"Convert image url to base64.\"\"\"\n    parsed_url = urlparse(url)\n\n    if not os.path.exists(url) and parsed_url.scheme != \"\":\n        # Web url\n        data = _get_bytes_from_web_url(url)\n        return data\n    if os.path.exists(url):\n        # Local file\n        with open(url, \"rb\") as f:\n            data = base64.b64encode(f.read()).decode(\"utf-8\")\n\n        return data\n\n    raise ValueError(\n        f\"The URL `{url}` is not a valid image URL or local file.\",\n    )\n\n\nclass OllamaChatFormatter(TruncatedFormatterBase):\n    \"\"\"The Ollama formatter class for chatbot scenario, where only a user\n    and an agent are involved. We use the `role` field to identify different\n    participants in the conversation.\n    \"\"\"\n\n    support_tools_api: bool = True\n    \"\"\"Whether support tools API\"\"\"\n\n    support_multiagent: bool = False\n    \"\"\"Whether support multi-agent conversations\"\"\"\n\n    support_vision: bool = True\n    \"\"\"Whether support vision data\"\"\"\n\n    supported_blocks: list[type] = [\n        TextBlock,\n        # Multimodal\n        ImageBlock,\n        # Tool use\n        ToolUseBlock,\n        ToolResultBlock,\n    ]\n    \"\"\"The list of supported message blocks\"\"\"\n\n    def __init__(\n        self,\n        promote_tool_result_images: bool = False,\n        token_counter: TokenCounterBase | None = None,\n        max_tokens: int | None = None,\n    ) -> None:\n        \"\"\"Initialize the Ollama chat formatter.\n\n        Args:\n            promote_tool_result_images (`bool`, defaults to `False`):\n                Whether to promote images from tool results to user messages.\n                Most LLM APIs don't support images in tool result blocks, but\n                do support them in user message blocks. When `True`, images are\n                extracted and appended as a separate user message with\n                explanatory text indicating their source.\n            token_counter (`TokenCounterBase | None`, optional):\n                A token counter instance used to count tokens in the messages.\n                If not provided, the formatter will format the messages\n                without considering token limits.\n            max_tokens (`int | None`, optional):\n                The maximum number of tokens allowed in the formatted\n                messages. If not provided, the formatter will not truncate\n                the messages.\n        \"\"\"\n        super().__init__(token_counter, max_tokens)\n        self.promote_tool_result_images = promote_tool_result_images\n\n    async def _format(\n        self,\n        msgs: list[Msg],\n    ) -> list[dict[str, Any]]:\n        \"\"\"Format message objects into Ollama API format.\n\n        Args:\n            msgs (`list[Msg]`):\n                The list of message objects to format.\n\n        Returns:\n            `list[dict[str, Any]]`:\n                The formatted messages as a list of dictionaries.\n        \"\"\"\n        self.assert_list_of_msgs(msgs)\n\n        messages: list = []\n        i = 0\n        while i < len(msgs):\n            msg = msgs[i]\n            content_blocks: list = []\n            tool_calls = []\n            images = []\n\n            for block in msg.get_content_blocks():\n                typ = block.get(\"type\")\n                if typ == \"text\":\n                    content_blocks.append({**block})\n\n                elif typ == \"tool_use\":\n                    tool_calls.append(\n                        {\n                            \"id\": block.get(\"id\"),\n                            \"type\": \"function\",\n                            \"function\": {\n                                \"name\": block.get(\"name\"),\n                                \"arguments\": block.get(\"input\", {}),\n                            },\n                        },\n                    )\n\n                elif typ == \"tool_result\":\n                    (\n                        textual_output,\n                        multimodal_data,\n                    ) = self.convert_tool_result_to_string(block[\"output\"])\n\n                    messages.append(\n                        {\n                            \"role\": \"tool\",\n                            \"tool_call_id\": block.get(\"id\"),\n                            \"content\": textual_output,\n                            \"name\": block.get(\"name\"),\n                        },\n                    )\n\n                    # Then, handle the multimodal data if any\n                    promoted_blocks: list = []\n                    for url, multimodal_block in multimodal_data:\n                        if (\n                            multimodal_block[\"type\"] == \"image\"\n                            and self.promote_tool_result_images\n                        ):\n                            promoted_blocks.extend(\n                                [\n                                    TextBlock(\n                                        type=\"text\",\n                                        text=f\"\\n- The image from '{url}': \",\n                                    ),\n                                    ImageBlock(\n                                        type=\"image\",\n                                        source=URLSource(\n                                            type=\"url\",\n                                            url=url,\n                                        ),\n                                    ),\n                                ],\n                            )\n\n                    if promoted_blocks:\n                        # Insert promoted blocks as new user message(s)\n                        promoted_blocks = [\n                            TextBlock(\n                                type=\"text\",\n                                text=\"<system-info>The following are \"\n                                \"the image contents from the tool \"\n                                f\"result of '{block['name']}':\",\n                            ),\n                            *promoted_blocks,\n                            TextBlock(\n                                type=\"text\",\n                                text=\"</system-info>\",\n                            ),\n                        ]\n\n                        msgs.insert(\n                            i + 1,\n                            Msg(\n                                name=\"user\",\n                                content=promoted_blocks,\n                                role=\"user\",\n                            ),\n                        )\n\n                elif typ == \"image\":\n                    images.append(\n                        _format_ollama_image_block(\n                            block,  # type: ignore[arg-type]\n                        ),\n                    )\n\n                else:\n                    logger.warning(\n                        \"Unsupported block type %s in the message, skipped.\",\n                        typ,\n                    )\n            content_msg = \"\\n\".join(\n                content.get(\"text\", \"\") for content in content_blocks\n            )\n            msg_ollama = {\n                \"role\": msg.role,\n                \"content\": content_msg or None,\n            }\n\n            if tool_calls:\n                msg_ollama[\"tool_calls\"] = tool_calls\n\n            if images:\n                msg_ollama[\"images\"] = images\n\n            if (\n                msg_ollama[\"content\"]\n                or msg_ollama.get(\"images\")\n                or msg_ollama.get(\"tool_calls\")\n            ):\n                messages.append(msg_ollama)\n\n            # Move to next message\n            i += 1\n\n        return messages\n\n\nclass OllamaMultiAgentFormatter(TruncatedFormatterBase):\n    \"\"\"\n    Ollama formatter for multi-agent conversations, where more than\n    a user and an agent are involved.\n    \"\"\"\n\n    support_tools_api: bool = True\n    \"\"\"Whether support tools API\"\"\"\n\n    support_multiagent: bool = True\n    \"\"\"Whether support multi-agent conversations\"\"\"\n\n    support_vision: bool = True\n    \"\"\"Whether support vision data\"\"\"\n\n    supported_blocks: list[type] = [\n        TextBlock,\n        # Multimodal\n        ImageBlock,\n        # Tool use\n        ToolUseBlock,\n        ToolResultBlock,\n    ]\n    \"\"\"The list of supported message blocks\"\"\"\n\n    def __init__(\n        self,\n        conversation_history_prompt: str = (\n            \"# Conversation History\\n\"\n            \"The content between <history></history> tags contains \"\n            \"your conversation history\\n\"\n        ),\n        promote_tool_result_images: bool = False,\n        token_counter: TokenCounterBase | None = None,\n        max_tokens: int | None = None,\n    ) -> None:\n        \"\"\"Initialize the Ollama multi-agent formatter.\n\n        Args:\n            conversation_history_prompt (`str`):\n                The prompt to use for the conversation history section.\n            promote_tool_result_images (`bool`, defaults to `False`):\n                Whether to promote images from tool results to user messages.\n                Most LLM APIs don't support images in tool result blocks, but\n                do support them in user message blocks. When `True`, images are\n                extracted and appended as a separate user message with\n                explanatory text indicating their source.\n            token_counter (`TokenCounterBase | None`, optional):\n                The token counter used for truncation.\n            max_tokens (`int | None`, optional):\n                The maximum number of tokens allowed in the formatted\n                messages. If `None`, no truncation will be applied.\n        \"\"\"\n        super().__init__(token_counter=token_counter, max_tokens=max_tokens)\n        self.conversation_history_prompt = conversation_history_prompt\n        self.promote_tool_result_images = promote_tool_result_images\n\n    async def _format_system_message(\n        self,\n        msg: Msg,\n    ) -> dict[str, Any]:\n        \"\"\"Format system message for the Ollama API.\"\"\"\n        return {\n            \"role\": \"system\",\n            \"content\": msg.get_text_content(),\n        }\n\n    async def _format_tool_sequence(\n        self,\n        msgs: list[Msg],\n    ) -> list[dict[str, Any]]:\n        \"\"\"Given a sequence of tool call/result messages, format them into\n        the required format for the Ollama API.\n\n        Args:\n            msgs (`list[Msg]`):\n                The list of messages containing tool calls/results to format.\n\n        Returns:\n            `list[dict[str, Any]]`:\n                A list of dictionaries formatted for the Ollama API.\n        \"\"\"\n        return await OllamaChatFormatter(\n            promote_tool_result_images=self.promote_tool_result_images,\n        ).format(msgs)\n\n    async def _format_agent_message(\n        self,\n        msgs: list[Msg],\n        is_first: bool = True,\n    ) -> list[dict[str, Any]]:\n        \"\"\"Given a sequence of messages without tool calls/results, format\n        them into the required format for the Ollama API.\n\n        Args:\n            msgs (`list[Msg]`):\n                A list of Msg objects to be formatted.\n            is_first (`bool`, defaults to `True`):\n                Whether this is the first agent message in the conversation.\n                If `True`, the conversation history prompt will be included.\n\n        Returns:\n            `list[dict[str, Any]]`:\n                A list of dictionaries formatted for the ollama API.\n        \"\"\"\n\n        if is_first:\n            conversation_history_prompt = self.conversation_history_prompt\n        else:\n            conversation_history_prompt = \"\"\n\n        # Format into required Ollama format\n        formatted_msgs: list[dict] = []\n\n        # Collect the multimodal files\n        conversation_blocks: list = []\n        accumulated_text = []\n        images = []\n        for msg in msgs:\n            for block in msg.get_content_blocks():\n                if block[\"type\"] == \"text\":\n                    accumulated_text.append(f\"{msg.name}: {block['text']}\")\n\n                elif block[\"type\"] == \"image\":\n                    # Handle the accumulated text as a single block\n                    if accumulated_text:\n                        conversation_blocks.append(\n                            {\"text\": \"\\n\".join(accumulated_text)},\n                        )\n                        accumulated_text.clear()\n\n                    images.append(_format_ollama_image_block(block))\n                    conversation_blocks.append({**block})\n\n        if accumulated_text:\n            conversation_blocks.append(\n                {\"text\": \"\\n\".join(accumulated_text)},\n            )\n\n        if conversation_blocks:\n            if conversation_blocks[0].get(\"text\"):\n                conversation_blocks[0][\"text\"] = (\n                    conversation_history_prompt\n                    + \"<history>\\n\"\n                    + conversation_blocks[0][\"text\"]\n                )\n\n            else:\n                conversation_blocks.insert(\n                    0,\n                    {\n                        \"text\": conversation_history_prompt + \"<history>\\n\",\n                    },\n                )\n\n            if conversation_blocks[-1].get(\"text\"):\n                conversation_blocks[-1][\"text\"] += \"\\n</history>\"\n\n            else:\n                conversation_blocks.append({\"text\": \"</history>\"})\n\n        conversation_blocks_text = \"\\n\".join(\n            conversation_block.get(\"text\", \"\")\n            for conversation_block in conversation_blocks\n        )\n\n        user_message = {\n            \"role\": \"user\",\n            \"content\": conversation_blocks_text,\n        }\n        if images:\n            user_message[\"images\"] = images\n        if conversation_blocks:\n            formatted_msgs.append(user_message)\n\n        return formatted_msgs\n"
  },
  {
    "path": "src/agentscope/formatter/_openai_formatter.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=too-many-branches, too-many-nested-blocks\n\"\"\"The OpenAI formatter for agentscope.\"\"\"\nimport base64\nimport json\nimport os\nfrom typing import Any\nfrom urllib.parse import urlparse\n\nimport requests\n\nfrom ._truncated_formatter_base import TruncatedFormatterBase\nfrom .._logging import logger\nfrom ..message import (\n    Msg,\n    URLSource,\n    TextBlock,\n    ImageBlock,\n    AudioBlock,\n    Base64Source,\n    ToolUseBlock,\n    ToolResultBlock,\n)\nfrom ..token import TokenCounterBase\n\n\ndef _format_openai_image_block(\n    image_block: ImageBlock,\n) -> dict[str, Any]:\n    \"\"\"Format an image block for OpenAI API.\n\n    Args:\n        image_block (`ImageBlock`):\n            The image block to format.\n\n    Returns:\n        `dict[str, Any]`:\n            A dictionary with \"type\" and \"image_url\" keys in OpenAI format.\n\n    Raises:\n        `ValueError`:\n            If the source type is not supported.\n    \"\"\"\n    source = image_block[\"source\"]\n    if source[\"type\"] == \"url\":\n        url = _to_openai_image_url(source[\"url\"])\n    elif source[\"type\"] == \"base64\":\n        data = source[\"data\"]\n        media_type = source[\"media_type\"]\n        url = f\"data:{media_type};base64,{data}\"\n    else:\n        raise ValueError(\n            f\"Unsupported image source type: {source['type']}\",\n        )\n\n    return {\n        \"type\": \"image_url\",\n        \"image_url\": {\n            \"url\": url,\n        },\n    }\n\n\ndef _to_openai_image_url(url: str) -> str:\n    \"\"\"Convert an image url to openai format. If the given url is a local\n    file, it will be converted to base64 format. Otherwise, it will be\n    returned directly.\n\n    Args:\n        url (`str`):\n            The local or public url of the image.\n    \"\"\"\n    # See https://platform.openai.com/docs/guides/vision for details of\n    # support image extensions.\n    support_image_extensions = (\n        \".png\",\n        \".jpg\",\n        \".jpeg\",\n        \".gif\",\n        \".webp\",\n    )\n\n    raw_url = url.removeprefix(\"file://\")\n    # For local files\n    if os.path.exists(raw_url) and os.path.isfile(raw_url):\n        if any(raw_url.endswith(_) for _ in support_image_extensions):\n            with open(raw_url, \"rb\") as image_file:\n                base64_image = base64.b64encode(image_file.read()).decode(\n                    \"utf-8\",\n                )\n            extension = raw_url.lower().split(\".\")[-1]\n            mime_type = f\"image/{extension}\"\n            return f\"data:{mime_type};base64,{base64_image}\"\n\n    # For web urls\n    parsed_url = urlparse(raw_url)\n    if parsed_url.scheme not in [\"\", \"file\"]:\n        return url\n\n    raise ValueError(\n        f'Invalid image URL: \"{url}\". It should be a local file or a web URL.',\n    )\n\n\ndef _to_openai_audio_data(source: URLSource | Base64Source) -> dict:\n    \"\"\"Covert an audio source to OpenAI format.\"\"\"\n    if source[\"type\"] == \"url\":\n        extension = source[\"url\"].split(\".\")[-1].lower()\n        if extension not in [\"wav\", \"mp3\"]:\n            raise TypeError(\n                f\"Unsupported audio file extension: {extension}, \"\n                \"wav and mp3 are supported.\",\n            )\n\n        parsed_url = urlparse(source[\"url\"])\n\n        if os.path.exists(source[\"url\"]):\n            with open(source[\"url\"], \"rb\") as audio_file:\n                data = base64.b64encode(audio_file.read()).decode(\"utf-8\")\n\n        # web url\n        elif parsed_url.scheme != \"\":\n            response = requests.get(source[\"url\"])\n            response.raise_for_status()\n            data = base64.b64encode(response.content).decode(\"utf-8\")\n\n        else:\n            raise ValueError(\n                f\"Unsupported audio source: {source['url']}, \"\n                \"it should be a local file or a web URL.\",\n            )\n\n        return {\n            \"data\": data,\n            \"format\": extension,\n        }\n\n    if source[\"type\"] == \"base64\":\n        data = source[\"data\"]\n        media_type = source[\"media_type\"]\n\n        if media_type not in [\"audio/wav\", \"audio/mp3\"]:\n            raise TypeError(\n                f\"Unsupported audio media type: {media_type}, \"\n                \"only audio/wav and audio/mp3 are supported.\",\n            )\n\n        return {\n            \"data\": data,\n            \"format\": media_type.split(\"/\")[-1],\n        }\n\n    raise TypeError(f\"Unsupported audio source: {source['type']}.\")\n\n\nclass OpenAIChatFormatter(TruncatedFormatterBase):\n    \"\"\"The OpenAI formatter class for chatbot scenario, where only a user\n    and an agent are involved. We use the `name` field in OpenAI API to\n    identify different entities in the conversation.\n    \"\"\"\n\n    support_tools_api: bool = True\n    \"\"\"Whether support tools API\"\"\"\n\n    support_multiagent: bool = True\n    \"\"\"Whether support multi-agent conversation\"\"\"\n\n    support_vision: bool = True\n    \"\"\"Whether support vision models\"\"\"\n\n    supported_blocks: list[type] = [\n        TextBlock,\n        ImageBlock,\n        AudioBlock,\n        ToolUseBlock,\n        ToolResultBlock,\n    ]\n    \"\"\"Supported message blocks for OpenAI API\"\"\"\n\n    def __init__(\n        self,\n        promote_tool_result_images: bool = False,\n        token_counter: TokenCounterBase | None = None,\n        max_tokens: int | None = None,\n    ) -> None:\n        \"\"\"Initialize the OpenAI chat formatter.\n\n        Args:\n            promote_tool_result_images (`bool`, defaults to `False`):\n                Whether to promote images from tool results to user messages.\n                Most LLM APIs don't support images in tool result blocks, but\n                do support them in user message blocks. When `True`, images are\n                extracted and appended as a separate user message with\n                explanatory text indicating their source.\n            token_counter (`TokenCounterBase | None`, optional):\n                A token counter instance used to count tokens in the messages.\n                If not provided, the formatter will format the messages\n                without considering token limits.\n            max_tokens (`int | None`, optional):\n                The maximum number of tokens allowed in the formatted\n                messages. If not provided, the formatter will not truncate\n                the messages.\n        \"\"\"\n        super().__init__(token_counter=token_counter, max_tokens=max_tokens)\n        self.promote_tool_result_images = promote_tool_result_images\n\n    async def _format(\n        self,\n        msgs: list[Msg],\n    ) -> list[dict[str, Any]]:\n        \"\"\"Format message objects into OpenAI API required format.\n\n        Args:\n            msgs (`list[Msg]`):\n                The list of Msg objects to format.\n\n        Returns:\n            `list[dict[str, Any]]`:\n                A list of dictionaries, where each dictionary has \"name\",\n                \"role\", and \"content\" keys.\n        \"\"\"\n        self.assert_list_of_msgs(msgs)\n\n        messages: list[dict] = []\n        i = 0\n        while i < len(msgs):\n            msg = msgs[i]\n            content_blocks = []\n            tool_calls = []\n\n            for block in msg.get_content_blocks():\n                typ = block.get(\"type\")\n                if typ == \"text\":\n                    content_blocks.append({**block})\n\n                elif typ == \"tool_use\":\n                    tool_calls.append(\n                        {\n                            \"id\": block.get(\"id\"),\n                            \"type\": \"function\",\n                            \"function\": {\n                                \"name\": block.get(\"name\"),\n                                \"arguments\": json.dumps(\n                                    block.get(\"input\", {}),\n                                    ensure_ascii=False,\n                                ),\n                            },\n                        },\n                    )\n\n                elif typ == \"tool_result\":\n                    (\n                        textual_output,\n                        multimodal_data,\n                    ) = self.convert_tool_result_to_string(block[\"output\"])\n\n                    messages.append(\n                        {\n                            \"role\": \"tool\",\n                            \"tool_call_id\": block.get(\"id\"),\n                            \"content\": (  # type: ignore[arg-type]\n                                textual_output\n                            ),\n                            \"name\": block.get(\"name\"),\n                        },\n                    )\n\n                    # Then, handle the multimodal data if any\n                    promoted_blocks: list = []\n                    for url, multimodal_block in multimodal_data:\n                        if (\n                            multimodal_block[\"type\"] == \"image\"\n                            and self.promote_tool_result_images\n                        ):\n                            promoted_blocks.extend(\n                                [\n                                    TextBlock(\n                                        type=\"text\",\n                                        text=f\"\\n- The image from '{url}': \",\n                                    ),\n                                    ImageBlock(\n                                        type=\"image\",\n                                        source=URLSource(\n                                            type=\"url\",\n                                            url=url,\n                                        ),\n                                    ),\n                                ],\n                            )\n\n                    if promoted_blocks:\n                        # Insert promoted blocks as new user message(s)\n                        promoted_blocks = [\n                            TextBlock(\n                                type=\"text\",\n                                text=\"<system-info>The following are \"\n                                \"the image contents from the tool \"\n                                f\"result of '{block['name']}':\",\n                            ),\n                            *promoted_blocks,\n                            TextBlock(\n                                type=\"text\",\n                                text=\"</system-info>\",\n                            ),\n                        ]\n\n                        msgs.insert(\n                            i + 1,\n                            Msg(\n                                name=\"user\",\n                                content=promoted_blocks,\n                                role=\"user\",\n                            ),\n                        )\n\n                elif typ == \"image\":\n                    content_blocks.append(\n                        _format_openai_image_block(\n                            block,  # type: ignore[arg-type]\n                        ),\n                    )\n\n                elif typ == \"audio\":\n                    # Filter out audio content when the multimodal model\n                    # outputs both text and audio, to prevent errors in\n                    # subsequent model calls\n                    if msg.role == \"assistant\":\n                        continue\n                    input_audio = _to_openai_audio_data(block[\"source\"])\n                    content_blocks.append(\n                        {\n                            \"type\": \"input_audio\",\n                            \"input_audio\": input_audio,\n                        },\n                    )\n\n                else:\n                    logger.warning(\n                        \"Unsupported block type %s in the message, skipped.\",\n                        typ,\n                    )\n\n            msg_openai = {\n                \"role\": msg.role,\n                \"name\": msg.name,\n                \"content\": content_blocks or None,\n            }\n\n            if tool_calls:\n                msg_openai[\"tool_calls\"] = tool_calls\n\n            # When both content and tool_calls are None, skipped\n            if msg_openai[\"content\"] or msg_openai.get(\"tool_calls\"):\n                messages.append(msg_openai)\n\n            # Move to next message\n            i += 1\n\n        return messages\n\n\nclass OpenAIMultiAgentFormatter(TruncatedFormatterBase):\n    \"\"\"\n    OpenAI formatter for multi-agent conversations, where more than\n    a user and an agent are involved.\n    .. tip:: This formatter is compatible with OpenAI API and\n    OpenAI-compatible services like vLLM, Azure OpenAI, and others.\n    \"\"\"\n\n    support_tools_api: bool = True\n    \"\"\"Whether support tools API\"\"\"\n\n    support_multiagent: bool = True\n    \"\"\"Whether support multi-agent conversation\"\"\"\n\n    support_vision: bool = True\n    \"\"\"Whether support vision models\"\"\"\n\n    supported_blocks: list[type] = [\n        TextBlock,\n        ImageBlock,\n        AudioBlock,\n        ToolUseBlock,\n        ToolResultBlock,\n    ]\n    \"\"\"Supported message blocks for OpenAI API\"\"\"\n\n    def __init__(\n        self,\n        conversation_history_prompt: str = (\n            \"# Conversation History\\n\"\n            \"The content between <history></history> tags contains \"\n            \"your conversation history\\n\"\n        ),\n        promote_tool_result_images: bool = False,\n        token_counter: TokenCounterBase | None = None,\n        max_tokens: int | None = None,\n    ) -> None:\n        \"\"\"Initialize the OpenAI multi-agent formatter.\n\n        Args:\n            conversation_history_prompt (`str`):\n                The prompt to use for the conversation history section.\n            promote_tool_result_images (`bool`, defaults to `False`):\n                Whether to promote images from tool results to user messages.\n                Most LLM APIs don't support images in tool result blocks, but\n                do support them in user message blocks. When `True`, images are\n                extracted and appended as a separate user message with\n                explanatory text indicating their source.\n            token_counter (`TokenCounterBase | None`, optional):\n                A token counter instance used to count tokens in the messages.\n                If not provided, the formatter will format the messages\n                without considering token limits.\n            max_tokens (`int | None`, optional):\n                The maximum number of tokens allowed in the formatted\n                messages. If not provided, the formatter will not truncate\n                the messages.\n        \"\"\"\n        super().__init__(token_counter=token_counter, max_tokens=max_tokens)\n        self.conversation_history_prompt = conversation_history_prompt\n        self.promote_tool_result_images = promote_tool_result_images\n\n    async def _format_tool_sequence(\n        self,\n        msgs: list[Msg],\n    ) -> list[dict[str, Any]]:\n        \"\"\"Given a sequence of tool call/result messages, format them into\n        the required format for the OpenAI API.\"\"\"\n        return await OpenAIChatFormatter(\n            promote_tool_result_images=self.promote_tool_result_images,\n        ).format(msgs)\n\n    async def _format_agent_message(\n        self,\n        msgs: list[Msg],\n        is_first: bool = True,\n    ) -> list[dict[str, Any]]:\n        \"\"\"Given a sequence of messages without tool calls/results, format\n        them into the required format for the OpenAI API.\"\"\"\n\n        if is_first:\n            conversation_history_prompt = self.conversation_history_prompt\n        else:\n            conversation_history_prompt = \"\"\n\n        # Format into required OpenAI format\n        formatted_msgs: list[dict] = []\n\n        conversation_blocks: list = []\n        accumulated_text = []\n        images = []\n        audios = []\n\n        for msg in msgs:\n            for block in msg.get_content_blocks():\n                if block[\"type\"] == \"text\":\n                    accumulated_text.append(f\"{msg.name}: {block['text']}\")\n\n                elif block[\"type\"] == \"image\":\n                    images.append(_format_openai_image_block(block))\n                elif block[\"type\"] == \"audio\":\n                    # Filter out audio content when the multimodal model\n                    # outputs both text and audio, to prevent errors in\n                    # subsequent model calls\n                    if msg.role == \"assistant\":\n                        continue\n                    input_audio = _to_openai_audio_data(block[\"source\"])\n                    audios.append(\n                        {\n                            \"type\": \"input_audio\",\n                            \"input_audio\": input_audio,\n                        },\n                    )\n\n        if accumulated_text:\n            conversation_blocks.append(\n                {\"text\": \"\\n\".join(accumulated_text)},\n            )\n\n        if conversation_blocks:\n            if conversation_blocks[0].get(\"text\"):\n                conversation_blocks[0][\"text\"] = (\n                    conversation_history_prompt\n                    + \"<history>\\n\"\n                    + conversation_blocks[0][\"text\"]\n                )\n\n            else:\n                conversation_blocks.insert(\n                    0,\n                    {\n                        \"text\": conversation_history_prompt + \"<history>\\n\",\n                    },\n                )\n\n            if conversation_blocks[-1].get(\"text\"):\n                conversation_blocks[-1][\"text\"] += \"\\n</history>\"\n\n            else:\n                conversation_blocks.append({\"text\": \"</history>\"})\n\n        conversation_blocks_text = \"\\n\".join(\n            conversation_block.get(\"text\", \"\")\n            for conversation_block in conversation_blocks\n        )\n\n        content_list: list[dict[str, Any]] = []\n        if conversation_blocks_text:\n            content_list.append(\n                {\n                    \"type\": \"text\",\n                    \"text\": conversation_blocks_text,\n                },\n            )\n        if images:\n            content_list.extend(images)\n        if audios:\n            content_list.extend(audios)\n\n        user_message = {\n            \"role\": \"user\",\n            \"content\": content_list,\n        }\n\n        if content_list:\n            formatted_msgs.append(user_message)\n\n        return formatted_msgs\n"
  },
  {
    "path": "src/agentscope/formatter/_truncated_formatter_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The truncated formatter base class, which allows to truncate the input\nmessages.\"\"\"\nfrom abc import ABC\nfrom copy import deepcopy\nfrom typing import (\n    Any,\n    Tuple,\n    Literal,\n    AsyncGenerator,\n)\n\nfrom ._formatter_base import FormatterBase\nfrom ..message import Msg\nfrom ..token import TokenCounterBase\nfrom ..tracing import trace_format\n\n\nclass TruncatedFormatterBase(FormatterBase, ABC):\n    \"\"\"Base class for truncated formatters, which formats input messages into\n    required formats with tokens under a specified limit.\"\"\"\n\n    def __init__(\n        self,\n        token_counter: TokenCounterBase | None = None,\n        max_tokens: int | None = None,\n    ) -> None:\n        \"\"\"Initialize the TruncatedFormatterBase.\n\n        Args:\n            token_counter (`TokenCounterBase | None`, optional):\n                A token counter instance used to count tokens in the messages.\n                If not provided, the formatter will format the messages\n                without considering token limits.\n            max_tokens (`int | None`, optional):\n                The maximum number of tokens allowed in the formatted\n                messages. If not provided, the formatter will not truncate\n                the messages.\n        \"\"\"\n        self.token_counter = token_counter\n\n        assert (\n            max_tokens is None or 0 < max_tokens\n        ), \"max_tokens must be greater than 0\"\n        self.max_tokens = max_tokens\n\n    @trace_format\n    async def format(\n        self,\n        msgs: list[Msg],\n        **kwargs: Any,\n    ) -> list[dict[str, Any]]:\n        \"\"\"Format the input messages into the required format. If token\n        counter and max token limit are provided, the messages will be\n        truncated to fit the limit.\n\n        Args:\n            msgs (`list[Msg]`):\n                The input messages to be formatted.\n\n        Returns:\n            `list[dict[str, Any]]`:\n                The formatted messages in the required format.\n        \"\"\"\n\n        # Check if the input messages are valid\n        self.assert_list_of_msgs(msgs)\n\n        msgs = deepcopy(msgs)\n\n        while True:\n            formatted_msgs = await self._format(msgs)\n            n_tokens = await self._count(formatted_msgs)\n\n            if (\n                n_tokens is None\n                or self.max_tokens is None\n                or n_tokens <= self.max_tokens\n            ):\n                return formatted_msgs\n\n            # truncate the input messages\n            msgs = await self._truncate(msgs)\n\n    async def _format(self, msgs: list[Msg]) -> list[dict[str, Any]]:\n        \"\"\"Format the input messages into the required format. This method\n        should be implemented by the subclasses.\"\"\"\n\n        formatted_msgs = []\n        start_index = 0\n        if len(msgs) > 0 and msgs[0].role == \"system\":\n            formatted_msgs.append(\n                await self._format_system_message(msgs[0]),\n            )\n            start_index = 1\n\n        is_first_agent_message = True\n        async for typ, group in self._group_messages(msgs[start_index:]):\n            match typ:\n                case \"tool_sequence\":\n                    formatted_msgs.extend(\n                        await self._format_tool_sequence(group),\n                    )\n                case \"agent_message\":\n                    formatted_msgs.extend(\n                        await self._format_agent_message(\n                            group,\n                            is_first_agent_message,\n                        ),\n                    )\n                    is_first_agent_message = False\n\n        return formatted_msgs\n\n    async def _format_system_message(\n        self,\n        msg: Msg,\n    ) -> dict[str, Any]:\n        \"\"\"Format system message for the LLM API.\n\n        .. note:: This is the default implementation. For certain LLM APIs\n        with specific requirements, you may need to implement a custom\n        formatting function to accommodate those particular needs.\n        \"\"\"\n        return {\n            \"role\": \"system\",\n            \"content\": msg.get_content_blocks(\"text\"),\n        }\n\n    async def _format_tool_sequence(\n        self,\n        msgs: list[Msg],\n    ) -> list[dict[str, Any]]:\n        \"\"\"Given a sequence of tool call/result messages, format them into\n        the required format for the LLM API.\"\"\"\n        raise NotImplementedError(\n            \"_format_tool_sequence is not implemented\",\n        )\n\n    async def _format_agent_message(\n        self,\n        msgs: list[Msg],\n        is_first: bool = True,\n    ) -> list[dict[str, Any]]:\n        \"\"\"Given a sequence of messages without tool calls/results, format\n        them into the required format for the LLM API.\"\"\"\n        raise NotImplementedError(\n            \"_format_agent_message is not implemented\",\n        )\n\n    async def _truncate(self, msgs: list[Msg]) -> list[Msg]:\n        \"\"\"Truncate the input messages, so that it can fit the token limit.\n        This function is called only when\n\n        - both `token_counter` and `max_tokens` are provided,\n        - the formatted output of the input messages exceeds the token limit.\n\n        .. tip:: This function only provides a simple strategy, and developers\n         can override this method to implement more sophisticated\n         truncation strategies.\n\n        .. note:: The tool call message should be truncated together with\n         its corresponding tool result message to satisfy the LLM API\n         requirements.\n\n        Args:\n            msgs (`list[Msg]`):\n                The input messages to be truncated.\n\n        Raises:\n            `ValueError`:\n                If the system prompt message already exceeds the token limit,\n                or if there are tool calls without corresponding tool results.\n\n        Returns:\n            `list[Msg]`:\n                The truncated messages.\n        \"\"\"\n        start_index = 0\n        if len(msgs) > 0 and msgs[0].role == \"system\":\n            if len(msgs) == 1:\n                # If the system prompt already exceeds the token limit, we\n                # raise an error.\n                raise ValueError(\n                    f\"The system prompt message already exceeds the token \"\n                    f\"limit ({self.max_tokens} tokens).\",\n                )\n\n            start_index = 1\n\n        # Create a tool call IDs queues to delete the corresponding tool\n        # result message\n        tool_call_ids = set()\n        for i in range(start_index, len(msgs)):\n            msg = msgs[i]\n            for block in msg.get_content_blocks(\"tool_use\"):\n                tool_call_ids.add(block[\"id\"])\n\n            for block in msg.get_content_blocks(\"tool_result\"):\n                try:\n                    tool_call_ids.remove(block[\"id\"])\n                except KeyError:\n                    pass\n\n            # We can stop truncating if the queue is empty\n            if len(tool_call_ids) == 0:\n                return msgs[:start_index] + msgs[i + 1 :]\n\n        if len(tool_call_ids) > 0:\n            raise ValueError(\n                \"The input messages contains tool call(s) that do not have \"\n                f\"the corresponding tool result(s): {tool_call_ids}. \",\n            )\n\n        return msgs[:start_index]\n\n    async def _count(self, msgs: list[dict[str, Any]]) -> int | None:\n        \"\"\"Count the number of tokens in the input messages. If token counter\n        is not provided, `None` will be returned.\n\n        Args:\n            msgs (`list[Msg]`):\n                The input messages to count tokens for.\n        \"\"\"\n        if self.token_counter is None:\n            return None\n\n        return await self.token_counter.count(msgs)\n\n    @staticmethod\n    async def _group_messages(\n        msgs: list[Msg],\n    ) -> AsyncGenerator[\n        Tuple[Literal[\"tool_sequence\", \"agent_message\"], list[Msg]],\n        None,\n    ]:\n        \"\"\"Group the input messages into two types and yield them as a\n        generator. The two types are:\n\n        - agent message that doesn't contain tool calls/results, and\n        - tool sequence that consisted of a sequence of tool calls/results\n\n        .. note:: The group operation is used in multi-agent scenario, where\n         multiple entities are involved in the input messages. So that to be\n         compatible with tools API, we have to group the messages and format\n         them with different strategies.\n\n        Args:\n            msgs (`list[Msg]`):\n                The input messages to be grouped, where the system prompt\n                message shouldn't be included.\n\n        Yields:\n            `AsyncGenerator[Tuple[str, list[Msg]], None]`:\n                A generator that yields tuples of group type and the list of\n                messages in that group. The group type can be either\n                \"tool_sequence\" or \"agent_message\".\n        \"\"\"\n\n        group_type: Literal[\"tool_sequence\", \"agent_message\"] | None = None\n        group = []\n        for msg in msgs:\n            if group_type is None:\n                if msg.has_content_blocks(\n                    \"tool_use\",\n                ) or msg.has_content_blocks(\"tool_result\"):\n                    group_type = \"tool_sequence\"\n                else:\n                    group_type = \"agent_message\"\n\n                group.append(msg)\n                continue\n\n            # determine if this msg has the same type as the current group\n            if group_type == \"tool_sequence\":\n                if msg.has_content_blocks(\n                    \"tool_use\",\n                ) or msg.has_content_blocks(\"tool_result\"):\n                    group.append(msg)\n\n                else:\n                    yield group_type, group\n                    group = [msg]\n                    group_type = \"agent_message\"\n\n            elif group_type == \"agent_message\":\n                if msg.has_content_blocks(\n                    \"tool_use\",\n                ) or msg.has_content_blocks(\"tool_result\"):\n                    yield group_type, group\n                    group = [msg]\n                    group_type = \"tool_sequence\"\n\n                else:\n                    group.append(msg)\n        if group_type:\n            yield group_type, group\n"
  },
  {
    "path": "src/agentscope/hooks/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The built-in hook functions in agentscope.\"\"\"\nfrom functools import partial\n\nfrom ._studio_hooks import (\n    as_studio_forward_message_pre_print_hook,\n)\nfrom .. import _config\nfrom ..agent import AgentBase\n\n\n__all__ = [\n    \"as_studio_forward_message_pre_print_hook\",\n]\n\n\ndef _equip_as_studio_hooks(\n    studio_url: str,\n) -> None:\n    \"\"\"Connect to the agentscope studio.\"\"\"\n    AgentBase.register_class_hook(\n        \"pre_print\",\n        \"as_studio_forward_message_pre_print_hook\",\n        partial(\n            as_studio_forward_message_pre_print_hook,\n            studio_url=studio_url,\n            run_id=_config.run_id,\n        ),\n    )\n"
  },
  {
    "path": "src/agentscope/hooks/_studio_hooks.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The studio related hook functions in agentscope.\"\"\"\nfrom typing import Any\n\nimport requests\nimport shortuuid\n\nfrom ..agent import AgentBase, UserAgent\nfrom .._logging import logger\n\n\ndef as_studio_forward_message_pre_print_hook(\n    self: AgentBase,\n    kwargs: dict[str, Any],\n    studio_url: str,\n    run_id: str,\n) -> None:\n    \"\"\"The pre-speak hook to forward messages to the studio.\"\"\"\n\n    msg = kwargs[\"msg\"]\n\n    message_data = msg.to_dict()\n\n    if hasattr(self, \"_reply_id\"):\n        reply_id = getattr(self, \"_reply_id\")\n    else:\n        reply_id = shortuuid.uuid()\n\n    n_retry = 0\n    while True:\n        try:\n            res = requests.post(\n                f\"{studio_url}/trpc/pushMessage\",\n                json={\n                    \"runId\": run_id,\n                    \"replyId\": reply_id,\n                    \"replyName\": getattr(self, \"name\", msg.name),\n                    \"replyRole\": \"user\"\n                    if isinstance(self, UserAgent)\n                    else \"assistant\",\n                    \"msg\": message_data,\n                },\n            )\n            res.raise_for_status()\n            break\n        except Exception as e:\n            if n_retry < 3:\n                n_retry += 1\n                continue\n\n            # Graceful degradation: log warning and return to avoid crashing\n            logger.warning(\n                \"Failed to forward message to Studio after %d retries: %s. \"\n                \"Agent will continue without Studio forwarding.\",\n                n_retry,\n                e,\n            )\n            return\n"
  },
  {
    "path": "src/agentscope/mcp/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The MCP module in AgentScope, that provides fine-grained control over\nthe MCP servers.\"\"\"\n\nfrom ._client_base import MCPClientBase\nfrom ._mcp_function import MCPToolFunction\nfrom ._stateful_client_base import StatefulClientBase\nfrom ._stdio_stateful_client import StdIOStatefulClient\nfrom ._http_stateless_client import HttpStatelessClient\nfrom ._http_stateful_client import HttpStatefulClient\n\n\n__all__ = [\n    \"MCPToolFunction\",\n    \"MCPClientBase\",\n    \"StatefulClientBase\",\n    \"StdIOStatefulClient\",\n    \"HttpStatelessClient\",\n    \"HttpStatefulClient\",\n]\n"
  },
  {
    "path": "src/agentscope/mcp/_client_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The base class for MCP clients in AgentScope.\"\"\"\nfrom abc import abstractmethod\nfrom typing import Callable, List\n\nimport mcp.types\n\nfrom .._logging import logger\nfrom ..message import (\n    ImageBlock,\n    Base64Source,\n    AudioBlock,\n    TextBlock,\n    VideoBlock,\n)\n\n\nclass MCPClientBase:\n    \"\"\"Base class for MCP clients.\"\"\"\n\n    def __init__(self, name: str) -> None:\n        \"\"\"Initialize the MCP client with a name.\n\n        Args:\n            name (`str`):\n                The name to identify the MCP server, which should be unique\n                across the MCP servers.\n        \"\"\"\n        self.name = name\n\n    @abstractmethod\n    async def get_callable_function(\n        self,\n        func_name: str,\n        wrap_tool_result: bool = True,\n    ) -> Callable:\n        \"\"\"Get a tool function by its name.\"\"\"\n\n    @staticmethod\n    def _convert_mcp_content_to_as_blocks(\n        mcp_content_blocks: list,\n    ) -> List[TextBlock | ImageBlock | AudioBlock | VideoBlock]:\n        \"\"\"Convert MCP content to AgentScope blocks.\"\"\"\n\n        as_content: list = []\n        for content in mcp_content_blocks:\n            if isinstance(content, mcp.types.TextContent):\n                as_content.append(\n                    TextBlock(\n                        type=\"text\",\n                        text=content.text,\n                    ),\n                )\n            elif isinstance(content, mcp.types.ImageContent):\n                as_content.append(\n                    ImageBlock(\n                        type=\"image\",\n                        source=Base64Source(\n                            type=\"base64\",\n                            media_type=content.mimeType,\n                            data=content.data,\n                        ),\n                    ),\n                )\n            elif isinstance(content, mcp.types.AudioContent):\n                as_content.append(\n                    AudioBlock(\n                        type=\"audio\",\n                        source=Base64Source(\n                            type=\"base64\",\n                            media_type=content.mimeType,\n                            data=content.data,\n                        ),\n                    ),\n                )\n            elif isinstance(content, mcp.types.EmbeddedResource):\n                if isinstance(\n                    content.resource,\n                    mcp.types.TextResourceContents,\n                ):\n                    as_content.append(\n                        TextBlock(\n                            type=\"text\",\n                            text=content.resource.model_dump_json(indent=2),\n                        ),\n                    )\n                else:\n                    # TODO: support the BlobResourceContents in the future,\n                    #  which is a base64-encoded string representing the\n                    #  binary data\n                    logger.error(\n                        \"Unsupported EmbeddedResource content type: %s. \"\n                        \"Skipping this content.\",\n                        type(content.resource),\n                    )\n            else:\n                logger.warning(\n                    \"Unsupported content type: %s. Skipping this content.\",\n                    type(content),\n                )\n        return as_content\n"
  },
  {
    "path": "src/agentscope/mcp/_http_stateful_client.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The MCP stateful HTTP client module in AgentScope.\"\"\"\nfrom typing import Any, Literal\n\nfrom mcp.client.sse import sse_client\nfrom mcp.client.streamable_http import streamablehttp_client\n\nfrom ._stateful_client_base import StatefulClientBase\n\n\nclass HttpStatefulClient(StatefulClientBase):\n    \"\"\"The stateful sse/streamable HTTP MCP client implementation in\n    AgentScope.\n\n    .. tip:: The stateful client is recommended for MCP servers that need to\n     maintain session states, e.g. web browsers or other interactive\n     MCP servers.\n\n    .. note:: The stateful client will maintain one session across multiple\n     tool calls, until the client is closed by explicitly calling the\n     `close()` method.\n\n    .. note:: When multiple HttpStatefulClient instances are connected,\n     they should be closed following the Last In First Out (LIFO) principle\n     to avoid potential errors. Always close the most recently registered\n     client first, then work backwards to the first one.\n     For more details, please refer to this `issue\n     <https://github.com/modelcontextprotocol/python-sdk/issues/577>`_.\n    \"\"\"\n\n    def __init__(\n        self,\n        name: str,\n        transport: Literal[\"streamable_http\", \"sse\"],\n        url: str,\n        headers: dict[str, str] | None = None,\n        timeout: float = 30,\n        sse_read_timeout: float = 60 * 5,\n        **client_kwargs: Any,\n    ) -> None:\n        \"\"\"Initialize the streamable HTTP MCP client.\n\n        Args:\n            name (`str`):\n                The name to identify the MCP server, which should be unique\n                across the MCP servers.\n            transport (`Literal[\"streamable_http\", \"sse\"]`):\n                The transport type of MCP server. Generally, the URL of sse\n                transport should end with `/sse`, while the streamable HTTP\n                URL ends with `/mcp`.\n            url (`str`):\n                The URL to the MCP server.\n            headers (`dict[str, str] | None`, optional):\n                Additional headers to include in the HTTP request.\n            timeout (`float`, optional):\n                The timeout for the HTTP request in seconds. Defaults to 30.\n            sse_read_timeout (`float`, optional):\n                The timeout for reading Server-Sent Events (SSE) in seconds.\n                Defaults to 300 (5 minutes).\n            **client_kwargs (`Any`):\n                The additional keyword arguments to pass to the streamable\n                HTTP client.\n        \"\"\"\n        super().__init__(name=name)\n\n        assert transport in [\"streamable_http\", \"sse\"]\n        self.transport = transport\n\n        if self.transport == \"streamable_http\":\n            self.client = streamablehttp_client(\n                url=url,\n                headers=headers,\n                timeout=timeout,\n                sse_read_timeout=sse_read_timeout,\n                **client_kwargs,\n            )\n        else:\n            self.client = sse_client(\n                url=url,\n                headers=headers,\n                timeout=timeout,\n                sse_read_timeout=sse_read_timeout,\n                **client_kwargs,\n            )\n"
  },
  {
    "path": "src/agentscope/mcp/_http_stateless_client.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The MCP streamable HTTP server.\"\"\"\nfrom contextlib import _AsyncGeneratorContextManager\nfrom typing import Any, Callable, Awaitable, Literal, List\n\nimport mcp.types\nfrom mcp import ClientSession\nfrom mcp.client.sse import sse_client\nfrom mcp.client.streamable_http import streamablehttp_client\n\nfrom . import MCPToolFunction\nfrom ._client_base import MCPClientBase\nfrom ..tool import ToolResponse\n\n\nclass HttpStatelessClient(MCPClientBase):\n    \"\"\"The sse/streamable HTTP MCP client implementation in AgentScope.\n\n    .. note:: Note this client is stateless, meaning it won't maintain the\n     session state across multiple tool calls. Each tool call will start a\n     new session and close it after the call is done.\n\n    \"\"\"\n\n    stateful: bool = False\n    \"\"\"Whether the MCP server is stateful, meaning it will maintain the\n    session state across multiple tool calls, or stateless, meaning it\n    will start a new session for each tool call.\"\"\"\n\n    def __init__(\n        self,\n        name: str,\n        transport: Literal[\"streamable_http\", \"sse\"],\n        url: str,\n        headers: dict[str, str] | None = None,\n        timeout: float = 30,\n        sse_read_timeout: float = 60 * 5,\n        **client_kwargs: Any,\n    ) -> None:\n        \"\"\"Initialize the streamable HTTP MCP server.\n\n        Args:\n            name (`str`):\n                The name to identify the MCP server, which should be unique\n                across the MCP servers.\n            transport (`Literal[\"streamable_http\", \"sse\"]`):\n                The transport type of MCP server. Generally, the URL of sse\n                transport should end with `/sse`, while the streamable HTTP\n                URL ends with `/mcp`.\n            url (`str`):\n                The URL of the MCP server.\n            headers (`dict[str, str] | None`, optional):\n                Additional headers to include in the HTTP request.\n            timeout (`float`, optional):\n                The timeout for the HTTP request in seconds. Defaults to 30.\n            sse_read_timeout (`float`, optional):\n                The timeout for reading Server-Sent Events (SSE) in seconds.\n                Defaults to 300 (5 minutes).\n            **client_kwargs (`Any`):\n                The additional keyword arguments to pass to the streamable\n                HTTP client.\n        \"\"\"\n        super().__init__(name=name)\n\n        assert transport in [\"streamable_http\", \"sse\"]\n\n        self.transport = transport\n\n        self.client_config = {\n            \"url\": url,\n            \"headers\": headers or {},\n            \"timeout\": timeout,\n            \"sse_read_timeout\": sse_read_timeout,\n            **client_kwargs,\n        }\n\n        self._tools = None\n\n    def get_client(self) -> _AsyncGeneratorContextManager[Any]:\n        \"\"\"The disposable MCP client object, which is a context manager.\"\"\"\n        if self.transport == \"sse\":\n            return sse_client(**self.client_config)\n\n        if self.transport == \"streamable_http\":\n            return streamablehttp_client(**self.client_config)\n\n        raise ValueError(\n            f\"Unsupported transport type: {self.transport}. \"\n            \"Supported types are 'sse' and 'streamable_http'.\",\n        )\n\n    async def get_callable_function(\n        self,\n        func_name: str,\n        wrap_tool_result: bool = True,\n        execution_timeout: float | None = None,\n    ) -> Callable[..., Awaitable[mcp.types.CallToolResult | ToolResponse]]:\n        \"\"\"Get a tool function by its name.\n\n        Args:\n            func_name (`str`):\n                The name of the tool function.\n            wrap_tool_result (`bool`, defaults to `True`):\n                Whether to wrap the tool result into agentscope's\n                `ToolResponse` object. If `False`, the raw result type\n                `mcp.types.CallToolResult` will be returned.\n            execution_timeout (`float | None`, optional):\n                The preset timeout in seconds for calling the tool function.\n\n        Returns:\n            `Callable[..., Awaitable[mcp.types.CallToolResult | \\\n            ToolResponse]]`:\n                An async tool function that returns either\n                `mcp.types.CallToolResult` or `ToolResponse` when called.\n        \"\"\"\n\n        if self._tools is None:\n            await self.list_tools()\n\n        target_tool = None\n        for tool in self._tools:\n            if tool.name == func_name:\n                target_tool = tool\n                break\n\n        if target_tool is None:\n            raise ValueError(\n                f\"Tool '{func_name}' not found in the MCP server \",\n            )\n\n        return MCPToolFunction(\n            mcp_name=self.name,\n            tool=target_tool,\n            wrap_tool_result=wrap_tool_result,\n            client_gen=self.get_client,\n            timeout=execution_timeout,\n        )\n\n    async def list_tools(self) -> List[mcp.types.Tool]:\n        \"\"\"List all tools available on the MCP server.\n\n        Returns:\n            `mcp.types.ListToolsResult`:\n                The result containing the list of tools.\n        \"\"\"\n        async with self.get_client() as cli:\n            read_stream, write_stream = cli[0], cli[1]\n            async with ClientSession(read_stream, write_stream) as session:\n                await session.initialize()\n                res = await session.list_tools()\n                self._tools = res.tools\n                return res.tools\n"
  },
  {
    "path": "src/agentscope/mcp/_mcp_function.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The MCP tool function class in AgentScope.\"\"\"\nfrom contextlib import _AsyncGeneratorContextManager\nfrom datetime import timedelta\nfrom typing import Any, Callable\n\nimport mcp\nfrom mcp import ClientSession\n\nfrom ._client_base import MCPClientBase\nfrom .._utils._common import _extract_json_schema_from_mcp_tool\nfrom ..tool import ToolResponse\n\n\nclass MCPToolFunction:\n    \"\"\"An MCP tool function class that can be called directly.\"\"\"\n\n    name: str\n    \"\"\"The name of the tool function.\"\"\"\n\n    description: str\n    \"\"\"The description of the tool function.\"\"\"\n\n    json_schema: dict[str, Any]\n    \"\"\"JSON schema of the tool function\"\"\"\n\n    def __init__(\n        self,\n        mcp_name: str,\n        tool: mcp.types.Tool,\n        wrap_tool_result: bool,\n        client_gen: Callable[..., _AsyncGeneratorContextManager[Any]]\n        | None = None,\n        session: ClientSession | None = None,\n        timeout: float | None = None,\n    ) -> None:\n        \"\"\"Initialize the MCP function.\n\n        Args:\n            mcp_name (`str`):\n                The name of the MCP instance.\n            tool (`mcp.types.Tool`):\n                The MCP tool definition.\n            wrap_tool_result (`bool`):\n                Whether to wrap the tool result into `ToolResponse` in\n                AgentScope.\n            client_gen (`Callable[..., _AsyncGeneratorContextManager[Any]] | \\\n            None`, *optional*):\n                The MCP client generator function. Either this or `session`\n                must be provided.\n            session (`ClientSession | None`, *optional*):\n                The MCP client session. Either this or `client_gen` must be\n                provided.\n            timeout (`float | None`, *optional*):\n                The timeout in seconds for tool execution. If not provided,\n                no timeout will be set.\n        \"\"\"\n        self.mcp_name = mcp_name\n        self.name = tool.name\n        self.description = tool.description\n        self.json_schema = _extract_json_schema_from_mcp_tool(tool)\n        self.wrap_tool_result = wrap_tool_result\n\n        if timeout:\n            self.timeout = timedelta(seconds=timeout)\n        else:\n            self.timeout = None\n\n        # Cannot be None at the same time\n        if (\n            client_gen is None\n            and session is None\n            or (client_gen is not None and session is not None)\n        ):\n            raise ValueError(\n                \"Either client or session must be provided, but not both.\",\n            )\n\n        self.client_gen = client_gen\n        self.session = session\n\n    async def __call__(\n        self,\n        **kwargs: Any,\n    ) -> mcp.types.CallToolResult | ToolResponse:\n        \"\"\"Call the MCP tool function with the given arguments, and return\n        the result.\"\"\"\n        if self.client_gen:\n            async with self.client_gen() as cli:\n                read_stream, write_stream = cli[0], cli[1]\n                async with ClientSession(read_stream, write_stream) as session:\n                    await session.initialize()\n                    res = await session.call_tool(\n                        self.name,\n                        arguments=kwargs,\n                        read_timeout_seconds=self.timeout,\n                    )\n\n        else:\n            res = await self.session.call_tool(\n                self.name,\n                arguments=kwargs,\n                read_timeout_seconds=self.timeout,\n            )\n\n        if self.wrap_tool_result:\n            as_content = MCPClientBase._convert_mcp_content_to_as_blocks(\n                res.content,\n            )\n            return ToolResponse(\n                content=as_content,\n                metadata=res.meta,\n            )\n\n        return res\n"
  },
  {
    "path": "src/agentscope/mcp/_stateful_client_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The base MCP stateful client class in AgentScope, that provides basic\n functionality for stateful MCP clients.\"\"\"\nfrom abc import ABC\nfrom contextlib import AsyncExitStack\nfrom typing import List\n\nimport mcp\nfrom mcp import ClientSession\n\nfrom ._client_base import MCPClientBase\nfrom ._mcp_function import MCPToolFunction\nfrom .._logging import logger\n\n\nclass StatefulClientBase(MCPClientBase, ABC):\n    \"\"\"The base class for stateful MCP clients in AgentScope, which maintains\n    the session state across multiple tool calls.\n\n    The developers should use `connect()` and `close()` methods to manage\n    the client lifecycle.\n    \"\"\"\n\n    is_connected: bool\n    \"\"\"If connected to the MCP server\"\"\"\n\n    def __init__(self, name: str) -> None:\n        \"\"\"Initialize the stateful MCP client.\n\n        Args:\n            name (`str`):\n                The name to identify the MCP server, which should be unique\n                across the MCP servers.\n        \"\"\"\n\n        super().__init__(name=name)\n\n        self.client = None\n        self.stack = None\n        self.session = None\n        self.is_connected = False\n\n        # Cache the tools to avoid fetching them multiple times\n        self._cached_tools = None\n\n    async def connect(self) -> None:\n        \"\"\"Connect to MCP server.\"\"\"\n        if self.is_connected:\n            raise RuntimeError(\n                \"The MCP server is already connected. Call close() \"\n                \"before connecting again.\",\n            )\n\n        self.stack = AsyncExitStack()\n\n        try:\n            context = await self.stack.enter_async_context(\n                self.client,\n            )\n            read_stream, write_stream = context[0], context[1]\n            self.session = ClientSession(read_stream, write_stream)\n            await self.stack.enter_async_context(self.session)\n            await self.session.initialize()\n\n            self.is_connected = True\n            logger.info(\"MCP client connected.\")\n        except Exception:\n            await self.stack.aclose()\n            self.stack = None\n            raise\n\n    async def close(self, ignore_errors: bool = True) -> None:\n        \"\"\"Clean up the MCP client resources. You must call this method when\n        your application is done.\n\n        Args:\n            ignore_errors (`bool`):\n                Whether to ignore errors during cleanup. Defaults to `True`.\n        \"\"\"\n        if not self.is_connected:\n            raise RuntimeError(\n                \"The MCP server is not connected. Call connect() before \"\n                \"closing.\",\n            )\n\n        try:\n            await self.stack.aclose()\n        except Exception as e:\n            if not ignore_errors:\n                raise e\n            logger.warning(\"Error during MCP client cleanup: %s\", e)\n        finally:\n            self.stack = None\n            self.session = None\n            self.is_connected = False\n\n    async def list_tools(self) -> List[mcp.types.Tool]:\n        \"\"\"Get all available tools from the server.\n\n        Returns:\n            `mcp.types.ListToolsResult`:\n                A list of available MCP tools.\n        \"\"\"\n        self._validate_connection()\n\n        res = await self.session.list_tools()\n\n        # Cache the tools for later use\n        self._cached_tools = res.tools\n        return res.tools\n\n    async def get_callable_function(\n        self,\n        func_name: str,\n        wrap_tool_result: bool = True,\n        execution_timeout: float | None = None,\n    ) -> MCPToolFunction:\n        \"\"\"Get an async tool function from the MCP server by its name, so\n        that you can call it directly, wrap it into your own function, or\n        anyway you like.\n\n        .. note:: Currently, only the text, image, and audio results are\n         supported in this function.\n\n        Args:\n            func_name (`str`):\n                The name of the tool function to get.\n            wrap_tool_result (`bool`):\n                Whether to wrap the tool result into agentscope's\n                `ToolResponse` object. If `False`, the raw result type\n                `mcp.types.CallToolResult` will be returned.\n            execution_timeout (`float | None`, optional):\n                The preset timeout in seconds for calling the tool function.\n\n        Returns:\n            `MCPToolFunction`:\n                A callable async function that returns either\n                `mcp.types.CallToolResult` or `ToolResponse` when called.\n        \"\"\"\n        self._validate_connection()\n\n        if self._cached_tools is None:\n            await self.list_tools()\n\n        target_tool = None\n        for tool in self._cached_tools:\n            if tool.name == func_name:\n                target_tool = tool\n                break\n\n        if target_tool is None:\n            raise ValueError(\n                f\"Tool '{func_name}' not found in the MCP server\",\n            )\n\n        return MCPToolFunction(\n            mcp_name=self.name,\n            tool=target_tool,\n            wrap_tool_result=wrap_tool_result,\n            session=self.session,\n            timeout=execution_timeout,\n        )\n\n    def _validate_connection(self) -> None:\n        \"\"\"Validate the connection to the MCP server.\"\"\"\n        if not self.is_connected:\n            raise RuntimeError(\n                \"The connection is not established. Call connect() \"\n                \"before using the client.\",\n            )\n\n        if not self.session:\n            raise RuntimeError(\n                \"The session is not initialized. Call connect() \"\n                \"before using the client.\",\n            )\n"
  },
  {
    "path": "src/agentscope/mcp/_stdio_stateful_client.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The StdIO MCP server implementation in AgentScope, which provides\nfunction-level fine-grained control over the MCP servers using standard IO.\"\"\"\nfrom typing import Literal\n\nfrom mcp import stdio_client, StdioServerParameters\n\nfrom ._stateful_client_base import StatefulClientBase\n\n\nclass StdIOStatefulClient(StatefulClientBase):\n    \"\"\"A client class that sets up and manage StdIO MCP server connections, and\n    provides function-level fine-grained control over the MCP servers.\n\n    .. tip:: The stateful client is recommended for MCP servers that need to\n     maintain session states, e.g. web browsers or other interactive\n     MCP servers.\n\n    .. note:: The stateful client will maintain one session across multiple\n     tool calls, until the client is closed by explicitly calling the\n     `close()` method.\n\n    .. note:: When multiple StdIOStatefulClient instances are connected,\n     they should be closed following the Last In First Out (LIFO) principle\n     to avoid potential errors. Always close the most recently registered\n     client first, then work backwards to the first one.\n     For more details, please refer to this `issue\n     <https://github.com/modelcontextprotocol/python-sdk/issues/577>`_.\n    \"\"\"\n\n    def __init__(\n        self,\n        name: str,\n        command: str,\n        args: list[str] | None = None,\n        env: dict[str, str] | None = None,\n        cwd: str | None = None,\n        encoding: str = \"utf-8\",\n        encoding_error_handler: Literal[\n            \"strict\",\n            \"ignore\",\n            \"replace\",\n        ] = \"strict\",\n    ) -> None:\n        \"\"\"Initialize the MCP server with std IO.\n\n        Args:\n            name (`str`):\n                The name to identify the MCP server, which should be unique\n                across the MCP servers.\n            command (`str`):\n                The executable to run to start the server.\n            args (`list[str] | None`, optional):\n                Command line arguments to pass to the executable.\n            env (`dict[str, str] | None`, optional):\n                The environment to use when spawning the process.\n            cwd (`str | None`, optional):\n                The working directory to use when spawning the process.\n            encoding (`str`, optional):\n                The text encoding used when sending/receiving messages to the\n                server. Defaults to \"utf-8\".\n            encoding_error_handler (`Literal[\"strict\", \"ignore\", \"replace\"]`, \\\n             defaults to \"strict\"):\n                The text encoding error handler.\n        \"\"\"\n        super().__init__(name=name)\n\n        self.client = stdio_client(\n            StdioServerParameters(\n                command=command,\n                args=args or [],\n                env=env,\n                cwd=cwd,\n                encoding=encoding,\n                encoding_error_handler=encoding_error_handler,\n            ),\n        )\n"
  },
  {
    "path": "src/agentscope/memory/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The memory module.\"\"\"\n\nfrom ._working_memory import (\n    MemoryBase,\n    InMemoryMemory,\n    RedisMemory,\n    AsyncSQLAlchemyMemory,\n)\nfrom ._long_term_memory import (\n    LongTermMemoryBase,\n    Mem0LongTermMemory,\n    ReMePersonalLongTermMemory,\n    ReMeTaskLongTermMemory,\n    ReMeToolLongTermMemory,\n)\n\n\n__all__ = [\n    # Working memory\n    \"MemoryBase\",\n    \"InMemoryMemory\",\n    \"RedisMemory\",\n    \"AsyncSQLAlchemyMemory\",\n    # Long-term memory\n    \"LongTermMemoryBase\",\n    \"Mem0LongTermMemory\",\n    \"ReMePersonalLongTermMemory\",\n    \"ReMeTaskLongTermMemory\",\n    \"ReMeToolLongTermMemory\",\n]\n"
  },
  {
    "path": "src/agentscope/memory/_long_term_memory/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The long-term memory module for AgentScope.\"\"\"\n\nfrom ._long_term_memory_base import LongTermMemoryBase\nfrom ._mem0 import Mem0LongTermMemory\nfrom ._reme import (\n    ReMePersonalLongTermMemory,\n    ReMeTaskLongTermMemory,\n    ReMeToolLongTermMemory,\n)\n\n__all__ = [\n    \"LongTermMemoryBase\",\n    \"Mem0LongTermMemory\",\n    \"ReMePersonalLongTermMemory\",\n    \"ReMeTaskLongTermMemory\",\n    \"ReMeToolLongTermMemory\",\n]\n"
  },
  {
    "path": "src/agentscope/memory/_long_term_memory/_long_term_memory_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The long-term memory base class.\"\"\"\n\nfrom typing import Any\n\nfrom agentscope.message import Msg\nfrom agentscope.module import StateModule\nfrom agentscope.tool import ToolResponse\n\n\nclass LongTermMemoryBase(StateModule):\n    \"\"\"The long-term memory base class, which should be a time-series\n    memory management system.\n\n    The `record_to_memory` and `retrieve_from_memory` methods are two tool\n    functions for agent to manage the long-term memory voluntarily. You can\n    choose not to implement these two functions.\n\n    The `record` and `retrieve` methods are for developers to use. For example,\n    retrieving/recording memory at the beginning of each reply, and adding\n    the retrieved memory to the system prompt.\n    \"\"\"\n\n    async def record(\n        self,\n        msgs: list[Msg | None],\n        **kwargs: Any,\n    ) -> Any:\n        \"\"\"A developer-designed method to record information from the given\n        input message(s) to the long-term memory.\"\"\"\n        raise NotImplementedError(\n            \"The `record` method is not implemented. \",\n        )\n\n    async def retrieve(\n        self,\n        msg: Msg | list[Msg] | None,\n        limit: int = 5,\n        **kwargs: Any,\n    ) -> str:\n        \"\"\"A developer-designed method to retrieve information from the\n        long-term memory based on the given input message(s). The retrieved\n        information will be added to the system prompt of the agent.\"\"\"\n        raise NotImplementedError(\n            \"The `retrieve` method is not implemented. \",\n        )\n\n    async def record_to_memory(\n        self,\n        thinking: str,\n        content: list[str],\n        **kwargs: Any,\n    ) -> ToolResponse:\n        \"\"\"Use this function to record important information that you may\n        need later. The target content should be specific and concise, e.g.\n        who, when, where, do what, why, how, etc.\n\n        Args:\n            thinking (`str`):\n                Your thinking and reasoning about what to record\n            content (`list[str]`):\n                The content to remember, which is a list of strings.\n        \"\"\"\n        raise NotImplementedError(\n            \"The `record_to_memory` method is not implemented. \"\n            \"You can implement it in your own long-term memory class.\",\n        )\n\n    async def retrieve_from_memory(\n        self,\n        keywords: list[str],\n        limit: int = 5,\n        **kwargs: Any,\n    ) -> ToolResponse:\n        \"\"\"Retrieve the memory based on the given keywords.\n\n        Args:\n            keywords (`list[str]`):\n                The keywords to search for in the memory, which should be\n                specific and concise, e.g. the person's name, the date, the\n                location, etc.\n            limit (`int`, optional):\n                The maximum number of memories to retrieve per search, i.e.,\n                the number of memories to retrieve for each keyword. Defaults\n                to 5.\n\n        Returns:\n            `list[Msg]`:\n                A list of messages that match the keywords.\n        \"\"\"\n        raise NotImplementedError(\n            \"The `retrieve_from_memory` method is not implemented. \"\n            \"You can implement it in your own long-term memory class.\",\n        )\n"
  },
  {
    "path": "src/agentscope/memory/_long_term_memory/_mem0/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The Mem0 long-term memory module for AgentScope.\"\"\"\n\nfrom ._mem0_long_term_memory import Mem0LongTermMemory\n\n\n__all__ = [\n    \"Mem0LongTermMemory\",\n]\n"
  },
  {
    "path": "src/agentscope/memory/_long_term_memory/_mem0/_mem0_long_term_memory.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Long-term memory implementation using mem0 library.\n\nThis module provides a long-term memory implementation that integrates\nwith the mem0 library to provide persistent memory storage and retrieval\ncapabilities for AgentScope agents.\n\"\"\"\nimport asyncio\nimport json\nfrom typing import Any, TYPE_CHECKING\nfrom importlib import metadata\n\nfrom pydantic import field_validator\n\nfrom ....embedding import EmbeddingModelBase\nfrom .._long_term_memory_base import LongTermMemoryBase\nfrom ....message import Msg, TextBlock\nfrom ....model import ChatModelBase\nfrom ....tool import ToolResponse\n\n\nif TYPE_CHECKING:\n    from mem0.configs.base import MemoryConfig\n    from mem0.vector_stores.configs import VectorStoreConfig\nelse:\n    MemoryConfig = Any\n    VectorStoreConfig = Any\n\n\ndef _create_agentscope_config_classes() -> tuple:\n    \"\"\"Create custom config classes for agentscope providers.\"\"\"\n    from mem0.embeddings.configs import EmbedderConfig\n    from mem0.llms.configs import LlmConfig\n\n    class _ASLlmConfig(LlmConfig):\n        \"\"\"Custom LLM config class that updates the validate_config method.\n\n        Attention: in mem0, the validate_config hardcodes the provider, so we\n        need to override the validate_config method to support the agentscope\n        providers. We will follow up with the mem0 to improve this.\n        \"\"\"\n\n        @field_validator(\"config\")\n        @classmethod\n        def validate_config(cls, v: Any, values: Any) -> Any:\n            \"\"\"Validate the LLM configuration.\"\"\"\n            from mem0.utils.factory import LlmFactory\n\n            provider = values.data.get(\"provider\")\n            if provider in LlmFactory.provider_to_class:\n                return v\n            raise ValueError(f\"Unsupported LLM provider: {provider}\")\n\n    class _ASEmbedderConfig(EmbedderConfig):\n        \"\"\"Custom embedder config class that updates the validate_config\n        method.\"\"\"\n\n        @field_validator(\"config\")\n        @classmethod\n        def validate_config(cls, v: Any, values: Any) -> Any:\n            \"\"\"Validate the embedder configuration.\"\"\"\n            from mem0.utils.factory import EmbedderFactory\n\n            provider = values.data.get(\"provider\")\n            if provider in EmbedderFactory.provider_to_class:\n                return v\n            raise ValueError(f\"Unsupported Embedder provider: {provider}\")\n\n    return _ASLlmConfig, _ASEmbedderConfig\n\n\nclass Mem0LongTermMemory(LongTermMemoryBase):\n    \"\"\"A class that implements the LongTermMemoryBase interface using mem0.\"\"\"\n\n    @staticmethod\n    def _setup_mem0_logging(suppress_mem0_logging: bool) -> None:\n        \"\"\"Suppress mem0 logging if requested.\n\n        Args:\n            suppress_mem0_logging (`bool`):\n                Whether to suppress mem0 logging. See class docstring for\n                details on QDRANT validation errors when using mem0 1.0.3.\n        \"\"\"\n        if suppress_mem0_logging:\n            import logging\n\n            logging.getLogger(\"mem0\").setLevel(logging.CRITICAL)\n            logging.getLogger(\"mem0.memory\").setLevel(logging.CRITICAL)\n            logging.getLogger(\"mem0.memory.main\").setLevel(logging.CRITICAL)\n\n    @staticmethod\n    def _register_agentscope_providers() -> None:\n        \"\"\"Register the agentscope providers with mem0.\n\n        Raises:\n            `ImportError`:\n                If the mem0 library is not installed.\n        \"\"\"\n        try:\n            from mem0.configs.llms.base import BaseLlmConfig\n            from mem0.utils.factory import LlmFactory, EmbedderFactory\n            from packaging import version\n\n            # Check mem0 version\n            current_version = metadata.version(\"mem0ai\")\n            is_mem0_version_low = version.parse(\n                current_version,\n            ) <= version.parse(\"0.1.115\")\n\n            # Register the agentscope providers with mem0\n            EmbedderFactory.provider_to_class[\"agentscope\"] = (\n                \"agentscope.memory._long_term_memory._mem0.\"\n                \"_mem0_utils.AgentScopeEmbedding\"\n            )\n            if is_mem0_version_low:\n                # For mem0 version <= 0.1.115, use the old style\n                LlmFactory.provider_to_class[\"agentscope\"] = (\n                    \"agentscope.memory._long_term_memory._mem0.\"\n                    \"_mem0_utils.AgentScopeLLM\"\n                )\n            else:\n                # For mem0 version > 0.1.115, use the new style\n                LlmFactory.provider_to_class[\"agentscope\"] = (\n                    \"agentscope.memory._long_term_memory._mem0.\"\n                    \"_mem0_utils.AgentScopeLLM\",\n                    BaseLlmConfig,\n                )\n\n        except ImportError as e:\n            raise ImportError(\n                \"Please install the mem0 library by `pip install mem0ai`\",\n            ) from e\n\n    @staticmethod\n    def _validate_identifiers(\n        agent_name: str | None,\n        user_name: str | None,\n        run_name: str | None,\n    ) -> None:\n        \"\"\"Validate that at least one identifier is provided.\n\n        Args:\n            agent_name (`str | None`):\n                The name of the agent.\n            user_name (`str | None`):\n                The name of the user.\n            run_name (`str | None`):\n                The name of the run/session.\n\n        Raises:\n            `ValueError`:\n                If all identifiers are None.\n        \"\"\"\n        if agent_name is None and user_name is None and run_name is None:\n            raise ValueError(\n                \"at least one of agent_name, user_name, and run_name is \"\n                \"required\",\n            )\n\n    @staticmethod\n    def _configure_mem0_config(\n        mem0_config: MemoryConfig | None,\n        model: ChatModelBase | None,\n        embedding_model: EmbeddingModelBase | None,\n        vector_store_config: VectorStoreConfig | None,\n        _ASLlmConfig: type,\n        _ASEmbedderConfig: type,\n        **kwargs: Any,\n    ) -> MemoryConfig:\n        \"\"\"Configure the mem0 MemoryConfig object.\n\n        Args:\n            mem0_config (`MemoryConfig | None`):\n                The existing mem0 config, if any.\n            model (`ChatModelBase | None`):\n                The chat model to use.\n            embedding_model (`EmbeddingModelBase | None`):\n                The embedding model to use.\n            vector_store_config (`VectorStoreConfig | None`):\n                The vector store config to use.\n            _ASLlmConfig (`type`):\n                The custom LLM config class for agentscope.\n            _ASEmbedderConfig (`type`):\n                The custom embedder config class for agentscope.\n            **kwargs (`Any`):\n                Additional keyword arguments, including 'on_disk' for\n                vector store configuration.\n\n        Returns:\n            `MemoryConfig`:\n                The configured MemoryConfig object.\n\n        Raises:\n            `ValueError`:\n                If `mem0_config` is None and either `model` or\n                `embedding_model` is None.\n        \"\"\"\n        import mem0\n\n        if mem0_config is not None:\n            # Case 1: mem0_config is provided - override specific\n            # configurations if individual params are given\n\n            # Override LLM configuration if model is provided\n            if model is not None:\n                mem0_config.llm = _ASLlmConfig(\n                    provider=\"agentscope\",\n                    config={\"model\": model},\n                )\n\n            # Override embedder configuration if embedding_model is provided\n            if embedding_model is not None:\n                mem0_config.embedder = _ASEmbedderConfig(\n                    provider=\"agentscope\",\n                    config={\"model\": embedding_model},\n                )\n\n            # Override vector store configuration if vector_store_config is\n            # provided\n            if vector_store_config is not None:\n                mem0_config.vector_store = vector_store_config\n\n        else:\n            # Case 2: mem0_config is not provided - create new configuration\n            # from individual parameters\n\n            # Validate that required parameters are provided\n            if model is None or embedding_model is None:\n                raise ValueError(\n                    \"model and embedding_model are required if mem0_config \"\n                    \"is not provided\",\n                )\n\n            # Create new MemoryConfig with provided LLM and embedder\n            mem0_config = mem0.configs.base.MemoryConfig(\n                llm=_ASLlmConfig(\n                    provider=\"agentscope\",\n                    config={\"model\": model},\n                ),\n                embedder=_ASEmbedderConfig(\n                    provider=\"agentscope\",\n                    config={\"model\": embedding_model},\n                ),\n            )\n\n            # Set vector store configuration\n            if vector_store_config is not None:\n                # Use provided vector store configuration\n                mem0_config.vector_store = vector_store_config\n            else:\n                # Use default Qdrant configuration with on-disk storage for\n                # persistence set on_disk to True to enable persistence,\n                # otherwise it will be in memory only\n                on_disk = kwargs.get(\"on_disk\", True)\n                mem0_config.vector_store = (\n                    mem0.vector_stores.configs.VectorStoreConfig(\n                        config={\"on_disk\": on_disk},\n                    )\n                )\n\n        return mem0_config\n\n    def __init__(\n        self,\n        agent_name: str | None = None,\n        user_name: str | None = None,\n        run_name: str | None = None,\n        model: ChatModelBase | None = None,\n        embedding_model: EmbeddingModelBase | None = None,\n        vector_store_config: VectorStoreConfig | None = None,\n        mem0_config: MemoryConfig | None = None,\n        default_memory_type: str | None = None,\n        suppress_mem0_logging: bool = True,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Initialize the Mem0LongTermMemory instance\n\n        Args:\n            agent_name (`str | None`, optional):\n                The name of the agent. Default is None.\n            user_name (`str | None`, optional):\n                The name of the user. Default is None.\n            run_name (`str | None`, optional):\n                The name of the run/session. Default is None.\n\n        .. note::\n            1. At least one of `agent_name`, `user_name`, or `run_name` is\n               required.\n            2. During memory recording, these parameters become metadata\n               for the stored memories.\n            3. **Important**: mem0 will extract memories from messages\n               containing role of \"user\" by default. If you want to\n               extract memories from messages containing role of\n               \"assistant\", you need to provide `agent_name`.\n            4. During memory retrieval, only memories with matching\n               metadata values will be returned.\n\n\n            model (`ChatModelBase | None`, optional):\n                The chat model to use for the long-term memory. If\n                mem0_config is provided, this will override the LLM\n                configuration. If mem0_config is None, this is required.\n            embedding_model (`EmbeddingModelBase | None`, optional):\n                The embedding model to use for the long-term memory. If\n                mem0_config is provided, this will override the embedder\n                configuration. If mem0_config is None, this is required.\n            vector_store_config (`VectorStoreConfig | None`, optional):\n                The vector store config to use for the long-term memory.\n                If mem0_config is provided, this will override the vector store\n                configuration. If mem0_config is None and this is not\n                provided, defaults to Qdrant with on_disk=True.\n            mem0_config (`MemoryConfig | None`, optional):\n                The mem0 config to use for the long-term memory.\n                If provided, individual\n                model/embedding_model/vector_store_config parameters will\n                override the corresponding configurations in mem0_config. If\n                None, a new MemoryConfig will be created using the provided\n                parameters.\n            default_memory_type (`str | None`, optional):\n                The type of memory to use. Default is None, to create a\n                semantic memory.\n            suppress_mem0_logging (`bool`, optional):\n                Whether to suppress mem0 logging. Default is True.\n\n            .. note::\n                When using vector database QDRANT with mem0 1.0.3, you may\n                encounter validation errors:\n                \"Error awaiting memory task (async): 6 validation errors for\n                PointStruct vector.list[float] Input should be a valid list\n                [type=list_type, ...]\"\n                According to the mem0 community\n                (see https://github.com/mem0ai/mem0/issues/3780),\n                these error messages are harmless and can be safely ignored.\n                Setting `suppress_mem0_logging=True` (the default) will\n                suppress these error messages.\n\n        Raises:\n            `ValueError`:\n                If `mem0_config` is None and either `model` or\n                `embedding_model` is None.\n        \"\"\"\n        super().__init__()\n\n        # Suppress mem0 logging if requested\n        self._setup_mem0_logging(suppress_mem0_logging)\n\n        # Register agentscope providers with mem0\n        self._register_agentscope_providers()\n\n        # Create the custom config classes for agentscope providers dynamically\n        _ASLlmConfig, _ASEmbedderConfig = _create_agentscope_config_classes()\n\n        # Validate identifiers\n        self._validate_identifiers(agent_name, user_name, run_name)\n\n        # Store agent and user identifiers for memory management\n        self.agent_id = agent_name\n        self.user_id = user_name\n        self.run_id = run_name\n\n        # Configure mem0_config\n        import mem0\n\n        mem0_config = self._configure_mem0_config(\n            mem0_config=mem0_config,\n            model=model,\n            embedding_model=embedding_model,\n            vector_store_config=vector_store_config,\n            _ASLlmConfig=_ASLlmConfig,\n            _ASEmbedderConfig=_ASEmbedderConfig,\n            **kwargs,\n        )\n\n        # Initialize the async memory instance with the configured settings\n        self.long_term_working_memory = mem0.AsyncMemory(mem0_config)\n\n        # Store the default memory type for future use\n        self.default_memory_type = default_memory_type\n\n    async def record_to_memory(\n        self,\n        thinking: str,\n        content: list[str],\n        **kwargs: Any,\n    ) -> ToolResponse:\n        \"\"\"Use this function to record important information that you may\n        need later. The target content should be specific and concise, e.g.\n        who, when, where, do what, why, how, etc.\n\n        Args:\n            thinking (`str`):\n                Your thinking and reasoning about what to record.\n            content (`list[str]`):\n                The content to remember, which is a list of strings.\n        \"\"\"\n        # Multi-strategy recording approach to ensure content persistence:\n        #\n        # This method employs a three-tier fallback strategy to maximize\n        # successful memory recording:\n        #\n        # 1. Primary: Record as \"user\" role message\n        #    - This is the default approach for capturing user-related\n        #      content\n        #    - Mem0 extracts and infers memories from messages containing\n        #      role of \"user\"\n        #\n        # 2. Fallback (if agent_id exists): Record as \"assistant\" role\n        #    message\n        #    - Triggered when primary recording yields no results\n        #    - In this case, mem0 will use the AGENT_MEMORY_EXTRACTION_PROMPT\n        #      in mem0/mem0/configs/prompts.py to extract memories from\n        #      messages containing role of \"assistant\", if agent_id is\n        #      provided, otherwise it will use the\n        #      USER_MEMORY_EXTRACTION_PROMPT in mem0/mem0/configs/prompts.py\n        #      to extract memories.\n        #\n        # 3. Last resort: Record as \"assistant\" with infer=False\n        #    - Used when both previous attempts yield no results\n        #    - Bypasses mem0's inference mechanism, which means no\n        #      inference is performed, mem0 will only record the content\n        #      as is.\n        #\n        # This graduated approach ensures that even if mem0's inference fails\n        # to extract meaningful memories, the raw content is still preserved.\n\n        try:\n            if thinking:\n                content = [thinking] + content\n\n            # Strategy 1: Record as user message first\n            results = await self._mem0_record(\n                [\n                    {\n                        \"role\": \"user\",\n                        \"content\": \"\\n\".join(content),\n                        \"name\": \"user\",\n                    },\n                ],\n                **kwargs,\n            )\n\n            # Strategy 2: Fallback to assistant message. In this case, if\n            # agent_id is provided, mem0 will use the\n            # AGENT_MEMORY_EXTRACTION_PROMPT in mem0/mem0/configs/prompts.py\n            # to extract memories from messages containing role of\n            # \"assistant\". If agent_id is not provided, mem0 will still use\n            # the USER_MEMORY_EXTRACTION_PROMPT in\n            # mem0/mem0/configs/prompts.py to extract memories.\n            if (\n                results\n                and isinstance(results, dict)\n                and \"results\" in results\n                and len(results[\"results\"]) == 0\n            ):\n                results = await self._mem0_record(\n                    [\n                        {\n                            \"role\": \"assistant\",\n                            \"content\": \"\\n\".join(content),\n                            \"name\": \"assistant\",\n                        },\n                    ],\n                    **kwargs,\n                )\n\n            # Strategy 3: Last resort - direct recording without inference.\n            # In this case, mem0 will not use any prompts to extract\n            # memories, it will only record the content as is.\n            if (\n                results\n                and isinstance(results, dict)\n                and \"results\" in results\n                and len(results[\"results\"]) == 0\n            ):\n                results = await self._mem0_record(\n                    [\n                        {\n                            \"role\": \"assistant\",\n                            \"content\": \"\\n\".join(content),\n                            \"name\": \"assistant\",\n                        },\n                    ],\n                    infer=False,\n                    **kwargs,\n                )\n\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Successfully recorded content to memory \"\n                        f\"{results}\",\n                    ),\n                ],\n            )\n\n        except Exception as e:\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Error recording memory: {str(e)}\",\n                    ),\n                ],\n            )\n\n    async def retrieve_from_memory(\n        self,\n        keywords: list[str],\n        limit: int = 5,\n        **kwargs: Any,\n    ) -> ToolResponse:\n        \"\"\"Retrieve the memory based on the given keywords.\n\n        Args:\n            keywords (`list[str]`):\n                Short, targeted search phrases (for example, a person's name,\n                a specific date, a location, or a phrase describing something\n                you want to retrieve from the memory). Each keyword is issued\n                as an independent query against the memory store.\n            limit (`int`, optional):\n                The maximum number of memories to retrieve per search,\n                defaults to 5.\n                i.e.,the number of memories to retrieve for\n                each keyword.\n        Returns:\n            `ToolResponse`:\n                A ToolResponse containing the retrieved memories as JSON text.\n        \"\"\"\n\n        try:\n            results = []\n            search_coroutines = [\n                self.long_term_working_memory.search(\n                    query=keyword,\n                    agent_id=self.agent_id,\n                    user_id=self.user_id,\n                    run_id=self.run_id,\n                    limit=limit,\n                )\n                for keyword in keywords\n            ]\n            search_results = await asyncio.gather(*search_coroutines)\n            for result in search_results:\n                if result:\n                    results.extend(\n                        [item[\"memory\"] for item in result[\"results\"]],\n                    )\n                    if \"relations\" in result.keys():\n                        results.extend(\n                            self._format_relations(result),\n                        )\n\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=\"\\n\".join(results),\n                    ),\n                ],\n            )\n\n        except Exception as e:\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Error retrieving memory: {str(e)}\",\n                    ),\n                ],\n            )\n\n    async def record(\n        self,\n        msgs: list[Msg | None],\n        memory_type: str | None = None,\n        infer: bool = True,\n        **kwargs: Any,\n    ) -> dict:\n        \"\"\"Record the content to the long-term memory.\n\n        Args:\n            msgs (`list[Msg | None]`):\n                The messages to record to memory.\n            memory_type (`str | None`, optional):\n                The type of memory to use. Default is None, to create a\n                semantic memory. \"procedural_memory\" is explicitly used for\n                procedural memories.\n            infer (`bool`, optional):\n                Whether to infer memory from the content. Default is True.\n            **kwargs (`Any`):\n                Additional keyword arguments for the mem0 recording.\n        \"\"\"\n        if isinstance(msgs, Msg):\n            msgs = [msgs]\n\n        # Filter out None\n        msg_list = [_ for _ in msgs if _]\n        if not all(isinstance(_, Msg) for _ in msg_list):\n            raise TypeError(\n                \"The input messages must be a list of Msg objects.\",\n            )\n\n        messages = [\n            {\n                \"role\": \"assistant\",\n                \"content\": \"\\n\".join([str(_.content) for _ in msg_list]),\n                \"name\": \"assistant\",\n            },\n        ]\n\n        results = await self._mem0_record(\n            messages,\n            memory_type=memory_type,\n            infer=infer,\n            **kwargs,\n        )\n        return results\n\n    def _format_relations(self, result: dict) -> list:\n        \"\"\"Format relations from search result.\n\n        Args:\n            result (`dict`):\n                The result from the memory search operation.\n\n        Returns:\n            `list`:\n                The formatted relations.\n                Each relation is a string in the format of:\n                \"{source} -- {relationship} -- {destination}\"\n        \"\"\"\n        if \"relations\" not in result:\n            return []\n        return [\n            f\"{relation['source']} -- \"\n            f\"{relation['relationship']} -- \"\n            f\"{relation['destination']}\"\n            for relation in result[\"relations\"]\n        ]\n\n    async def _mem0_record(\n        self,\n        messages: str | list[dict],\n        memory_type: str | None = None,\n        infer: bool = True,\n        **kwargs: Any,\n    ) -> dict:\n        \"\"\"Record the content to the long-term memory.\n\n        Args:\n            messages (`str`):\n                The content to remember, which is a string or a list of\n                dictionaries representing messages.\n            memory_type (`str | None`, optional):\n                The type of memory to use. Default is None, to create a\n                semantic memory. \"procedural_memory\" is explicitly used for\n                procedural memories.\n            infer (`bool`, optional):\n                Whether to infer memory from the content. Default is True.\n            **kwargs (`Any`):\n                Additional keyword arguments.\n\n        Returns:\n            `dict`:\n                The result from the memory recording operation.\n        \"\"\"\n        results = await self.long_term_working_memory.add(\n            messages=messages,\n            agent_id=self.agent_id,\n            user_id=self.user_id,\n            run_id=self.run_id,\n            memory_type=(\n                memory_type\n                if memory_type is not None\n                else self.default_memory_type\n            ),\n            infer=infer,\n            **kwargs,\n        )\n        return results\n\n    async def retrieve(\n        self,\n        msg: Msg | list[Msg] | None,\n        limit: int = 5,\n        **kwargs: Any,\n    ) -> str:\n        \"\"\"Retrieve the content from the long-term memory.\n\n        Args:\n            msg (`Msg | list[Msg] | None`):\n                The message to search for in the memory, which should be\n                specific and concise, e.g. the person's name, the date, the\n                location, etc.\n            limit (`int`, optional):\n                The maximum number of memories to retrieve per search, i.e.,\n                the number of memories to retrieve for the message. if the\n                message is a list of messages, the limit will be applied to\n                each message. If the message is a single message, then the\n                limit is the total number of memories to retrieve for the\n                message. Defaults to 5.\n            **kwargs (`Any`):\n                Additional keyword arguments.\n\n        Returns:\n            `str`:\n                The retrieved memory\n        \"\"\"\n        if isinstance(msg, Msg):\n            msg = [msg]\n\n        if not isinstance(msg, list) or not all(\n            isinstance(_, Msg) for _ in msg\n        ):\n            raise TypeError(\n                \"The input message must be a Msg or a list of Msg objects.\",\n            )\n\n        msg_strs = [\n            json.dumps(_.to_dict()[\"content\"], ensure_ascii=False) for _ in msg\n        ]\n\n        results = []\n        search_coroutines = [\n            self.long_term_working_memory.search(\n                query=item,\n                agent_id=self.agent_id,\n                user_id=self.user_id,\n                run_id=self.run_id,\n                limit=limit,\n            )\n            for item in msg_strs\n        ]\n        search_results = await asyncio.gather(*search_coroutines)\n        for result in search_results:\n            if result:\n                results.extend(\n                    [memory[\"memory\"] for memory in result[\"results\"]],\n                )\n                if \"relations\" in result.keys():\n                    results.extend(\n                        self._format_relations(result),\n                    )\n\n        return \"\\n\".join(results)\n"
  },
  {
    "path": "src/agentscope/memory/_long_term_memory/_mem0/_mem0_utils.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Utility classes for integrating AgentScope with mem0 library.\n\nThis module provides wrapper classes that allow AgentScope models to be used\nwith the mem0 library for long-term memory functionality.\n\"\"\"\nimport asyncio\nimport atexit\nimport threading\nfrom typing import Any, Coroutine, Dict, List, Literal\n\nfrom mem0.configs.embeddings.base import BaseEmbedderConfig\nfrom mem0.configs.llms.base import BaseLlmConfig\nfrom mem0.embeddings.base import EmbeddingBase\nfrom mem0.llms.base import LLMBase\n\nfrom ....embedding import EmbeddingModelBase\nfrom ....model import ChatModelBase, ChatResponse\n\n\nclass _EventLoopManager:\n    \"\"\"Global event loop manager for running async operations in sync context.\n\n    This manager creates and maintains a persistent background event loop\n    that runs in a separate daemon thread. This ensures that async model\n    clients (like Ollama AsyncClient) remain bound to the same event loop\n    across multiple calls, avoiding \"Event loop is closed\" errors.\n    \"\"\"\n\n    _DEFAULT_TIMEOUT = 5.0  # Default timeout in seconds\n\n    def __init__(self) -> None:\n        \"\"\"Initialize the event loop manager.\"\"\"\n        self.loop: asyncio.AbstractEventLoop | None = None\n        self.thread: threading.Thread | None = None\n        self.lock = threading.Lock()\n        self.loop_started = threading.Event()\n\n        # Register cleanup function to be called on program exit\n        atexit.register(self.cleanup)\n\n    def get_loop(self) -> asyncio.AbstractEventLoop:\n        \"\"\"Get or create the persistent background event loop.\n\n        Returns:\n            `asyncio.AbstractEventLoop`:\n                The persistent event loop running in a background thread.\n\n        Raises:\n            `RuntimeError`: If the event loop fails to start within the\n            timeout.\n        \"\"\"\n        with self.lock:\n            if self.loop is None or self.loop.is_closed():\n\n                def run_loop() -> None:\n                    \"\"\"Run the event loop in the background thread.\"\"\"\n                    loop = asyncio.new_event_loop()\n                    asyncio.set_event_loop(loop)\n                    # Store the loop reference before starting\n                    self.loop = loop\n                    self.loop_started.set()\n                    loop.run_forever()\n\n                # Clear the event before starting the thread\n                self.loop_started.clear()\n\n                # Create and start the background thread\n                self.thread = threading.Thread(\n                    target=run_loop,\n                    daemon=True,\n                    name=\"AgentScope-AsyncLoop\",\n                )\n                self.thread.start()\n\n                # Wait for the loop to be ready\n                if not self.loop_started.wait(timeout=self._DEFAULT_TIMEOUT):\n                    raise RuntimeError(\n                        \"Timeout waiting for event loop to start\",\n                    )\n\n            # After waiting, self.loop should be set by the background thread\n            assert (\n                self.loop is not None\n            ), \"Event loop was not initialized properly\"\n            return self.loop\n\n    def cleanup(self) -> None:\n        \"\"\"Cleanup the event loop and thread on program exit.\"\"\"\n        with self.lock:\n            if self.loop is not None and not self.loop.is_closed():\n                # Stop the event loop gracefully\n                self.loop.call_soon_threadsafe(self.loop.stop)\n\n                # Wait for the thread to finish\n                if self.thread is not None and self.thread.is_alive():\n                    self.thread.join(timeout=self._DEFAULT_TIMEOUT)\n\n                # Close the loop\n                self.loop.close()\n                self.loop = None\n                self.thread = None\n\n\n# Global event loop manager instance\n_event_loop_manager = _EventLoopManager()\n\n\ndef _run_async_in_persistent_loop(coro: Coroutine) -> Any:\n    \"\"\"Run an async coroutine in the persistent background event loop.\n\n    This function uses a global event loop manager to ensure that all\n    async operations run in the same event loop, which is crucial for\n    async clients like Ollama that bind to a specific event loop.\n\n    Args:\n        coro (`Coroutine`):\n            The coroutine to run.\n\n    Returns:\n        `Any`:\n            The result of the coroutine.\n\n    Raises:\n        `RuntimeError`:\n            If there's an error running the coroutine.\n    \"\"\"\n    loop = _event_loop_manager.get_loop()\n    future = asyncio.run_coroutine_threadsafe(coro, loop)\n    return future.result()\n\n\nclass AgentScopeLLM(LLMBase):\n    \"\"\"Wrapper for the AgentScope LLM.\n\n    This class is a wrapper for the AgentScope LLM. It is used to generate\n    responses using the AgentScope LLM in mem0.\n    \"\"\"\n\n    def __init__(self, config: BaseLlmConfig | None = None):\n        \"\"\"Initialize the AgentScopeLLM wrapper.\n\n        Args:\n            config (`BaseLlmConfig | None`, optional):\n                Configuration object for the LLM. Default is None.\n        \"\"\"\n        super().__init__(config)\n\n        if self.config.model is None:\n            raise ValueError(\"`model` parameter is required\")\n\n        if not isinstance(self.config.model, ChatModelBase):\n            raise ValueError(\"`model` must be an instance of ChatModelBase\")\n\n        self.agentscope_model = self.config.model\n\n    def _parse_response(\n        self,\n        model_response: ChatResponse,\n        has_tool: bool,\n    ) -> str | dict:\n        \"\"\"Parse the model response into a string or\n        a dict to follow the mem0 library's format.\n\n        Args:\n            model_response (`ChatResponse`): The response from the model.\n            has_tool (`bool`): Whether there are tool calls in the response.\n\n        Returns:\n            `str | dict`:\n                The parsed response. If has_tool is True, return a dict\n                with \"content\" and \"tool_calls\" keys. Otherwise, return\n                a string.\n        \"\"\"\n        text_parts: list[str] = []\n        thinking_parts: list[str] = []\n        tool_parts = []\n        for block in model_response.content:\n            # Handle TextBlock\n            if isinstance(block, dict) and block.get(\"type\") == \"text\":\n                text_parts.append(str(block.get(\"text\", \"\")))\n\n            # Handle ThinkingBlock\n            elif isinstance(block, dict) and block.get(\"type\") == \"thinking\":\n                thinking_parts.append(\n                    f\"[Thinking: {block.get('thinking', '')}]\",\n                )\n            # Handle ToolUseBlock\n            elif isinstance(block, dict) and block.get(\"type\") == \"tool_use\":\n                tool_name = block.get(\"name\")\n                tool_input = block.get(\"input\", {})\n                tool_parts.append(\n                    {\n                        \"name\": tool_name,\n                        \"arguments\": tool_input,\n                    },\n                )\n        text_part = thinking_parts + text_parts\n        if has_tool:\n            # If there are tool calls, return the content and tool calls\n            return {\n                \"content\": \"\\n\".join(text_part) if len(text_part) > 0 else \"\",\n                \"tool_calls\": tool_parts,\n            }\n        else:\n            return \"\\n\".join(text_part) if len(text_part) > 0 else \"\"\n\n    def generate_response(\n        self,\n        messages: List[Dict[str, str]],\n        response_format: Any | None = None,\n        tools: List[Dict] | None = None,\n        tool_choice: str = \"auto\",\n    ) -> str | dict:\n        \"\"\"Generate a response based on the given messages using agentscope.\n\n        Args:\n            messages (`List[Dict[str, str]]`):\n                List of message dicts containing 'role' and 'content'.\n            response_format (`Any | None`, optional):\n                Format of the response. Not used in AgentScope.\n            tools (`List[Dict] | None`, optional):\n                List of tools that the model can call. Not used in AgentScope.\n            tool_choice (`str`, optional):\n                Tool choice method. Not used in AgentScope.\n\n        Returns:\n            `str | dict`:\n                The generated response.\n        \"\"\"\n        # pylint: disable=unused-argument\n        try:\n            # Convert the messages to AgentScope's format\n            agentscope_messages = []\n            for message in messages:\n                role = message[\"role\"]\n                content = message[\"content\"]\n\n                if role in [\"system\", \"user\", \"assistant\"]:\n                    agentscope_messages.append(\n                        {\"role\": role, \"content\": content},\n                    )\n\n            if not agentscope_messages:\n                raise ValueError(\n                    \"No valid messages found in the messages list\",\n                )\n\n            # Use the agentscope model to generate response (async call)\n            async def _async_call() -> ChatResponse:\n                # TODO: handle the streaming response or forbidden streaming\n                #  mode\n                return await self.agentscope_model(  # type: ignore\n                    agentscope_messages,\n                    tools=tools,\n                )\n\n            # Run in the persistent event loop\n            # This ensures the model client (e.g., Ollama AsyncClient)\n            # always runs in the same event loop, avoiding binding issues\n            response = _run_async_in_persistent_loop(\n                _async_call(),\n            )\n            has_tool = tools is not None\n\n            # Extract text from the response content blocks\n            if not response.content:\n                if has_tool:\n                    return {\n                        \"content\": \"\",\n                        \"tool_calls\": [],\n                    }\n                else:\n                    return \"\"\n\n            return self._parse_response(response, has_tool)\n\n        except Exception as e:\n            raise RuntimeError(\n                f\"Error generating response using agentscope model: {str(e)}\",\n            ) from e\n\n\nclass AgentScopeEmbedding(EmbeddingBase):\n    \"\"\"Wrapper for the AgentScope Embedding model.\n\n    This class is a wrapper for the AgentScope Embedding model. It is used\n    to generate embeddings using the AgentScope Embedding model in mem0.\n    \"\"\"\n\n    def __init__(self, config: BaseEmbedderConfig | None = None):\n        \"\"\"Initialize the AgentScopeEmbedding wrapper.\n\n        Args:\n            config (`BaseEmbedderConfig | None`, optional):\n                Configuration object for the embedder. Default is None.\n        \"\"\"\n        super().__init__(config)\n\n        if self.config.model is None:\n            raise ValueError(\"`model` parameter is required\")\n\n        if not isinstance(self.config.model, EmbeddingModelBase):\n            raise ValueError(\n                \"`model` must be an instance of EmbeddingModelBase\",\n            )\n\n        self.agentscope_model = self.config.model\n\n    def embed(\n        self,\n        text: str | List[str],\n        memory_action: Literal[  # pylint: disable=unused-argument\n            \"add\",\n            \"search\",\n            \"update\",\n        ]\n        | None = None,\n    ) -> List[float]:\n        \"\"\"Get the embedding for the given text using AgentScope.\n\n        Args:\n            text (`str | List[str]`):\n                The text to embed.\n            memory_action (`Literal[\"add\", \"search\", \"update\"] | None`, \\\n            optional):\n                The type of embedding to use. Must be one of \"add\", \"search\",\n                or \"update\". Defaults to None.\n\n        Returns:\n            `List[float]`:\n                The embedding vector.\n        \"\"\"\n        try:\n            # Convert single text to list for AgentScope embedding model\n            text_list = [text] if isinstance(text, str) else text\n\n            # Use the agentscope model to generate embedding (async call)\n            async def _async_call() -> Any:\n                response = await self.agentscope_model(text_list)\n                return response\n\n            # Run in the persistent event loop\n            # This ensures the model client (e.g., Ollama AsyncClient)\n            # always runs in the same event loop, avoiding binding issues\n            response = _run_async_in_persistent_loop(\n                _async_call(),\n            )\n\n            # Extract the embedding vector from the first Embedding object\n            # response.embeddings is a list of Embedding objects\n            # Each Embedding object has an 'embedding' attribute containing\n            # the vector\n            embedding = response.embeddings[0]\n\n            if embedding is None:\n                raise ValueError(\"Failed to extract embedding from response\")\n            return embedding\n\n        except Exception as e:\n            raise RuntimeError(\n                f\"Error generating embedding using agentscope model: {str(e)}\",\n            ) from e\n"
  },
  {
    "path": "src/agentscope/memory/_long_term_memory/_reme/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The reme memory module.\"\"\"\n\nfrom ._reme_personal_long_term_memory import ReMePersonalLongTermMemory\nfrom ._reme_task_long_term_memory import ReMeTaskLongTermMemory\nfrom ._reme_tool_long_term_memory import ReMeToolLongTermMemory\n\n__all__ = [\n    \"ReMePersonalLongTermMemory\",\n    \"ReMeTaskLongTermMemory\",\n    \"ReMeToolLongTermMemory\",\n]\n"
  },
  {
    "path": "src/agentscope/memory/_long_term_memory/_reme/_reme_long_term_memory_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Base long-term memory implementation using ReMe library.\n\nThis module provides a base class for long-term memory implementations\nthat integrate with the ReMe library. ReMe enables agents to maintain\npersistent, searchable memories across sessions and contexts.\n\nThe module handles the integration between AgentScope's memory system and\nthe ReMe library, including:\n- Model configuration and API credential management\n- Context lifecycle management (async context managers)\n- Graceful handling of missing dependencies\n- Error handling with helpful installation instructions\n\nKey Features:\n- Supports both DashScope and OpenAI model providers\n- Automatic extraction of API credentials and endpoints\n- Flexible configuration via config files or kwargs\n- Safe fallback behavior when reme_ai is not installed\n\nDependencies:\n    The ReMe library is an optional dependency that must be installed:\n\n        .. code-block:: bash\n\n            pip install reme-ai\n    For more information, visit: https://github.com/modelscope/reMe\n\nSubclasses:\n    This base class is extended by specific memory type implementations:\n    - ReMeToolLongTermMemory: For tool execution patterns and guidelines\n    - ReMeTaskLongTermMemory: For task execution experiences and learnings\n    - ReMePersonalLongTermMemory: For user preferences and personal information\n\nExample:\n    .. code-block:: python\n\n        from agentscope.models import OpenAIChatModel\n        from agentscope.embedding import OpenAITextEmbedding\n        from agentscope.memory._reme import ReMeToolLongTermMemory\n\n        # Initialize models\n        model = OpenAIChatModel(model_name=\"gpt-4\", api_key=\"...\")\n        embedding = OpenAITextEmbedding(\n            model_name=\"text-embedding-3-small\", api_key=\"...\")\n\n        # Create memory instance\n        memory = ReMeToolLongTermMemory(\n            agent_name=\"my_agent\",\n            user_name=\"user_123\",\n            model=model,\n            embedding_model=embedding\n        )\n\n        # Use memory in async context\n        async with memory:\n            # Record tool execution\n            await memory.record_to_memory(\n                thinking=\"This tool worked well for data processing\",\n                content=['{\"tool_name\": \"process_data\", \"success\": true, ...}']\n            )\n\n            # Retrieve tool guidelines\n            result = await memory.retrieve_from_memory(\n                keywords=[\"process_data\"]\n            )\n\n\"\"\"\nfrom abc import ABCMeta\nfrom typing import Any\n\nfrom .._long_term_memory_base import LongTermMemoryBase\nfrom ....embedding import DashScopeTextEmbedding, OpenAITextEmbedding\nfrom ....model import DashScopeChatModel, OpenAIChatModel\n\n\nclass ReMeLongTermMemoryBase(LongTermMemoryBase, metaclass=ABCMeta):\n    \"\"\"Base class for ReMe-based long-term memory implementations.\n\n    This class provides the foundation for integrating AgentScope with the ReMe\n    library, enabling agents to maintain and retrieve long-term memories across\n    different contexts.\n\n    The ReMe library must be installed separately:\n        pip install reme-ai\n\n    If the library is not installed, a warning will be issued during\n    initialization,\n    and runtime errors with installation instructions will be raised\n    when attempting\n    to use memory operations.\n    \"\"\"\n\n    def __init__(\n        self,\n        agent_name: str | None = None,\n        user_name: str | None = None,\n        run_name: str | None = None,\n        model: DashScopeChatModel | OpenAIChatModel | None = None,\n        embedding_model: (\n            DashScopeTextEmbedding | OpenAITextEmbedding | None\n        ) = None,\n        reme_config_path: str | None = None,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Initialize the ReMe-based long-term memory.\n\n        This constructor sets up the connection to the ReMe\n        library and configures\n        the necessary models for memory operations. The ReMe app\n        will be initialized\n        with the provided model configurations.\n\n        Args:\n            agent_name (`str | None`, optional):\n                Name identifier for the agent. Used for organizing\n                memories by agent.\n            user_name (`str | None`, optional):\n                Unique identifier for the user or workspace. This maps\n                to workspace_id in ReMe and helps isolate memories across\n                different users/workspaces.\n            run_name (`str | None`, optional):\n                Name identifier for the current execution run or session.\n            model (`DashScopeChatModel | OpenAIChatModel | None`, optional):\n                The chat model to use for memory operations. The model's\n                API credentials and endpoint will be extracted and\n                passed to ReMe.\n            embedding_model (`DashScopeTextEmbedding | OpenAITextEmbedding | \\\n            None`, optional):\n                The embedding model to use for semantic memory retrieval.\n                The model's API credentials and endpoint will be\n                extracted and passed to ReMe.\n            reme_config_path (`str | None`, optional):\n                Path to a custom ReMe configuration file. If not provided, ReMe\n                will use its default configuration.\n            **kwargs (`Any`):\n                Additional keyword arguments to pass to the\n                ReMeApp constructor.\n                These can include custom ReMe configuration parameters.\n\n        Raises:\n            `ValueError`:\n                If the provided model is not a DashScopeChatModel or\n                OpenAIChatModel, or if the embedding_model is not a\n                DashScopeTextEmbedding or OpenAITextEmbedding.\n\n        Note:\n            If the reme_ai library is not installed, a warning will be\n            issued and self.app will be set to None. Subsequent memory\n            operations will raise RuntimeError with installation\n            instructions.\n\n        Example:\n            .. code-block:: python\n\n                from agentscope.models import OpenAIChatModel\n                from agentscope.embedding import OpenAITextEmbedding\n                from agentscope.memory._reme import ReMeToolLongTermMemory\n\n                # Initialize models\n                model = OpenAIChatModel(\n                    model_name=\"gpt-4\",\n                    api_key=\"your-api-key\"\n                )\n                embedding = OpenAITextEmbedding(\n                    model_name=\"text-embedding-3-small\",\n                    api_key=\"your-api-key\"\n                )\n\n                # Create memory instance\n                memory = ReMeToolLongTermMemory(\n                    agent_name=\"my_agent\",\n                    user_name=\"user_123\",\n                    run_name=\"session_001\",\n                    model=model,\n                    embedding_model=embedding\n                )\n\n                # Use with async context manager\n                async with memory:\n                    # Memory operations...\n                    pass\n\n        \"\"\"\n        super().__init__()\n\n        # Store agent and workspace identifiers\n        self.agent_name = agent_name\n        # Maps to ReMe's workspace_id concept\n        self.workspace_id = user_name\n        self.run_name = run_name\n\n        # Build configuration arguments for ReMeApp\n        # These will be passed as command-line style config overrides\n        config_args = []\n\n        # Extract LLM API credentials based on model type\n        # DashScope uses a fixed endpoint, OpenAI can have custom base_url\n        if isinstance(model, DashScopeChatModel):\n            llm_api_base = \"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n            llm_api_key = model.api_key\n\n        elif isinstance(model, OpenAIChatModel):\n            llm_api_base = str(getattr(model.client, \"base_url\", None))\n            llm_api_key = str(getattr(model.client, \"api_key\", None))\n\n        else:\n            raise ValueError(\n                f\"model must be a DashScopeChatModel or \"\n                f\"OpenAIChatModel instance. \"\n                f\"Got {type(model).__name__} instead.\",\n            )\n\n        # Extract model name and add to config if provided\n        llm_model_name = model.model_name\n\n        if llm_model_name:\n            config_args.append(f\"llm.default.model_name={llm_model_name}\")\n\n        # Extract embedding model API credentials based on type\n        # Similar to LLM, DashScope uses fixed endpoint,\n        # OpenAI can be customized\n        if isinstance(embedding_model, DashScopeTextEmbedding):\n            embedding_api_base = (\n                \"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n            )\n            embedding_api_key = embedding_model.api_key\n\n        elif isinstance(embedding_model, OpenAITextEmbedding):\n            embedding_api_base = str(\n                getattr(embedding_model.client, \"base_url\", None),\n            )\n            embedding_api_key = str(\n                getattr(embedding_model.client, \"api_key\", None),\n            )\n\n        else:\n            raise ValueError(\n                \"embedding_model must be a DashScopeTextEmbedding or \"\n                \"OpenAITextEmbedding instance. \"\n                f\"Got {type(embedding_model).__name__} instead.\",\n            )\n\n        # Extract embedding model name and add to config if provided\n        embedding_model_name = embedding_model.model_name\n\n        if embedding_model_name:\n            config_args.append(\n                f\"embedding_model.default.model_name={embedding_model_name}\",\n            )\n\n        dimensions = embedding_model.dimensions\n        config_args.append(\n            f'embedding_model.default.params={{\"dimensions\": {dimensions}}}',\n        )\n\n        # Attempt to import and initialize ReMe\n        # If import fails, set app to None and issue a warning\n        # This allows the class to be instantiated even without\n        # reme_ai installed\n        try:\n            from reme_ai import ReMeApp\n        except ImportError as e:\n            raise ImportError(\n                \"The 'reme_ai' library is required for ReMe-based \"\n                \"long-term memory. Please install it by `pip install reme-ai`,\"\n                \"and visit: https://github.com/modelscope/reMe for more \"\n                \"information.\",\n            ) from e\n\n        # Initialize ReMe with extracted configurations\n        self.app = ReMeApp(\n            *config_args,  # Config overrides as positional args\n            llm_api_key=llm_api_key,\n            llm_api_base=llm_api_base,\n            embedding_api_key=embedding_api_key,\n            embedding_api_base=embedding_api_base,\n            # Optional custom config file\n            config_path=reme_config_path,\n            # Additional ReMe-specific configurations\n            **kwargs,\n        )\n\n        # Track if the app context is active (started via __aenter__)\n        self._app_started = False\n\n    async def __aenter__(self) -> \"ReMeLongTermMemoryBase\":\n        \"\"\"Async context manager entry point.\n\n        This method is called when entering an async context\n        (using 'async with'). It initializes the ReMe app context if\n        available, enabling memory operations within the context block.\n\n        Returns:\n            `ReMeLongTermMemoryBase`:\n                The memory instance itself, allowing it to be used in\n                the context.\n\n        Example:\n            .. code-block:: python\n\n                memory = ReMeToolLongTermMemory(\n                    agent_name=\"my_agent\",\n                    model=model,\n                    embedding_model=embedding\n                )\n\n                async with memory:\n                    # Memory operations can be performed here\n                    await memory.record_to_memory(\n                        thinking=\"Recording tool usage\",\n                        content=[...]\n                    )\n\n        \"\"\"\n        if self.app is not None:\n            await self.app.__aenter__()\n            self._app_started = True\n        return self\n\n    async def __aexit__(\n        self,\n        exc_type: Any = None,\n        exc_val: Any = None,\n        exc_tb: Any = None,\n    ) -> None:\n        \"\"\"Async context manager exit point.\n\n        This method is called when exiting an async context (at the end\n        of 'async with' block or when an exception occurs). It properly\n        cleans up the ReMe app context and resources.\n\n        Args:\n            exc_type (`Any`):\n                The type of exception that occurred, if any. None if no\n                exception.\n            exc_val (`Any`):\n                The exception instance that occurred, if any. None if no\n                exception.\n            exc_tb (`Any`):\n                The traceback object for the exception, if any. None if\n                no exception.\n\n        .. note:: This method will gracefully handle the case where self.app\n         is None (reme_ai not installed) by skipping the cleanup but still\n         marking the app as stopped. It will also always set _app_started\n         to False, ensuring the memory state is properly reset.\n\n        Example:\n            .. code-block:: python\n\n                async with memory:\n                    try:\n                        # Memory operations\n                        await memory.record_to_memory(...)\n                    except Exception as e:\n                        # __aexit__ will be called even if an exception occurs\n                        print(f\"Error: {e}\")\n                # __aexit__ has been called and resources are cleaned up\n\n        \"\"\"\n        if self.app is not None:\n            await self.app.__aexit__(exc_type, exc_val, exc_tb)\n        self._app_started = False\n"
  },
  {
    "path": "src/agentscope/memory/_long_term_memory/_reme/_reme_personal_long_term_memory.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Personal memory implementation using ReMe library.\n\nThis module provides a personal memory implementation that integrates\nwith the ReMe library to provide persistent personal memory storage and\nretrieval capabilities for AgentScope agents.\n\n\"\"\"\nfrom typing import Any\n\nfrom ._reme_long_term_memory_base import ReMeLongTermMemoryBase\nfrom ...._logging import logger\nfrom ....message import Msg, TextBlock\nfrom ....tool import ToolResponse\n\n\nclass ReMePersonalLongTermMemory(ReMeLongTermMemoryBase):\n    \"\"\"Personal memory implementation using ReMe library.\"\"\"\n\n    async def record_to_memory(\n        self,\n        thinking: str,\n        content: list[str],\n        **kwargs: Any,\n    ) -> ToolResponse:\n        \"\"\"Record important user information to long-term memory.\n\n        Record important user information to long-term memory for future\n        reference.\n\n        Use this function to save user's personal information,\n        preferences, habits, and facts that you may need in future\n        conversations. This enables you to provide personalized and\n        contextually relevant responses.\n\n        When to record:\n\n        - User shares personal preferences (e.g., \"I prefer homestays\n          when traveling\")\n        - User mentions habits or routines (e.g., \"I start work at 9 AM\")\n        - User states likes/dislikes (e.g., \"I enjoy drinking green tea\")\n        - User provides personal facts (e.g., \"I work as a software\n          engineer\")\n\n        What to record: Be specific and structured. Include who, when,\n        where, what, why, and how when relevant.\n\n        Args:\n            thinking (`str`):\n                Your reasoning about why this information is worth\n                recording and how it might be useful later.\n            content (`list[str]`):\n                List of specific facts to remember. Each string should be\n                a clear, standalone piece of information. Examples:\n                [\"User prefers homestays in Hangzhou\", \"User likes\n                visiting West Lake in the morning\"].\n            **kwargs (`Any`):\n                Additional keyword arguments for the recording operation.\n\n        Returns:\n            `ToolResponse`:\n                Confirmation message indicating successful memory\n                recording.\n        \"\"\"\n        logger.info(\n            \"[ReMePersonalMemory] Entering record_to_memory - \"\n            \"thinking: %s, content: %s, kwargs: %s\",\n            thinking,\n            content,\n            kwargs,\n        )\n\n        if not self._app_started:\n            raise RuntimeError(\n                \"ReMeApp context not started. \"\n                \"Please use 'async with' to initialize the app.\",\n            )\n\n        try:\n            # Prepare messages for personal memory recording\n            messages = []\n\n            # Add thinking as a user message if provided\n            if thinking:\n                messages.append(\n                    {\n                        \"role\": \"user\",\n                        \"content\": thinking,\n                    },\n                )\n\n            # Add content items as user messages\n            for item in content:\n                messages.append(\n                    {\n                        \"role\": \"user\",\n                        \"content\": item,\n                    },\n                )\n                # Add a simple assistant acknowledgment\n                messages.append(\n                    {\n                        \"role\": \"assistant\",\n                        \"content\": (\n                            \"I understand and will remember this \"\n                            \"information.\"\n                        ),\n                    },\n                )\n\n            result = await self.app.async_execute(\n                name=\"summary_personal_memory\",\n                workspace_id=self.workspace_id,\n                trajectories=[\n                    {\n                        \"messages\": messages,\n                    },\n                ],\n                **kwargs,\n            )\n\n            # Extract metadata about stored memories if available\n            metadata = result.get(\"metadata\", {})\n            memory_list = metadata.get(\"memory_list\", [])\n\n            if memory_list:\n                summary_text = (\n                    f\"Successfully recorded {len(memory_list)} \"\n                    f\"memory/memories to personal memory.\"\n                )\n            else:\n                summary_text = \"Memory recording completed.\"\n\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=summary_text,\n                    ),\n                ],\n                metadata={\"result\": result},\n            )\n\n        except Exception as e:\n            logger.exception(\"Error recording memory: %s\", str(e))\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Error recording memory: {str(e)}\",\n                    ),\n                ],\n            )\n\n    async def retrieve_from_memory(\n        self,\n        keywords: list[str],\n        limit: int = 5,\n        **kwargs: Any,\n    ) -> ToolResponse:\n        \"\"\"Search and retrieve relevant information from long-term memory.\n\n        .. note:: You should call this function BEFORE answering\n         questions about the user's preferences, past information, or\n         personal details. This ensures you provide accurate information\n         based on stored memories rather than guessing.\n\n        Use this when:\n\n        - User asks \"what do I like?\", \"what are my preferences?\",\n          \"what do you know about me?\"\n        - User asks about their past behaviors, habits, or stated\n          preferences\n        - User refers to information they shared in previous\n          conversations\n        - You need to personalize responses based on user's history\n\n        Args:\n            keywords (`list[str]`):\n                Keywords to search for in memory. Be specific and use\n                multiple keywords for better results. Examples:\n                [\"travel preferences\", \"Hangzhou\"], [\"work habits\",\n                \"morning routine\"], [\"food preferences\", \"tea\"].\n            limit (`int`, optional):\n                The maximum number of memories to retrieve per search, i.e.,\n                the number of memories to retrieve for each keyword. Defaults\n                to 3.\n            **kwargs (`Any`):\n                Additional keyword arguments for the retrieval operation.\n\n        Returns:\n            `ToolResponse`:\n                Retrieved memories matching the keywords. If no memories\n                found, you'll receive a message indicating that.\n        \"\"\"\n        logger.info(\n            \"[ReMePersonalMemory] Entering retrieve_from_memory - \"\n            \"keywords: %s, kwargs: %s\",\n            keywords,\n            kwargs,\n        )\n\n        if not self._app_started:\n            raise RuntimeError(\n                \"ReMeApp context not started. \"\n                \"Please use 'async with' to initialize the app.\",\n            )\n\n        try:\n            results = []\n\n            # Search for each keyword\n            for keyword in keywords:\n                result = await self.app.async_execute(\n                    name=\"retrieve_personal_memory\",\n                    workspace_id=self.workspace_id,\n                    query=keyword,\n                    top_k=limit,\n                    **kwargs,\n                )\n\n                # Extract the answer from the result\n                answer = result.get(\"answer\", \"\")\n                if answer:\n                    results.append(f\"Keyword '{keyword}':\\n{answer}\")\n\n            # Combine all results\n            if results:\n                combined_text = \"\\n\\n\".join(results)\n            else:\n                combined_text = \"No memories found for the given keywords.\"\n\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=combined_text,\n                    ),\n                ],\n            )\n\n        except Exception as e:\n            logger.exception(\"Error retrieving memory: %s\", str(e))\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Error retrieving memory: {str(e)}\",\n                    ),\n                ],\n            )\n\n    async def record(\n        self,\n        msgs: list[Msg | None],\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Record the content to the long-term memory.\n\n        This method converts AgentScope messages to ReMe's format and\n        records them using the personal memory flow.\n\n        Args:\n            msgs (`list[Msg | None]`):\n                The messages to record to memory.\n            **kwargs (`Any`):\n                Additional keyword arguments for the mem0 recording.\n        \"\"\"\n        if isinstance(msgs, Msg):\n            msgs = [msgs]\n\n        # Filter out None\n        msg_list = [_ for _ in msgs if _]\n        if not msg_list:\n            return\n\n        if not all(isinstance(_, Msg) for _ in msg_list):\n            raise TypeError(\n                \"The input messages must be a list of Msg objects.\",\n            )\n\n        if not self._app_started:\n            raise RuntimeError(\n                \"ReMeApp context not started. \"\n                \"Please use 'async with' to initialize the app.\",\n            )\n\n        try:\n            # Convert AgentScope messages to ReMe format\n            messages = []\n            for msg in msg_list:\n                # Extract content as string\n                if isinstance(msg.content, str):\n                    content_str = msg.content\n                elif isinstance(msg.content, list):\n                    # Join content blocks into a single string\n                    content_parts = []\n                    for block in msg.content:\n                        if isinstance(block, dict) and \"text\" in block:\n                            content_parts.append(block[\"text\"])\n                        elif isinstance(block, dict) and \"thinking\" in block:\n                            content_parts.append(block[\"thinking\"])\n                    content_str = \"\\n\".join(content_parts)\n                else:\n                    content_str = str(msg.content)\n\n                messages.append(\n                    {\n                        \"role\": msg.role,\n                        \"content\": content_str,\n                    },\n                )\n\n            await self.app.async_execute(\n                name=\"summary_personal_memory\",\n                workspace_id=self.workspace_id,\n                trajectories=[\n                    {\n                        \"messages\": messages,\n                    },\n                ],\n                **kwargs,\n            )\n\n        except Exception as e:\n            # Log the error but don't raise to maintain compatibility\n            logger.exception(\"Error recording messages to memory: %s\", str(e))\n            import warnings\n\n            warnings.warn(f\"Error recording messages to memory: {str(e)}\")\n\n    async def retrieve(\n        self,\n        msg: Msg | list[Msg] | None,\n        limit: int = 5,\n        **kwargs: Any,\n    ) -> str:\n        \"\"\"Retrieve the content from the long-term memory.\n\n        Args:\n            msg (`Msg | list[Msg] | None`):\n                The message to search for in the memory, which should be\n                specific and concise, e.g. the person's name, the date, the\n                location, etc.\n            limit (`int`, optional):\n                The maximum number of memories to retrieve per search, i.e.,\n                the number of memories to retrieve for the message. If the\n                message is a list of messages, the limit applies to each\n                message. If the message is a single message, the limit is the\n                total number of memories to retrieve for that message. Defaults\n                to 5.\n            **kwargs (`Any`):\n                Additional keyword arguments.\n\n        Returns:\n            `str`:\n                The retrieved memory as a string.\n        \"\"\"\n        if msg is None:\n            return \"\"\n\n        if isinstance(msg, Msg):\n            msg = [msg]\n\n        if not isinstance(msg, list) or not all(\n            isinstance(_, Msg) for _ in msg\n        ):\n            raise TypeError(\n                \"The input message must be a Msg or a list of Msg objects.\",\n            )\n\n        if not self._app_started:\n            raise RuntimeError(\n                \"ReMeApp context not started. \"\n                \"Please use 'async with' to initialize the app.\",\n            )\n\n        try:\n            # Only use the last message's content for retrieval\n            last_msg = msg[-1]\n            query = \"\"\n\n            if isinstance(last_msg.content, str):\n                query = last_msg.content\n            elif isinstance(last_msg.content, list):\n                # Extract text from content blocks\n                content_parts = []\n                for block in last_msg.content:\n                    if isinstance(block, dict) and \"text\" in block:\n                        content_parts.append(block[\"text\"])\n                    elif isinstance(block, dict) and \"thinking\" in block:\n                        content_parts.append(block[\"thinking\"])\n                query = \"\\n\".join(content_parts)\n\n            if not query:\n                return \"\"\n\n            # Retrieve using the query from the last message\n            result = await self.app.async_execute(\n                name=\"retrieve_personal_memory\",\n                workspace_id=self.workspace_id,\n                query=query,\n                top_k=limit,\n                **kwargs,\n            )\n\n            return result.get(\"answer\", \"\")\n\n        except Exception as e:\n            logger.exception(\"Error retrieving memory: %s\", str(e))\n            import warnings\n\n            warnings.warn(f\"Error retrieving memory: {str(e)}\")\n            return \"\"\n"
  },
  {
    "path": "src/agentscope/memory/_long_term_memory/_reme/_reme_task_long_term_memory.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Task memory implementation using ReMe library.\n\nThis module provides a task memory implementation that integrates\nwith the ReMe library to learn from execution trajectories and\nretrieve relevant task experiences.\n\n\"\"\"\nfrom typing import Any\n\nfrom ._reme_long_term_memory_base import ReMeLongTermMemoryBase\nfrom ...._logging import logger\nfrom ....message import Msg, TextBlock\nfrom ....tool import ToolResponse\n\n\nclass ReMeTaskLongTermMemory(ReMeLongTermMemoryBase):\n    \"\"\"Task memory implementation using ReMe library.\n\n    Task memory learns from execution trajectories and provides\n    retrieval of relevant task experiences.\n\n    \"\"\"\n\n    async def record_to_memory(\n        self,\n        thinking: str,\n        content: list[str],\n        **kwargs: Any,\n    ) -> ToolResponse:\n        \"\"\"Record task execution experiences and learnings.\n\n        Record task execution experiences and learnings to long-term\n        memory.\n\n        Use this function to save valuable task-related knowledge that\n        can help with future similar tasks. This enables learning from\n        experience and improving over time.\n\n        When to record:\n\n        - After solving technical problems or completing tasks\n        - When discovering useful techniques or approaches\n        - After implementing solutions with specific steps\n        - When learning best practices or important lessons\n\n        What to record: Be detailed and actionable. Include:\n\n        - Task description and context\n        - Step-by-step execution details\n        - Specific techniques and methods used\n        - Results, outcomes, and effectiveness\n        - Lessons learned and considerations\n\n        Args:\n            thinking (`str`):\n                Your reasoning about why this task experience is valuable\n                and what makes it worth remembering for future reference.\n            content (`list[str]`):\n                List of specific task insights to remember. Each string\n                should be a clear, actionable piece of information.\n                Examples: [\"Add indexes on WHERE clause columns to speed\n                up queries\", \"Use EXPLAIN ANALYZE to identify missing\n                indexes\"].\n            **kwargs (`Any`):\n                Additional keyword arguments. Can include 'score' (float)\n                to indicate the quality/success of this approach\n                (default: 1.0).\n\n        Returns:\n            `ToolResponse`:\n                Confirmation message indicating successful memory\n                recording.\n        \"\"\"\n        logger.info(\n            \"[ReMeTaskMemory] Entering record_to_memory - \"\n            \"thinking: %s, content: %s, kwargs: %s\",\n            thinking,\n            content,\n            kwargs,\n        )\n\n        if not self._app_started:\n            raise RuntimeError(\n                \"ReMeApp context not started. \"\n                \"Please use 'async with' to initialize the app.\",\n            )\n\n        try:\n            # Prepare messages for task memory recording\n            messages = []\n\n            # Add thinking as a user message if provided\n            if thinking:\n                messages.append(\n                    {\n                        \"role\": \"user\",\n                        \"content\": thinking,\n                    },\n                )\n\n            # Add content items as user-assistant pairs\n            for item in content:\n                messages.append(\n                    {\n                        \"role\": \"user\",\n                        \"content\": item,\n                    },\n                )\n                # Add a simple assistant acknowledgment\n                messages.append(\n                    {\n                        \"role\": \"assistant\",\n                        \"content\": \"Task information recorded.\",\n                    },\n                )\n\n            result = await self.app.async_execute(\n                name=\"summary_task_memory\",\n                workspace_id=self.workspace_id,\n                trajectories=[\n                    {\n                        \"messages\": messages,\n                        \"score\": kwargs.pop(\"score\", 1.0),\n                    },\n                ],\n                **kwargs,\n            )\n\n            # Extract metadata if available\n            summary_text = (\n                f\"Successfully recorded {len(content)} task memory/memories.\"\n            )\n\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=summary_text,\n                    ),\n                ],\n                metadata={\"result\": result},\n            )\n\n        except Exception as e:\n            logger.exception(\"Error recording task memory: %s\", str(e))\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Error recording task memory: {str(e)}\",\n                    ),\n                ],\n            )\n\n    async def retrieve_from_memory(\n        self,\n        keywords: list[str],\n        limit: int = 5,\n        **kwargs: Any,\n    ) -> ToolResponse:\n        \"\"\"Search and retrieve relevant task experiences.\n\n        Search and retrieve relevant task experiences from long-term\n        memory.\n\n        IMPORTANT: You should call this function BEFORE attempting to\n        solve problems or answer technical questions. This ensures you\n        leverage experiences and proven solutions rather than\n        starting from scratch.\n\n        Use this when:\n        - Asked to solve a technical problem or implement a solution\n        - Asked for recommendations, best practices, or approaches\n        - Asked \"what do you know about...?\" or \"have you seen this\n          before?\"\n        - Dealing with tasks that may be similar to experiences\n        - Need to recall specific techniques or methods\n\n        Benefits of retrieving first:\n        - Learn from past successes and mistakes\n        - Provide more accurate, battle-tested solutions\n        - Avoid reinventing the wheel\n        - Give consistent, informed recommendations\n\n        Args:\n            keywords (`list[str]`):\n                Keywords describing the task or problem domain. Be\n                specific and use technical terms. Examples:\n                [\"database optimization\", \"slow queries\"], [\"API design\",\n                \"rate limiting\"], [\"code refactoring\", \"Python\"].\n            limit (`int`, optional):\n                The maximum number of memories to retrieve per search, i.e.,\n                the number of memories to retrieve for each keyword. Defaults\n                to 5.\n            **kwargs (`Any`):\n                Additional keyword arguments. Can include 'top_k' (int)\n                to specify number of experiences to retrieve\n                (default: 3).\n\n        Returns:\n            `ToolResponse`:\n                Retrieved task experiences and learnings. If no relevant\n                experiences found, you'll receive a message indicating\n                that.\n        \"\"\"\n        logger.info(\n            \"[ReMeTaskMemory] Entering retrieve_from_memory - \"\n            \"keywords: %s, kwargs: %s\",\n            keywords,\n            kwargs,\n        )\n\n        if not self._app_started:\n            raise RuntimeError(\n                \"ReMeApp context not started. \"\n                \"Please use 'async with' to initialize the app.\",\n            )\n\n        try:\n            results = []\n\n            # Search for each keyword\n            for keyword in keywords:\n                result = await self.app.async_execute(\n                    name=\"retrieve_task_memory\",\n                    workspace_id=self.workspace_id,\n                    query=keyword,\n                    top_k=limit,\n                    **kwargs,\n                )\n\n                # Extract the answer from the result\n                answer = result.get(\"answer\", \"\")\n                if answer:\n                    results.append(f\"Keyword '{keyword}':\\n{answer}\")\n\n            # Combine all results\n            if results:\n                combined_text = \"\\n\\n\".join(results)\n            else:\n                combined_text = (\n                    \"No task experiences found for the given keywords.\"\n                )\n\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=combined_text,\n                    ),\n                ],\n            )\n\n        except Exception as e:\n            logger.exception(\"Error retrieving task memory: %s\", str(e))\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Error retrieving task memory: {str(e)}\",\n                    ),\n                ],\n            )\n\n    async def record(\n        self,\n        msgs: list[Msg | None],\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Record the content to the task memory.\n\n        This method converts AgentScope messages to ReMe's format and\n        records them as a task execution trajectory.\n\n        Args:\n            msgs (`list[Msg | None]`):\n                The messages to record to memory.\n            **kwargs (`Any`):\n                Additional keyword arguments for the recording.\n                Can include 'score' (float) for trajectory scoring\n                (default: 1.0).\n        \"\"\"\n        if isinstance(msgs, Msg):\n            msgs = [msgs]\n\n        # Filter out None\n        msg_list = [_ for _ in msgs if _]\n        if not msg_list:\n            return\n\n        if not all(isinstance(_, Msg) for _ in msg_list):\n            raise TypeError(\n                \"The input messages must be a list of Msg objects.\",\n            )\n\n        if not self._app_started:\n            raise RuntimeError(\n                \"ReMeApp context not started. \"\n                \"Please use 'async with' to initialize the app.\",\n            )\n\n        try:\n            # Convert AgentScope messages to ReMe format\n            messages = []\n            for msg in msg_list:\n                # Extract content as string\n                if isinstance(msg.content, str):\n                    content_str = msg.content\n                elif isinstance(msg.content, list):\n                    # Join content blocks into a single string\n                    content_parts = []\n                    for block in msg.content:\n                        if isinstance(block, dict) and \"text\" in block:\n                            content_parts.append(block[\"text\"])\n                        elif isinstance(block, dict) and \"thinking\" in block:\n                            content_parts.append(block[\"thinking\"])\n                    content_str = \"\\n\".join(content_parts)\n                else:\n                    content_str = str(msg.content)\n\n                messages.append(\n                    {\n                        \"role\": msg.role,\n                        \"content\": content_str,\n                    },\n                )\n\n            # Extract score from kwargs if provided, default to 1.0\n            score = kwargs.pop(\"score\", 1.0)\n\n            await self.app.async_execute(\n                name=\"summary_task_memory\",\n                workspace_id=self.workspace_id,\n                trajectories=[\n                    {\n                        \"messages\": messages,\n                        \"score\": score,\n                    },\n                ],\n                **kwargs,\n            )\n\n        except Exception as e:\n            # Log the error but don't raise to maintain compatibility\n            logger.exception(\n                \"Error recording messages to task memory: %s\",\n                str(e),\n            )\n            import warnings\n\n            warnings.warn(\n                f\"Error recording messages to task memory: {str(e)}\",\n            )\n\n    async def retrieve(\n        self,\n        msg: Msg | list[Msg] | None,\n        limit: int = 5,\n        **kwargs: Any,\n    ) -> str:\n        \"\"\"Retrieve relevant task experiences from memory.\n\n        Args:\n            msg (`Msg | list[Msg] | None`):\n                The message to search for relevant task experiences.\n            limit (`int`, optional):\n                The maximum number of memories to retrieve per search, i.e.,\n                the number of memories to retrieve for the message. If the\n                message is a list of messages, the limit applies to each\n                message. If the message is a single message, the limit is the\n                total number of memories to retrieve for that message. Defaults\n                to 3.\n            **kwargs (`Any`):\n                Additional keyword arguments.\n\n        Returns:\n            `str`:\n                The retrieved task experiences as a string.\n        \"\"\"\n        if msg is None:\n            return \"\"\n\n        if isinstance(msg, Msg):\n            msg = [msg]\n\n        if not isinstance(msg, list) or not all(\n            isinstance(_, Msg) for _ in msg\n        ):\n            raise TypeError(\n                \"The input message must be a Msg or a list of Msg objects.\",\n            )\n\n        if not self._app_started:\n            raise RuntimeError(\n                \"ReMeApp context not started. \"\n                \"Please use 'async with' to initialize the app.\",\n            )\n\n        try:\n            # Only use the last message's content for retrieval\n            last_msg = msg[-1]\n            query = \"\"\n\n            if isinstance(last_msg.content, str):\n                query = last_msg.content\n            elif isinstance(last_msg.content, list):\n                # Extract text from content blocks\n                content_parts = []\n                for block in last_msg.content:\n                    if isinstance(block, dict) and \"text\" in block:\n                        content_parts.append(block[\"text\"])\n                    elif isinstance(block, dict) and \"thinking\" in block:\n                        content_parts.append(block[\"thinking\"])\n                query = \"\\n\".join(content_parts)\n\n            if not query:\n                return \"\"\n\n            # Retrieve using the query from the last message\n            result = await self.app.async_execute(\n                name=\"retrieve_task_memory\",\n                workspace_id=self.workspace_id,\n                query=query,\n                top_k=limit,\n                **kwargs,\n            )\n\n            return result.get(\"answer\", \"\")\n\n        except Exception as e:\n            logger.exception(\"Error retrieving task memory: %s\", str(e))\n            import warnings\n\n            warnings.warn(f\"Error retrieving task memory: {str(e)}\")\n            return \"\"\n"
  },
  {
    "path": "src/agentscope/memory/_long_term_memory/_reme/_reme_tool_long_term_memory.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tool memory implementation using ReMe library.\n\nThis module provides a tool memory implementation that integrates\nwith the ReMe library to record tool execution results and retrieve\ntool usage guidelines.\n\n\"\"\"\nfrom typing import Any\n\nfrom ._reme_long_term_memory_base import ReMeLongTermMemoryBase\nfrom ...._logging import logger\nfrom ....message import Msg, TextBlock\nfrom ....tool import ToolResponse\n\n\nclass ReMeToolLongTermMemory(ReMeLongTermMemoryBase):\n    \"\"\"Tool memory implementation using ReMe library.\n\n    Tool memory records tool execution results and generates usage\n    guidelines from the execution history.\n\n    \"\"\"\n\n    async def record_to_memory(\n        self,\n        thinking: str,\n        content: list[str],\n        **kwargs: Any,\n    ) -> ToolResponse:\n        \"\"\"Record tool execution results to build tool usage patterns.\n\n        Record tool execution results to build a knowledge base of tool\n        usage patterns.\n\n        Use this function after successfully using tools to capture\n        execution details, results, and performance metrics. Over time,\n        this builds comprehensive usage guidelines and best practices\n        for each tool.\n\n        When to record:\n\n        - After successfully executing any tool\n        - After tool failures (to learn what doesn't work)\n        - When discovering effective parameter combinations\n        - After noteworthy tool usage patterns\n\n        What to record: Each tool execution should include complete\n        execution details.\n\n        Args:\n            thinking (`str`):\n                Your reasoning about why this tool execution is worth\n                recording. Mention what worked well, what could be\n                improved, or lessons learned.\n            content (`list[str]`):\n                List of JSON strings, each representing a tool execution.\n                Each JSON must have these fields:\n                - create_time: Timestamp in format \"YYYY-MM-DD HH:MM:SS\"\n                - tool_name: Name of the tool executed\n                - input: Input parameters as a dict\n                - output: Tool's output as a string\n                - token_cost: Token cost (integer)\n                - success: Whether execution succeeded (boolean)\n                - time_cost: Execution time in seconds (float)\n\n                Example: '{\"create_time\": \"2024-01-01 10:00:00\",\n                \"tool_name\": \"search\", \"input\": {\"query\": \"Python\"},\n                \"output\": \"Found 10 results\", \"token_cost\": 100,\n                \"success\": true, \"time_cost\": 1.2}'\n            **kwargs (`Any`):\n                Additional keyword arguments for the recording operation.\n\n        Returns:\n            `ToolResponse`:\n                Confirmation message with number of executions recorded\n                and guidelines generated.\n        \"\"\"\n        logger.info(\n            \"[ReMeToolMemory] Entering record_to_memory - \"\n            \"thinking: %s, content: %s, kwargs: %s\",\n            thinking,\n            content,\n            kwargs,\n        )\n\n        if not self._app_started:\n            raise RuntimeError(\n                \"ReMeApp context not started. \"\n                \"Please use 'async with' to initialize the app.\",\n            )\n\n        try:\n            import json\n\n            # Parse each content item as a tool_call_result\n            tool_call_results = []\n            tool_names_set = set()\n\n            for item in content:\n                try:\n                    # Parse JSON string to dict\n                    tool_call_result = json.loads(item)\n                    tool_call_results.append(tool_call_result)\n\n                    # Track tool names for summary\n                    if \"tool_name\" in tool_call_result:\n                        tool_names_set.add(tool_call_result[\"tool_name\"])\n\n                except json.JSONDecodeError as e:\n                    # Skip invalid JSON items\n                    import warnings\n\n                    warnings.warn(\n                        f\"Failed to parse tool call result JSON: {item}. \"\n                        f\"Error: {str(e)}\",\n                    )\n                    continue\n\n            if not tool_call_results:\n                return ToolResponse(\n                    content=[\n                        TextBlock(\n                            type=\"text\",\n                            text=\"No valid tool call results to record.\",\n                        ),\n                    ],\n                )\n\n            # First, add the tool call results\n            await self.app.async_execute(\n                name=\"add_tool_call_result\",\n                workspace_id=self.workspace_id,\n                tool_call_results=tool_call_results,\n                **kwargs,\n            )\n\n            # Then, summarize the tool memory for the affected tools\n            if tool_names_set:\n                tool_names_list = list(tool_names_set)\n                await self.app.async_execute(\n                    name=\"summary_tool_memory\",\n                    workspace_id=self.workspace_id,\n                    tool_names=tool_names_list,\n                    **kwargs,\n                )\n\n            num_results = len(tool_call_results)\n            summary_text = (\n                f\"Successfully recorded {num_results} tool execution \"\n                f\"result{'s' if num_results > 1 else ''} and generated \"\n                f\"usage guidelines.\"\n            )\n\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=summary_text,\n                    ),\n                ],\n            )\n\n        except Exception as e:\n            logger.exception(\"Error recording tool memory: %s\", str(e))\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Error recording tool memory: {str(e)}\",\n                    ),\n                ],\n            )\n\n    async def retrieve_from_memory(\n        self,\n        keywords: list[str],\n        limit: int = 5,\n        **kwargs: Any,\n    ) -> ToolResponse:\n        \"\"\"Retrieve usage guidelines and best practices for tools.\n\n        Retrieve usage guidelines and best practices for specific tools.\n\n        .. note:: You should call this function BEFORE using a tool,\n         especially if you're uncertain about its proper usage or want to\n         follow established best practices. This retrieves synthesized\n         guidelines based on past tool executions.\n\n        Use this when:\n\n        - About to use a tool and want to know the best practices\n        - Uncertain about tool parameters or usage patterns\n        - Want to learn from past successful/failed tool executions\n        - User asks \"how should I use this tool?\" or \"what's the best\n          way to...\"\n        - Need to understand tool performance characteristics or\n          limitations\n\n        Benefits of retrieving first:\n\n        - Learn from accumulated tool usage experience\n        - Avoid common mistakes and pitfalls\n        - Use optimal parameter combinations\n        - Understand tool performance and cost characteristics\n        - Follow established best practices\n\n        Args:\n            keywords (`list[str]`):\n                List of tool names to retrieve guidelines for. Use the\n                exact tool names. Examples: [\"search\"],\n                [\"database_query\", \"cache_get\"], [\"api_call\"].\n            limit (`int`, optional):\n                The maximum number of memories to retrieve per search, i.e.,\n                the number of memories to retrieve for each keyword. Defaults\n                to 5.\n            **kwargs (`Any`):\n                Additional keyword arguments for the retrieval operation.\n\n        Returns:\n            `ToolResponse`:\n                Retrieved usage guidelines and best practices for the\n                specified tools. If no guidelines exist yet, you'll\n                receive a message indicating that.\n        \"\"\"\n        logger.info(\n            \"[ReMeToolMemory] Entering retrieve_from_memory - \"\n            \"keywords: %s, kwargs: %s\",\n            keywords,\n            kwargs,\n        )\n\n        if not self._app_started:\n            raise RuntimeError(\n                \"ReMeApp context not started. \"\n                \"Please use 'async with' to initialize the app.\",\n            )\n\n        try:\n            # Join all tool names with comma\n            tool_names = \",\".join(keywords)\n\n            # Retrieve tool guidelines for all tools at once\n            result = await self.app.async_execute(\n                name=\"retrieve_tool_memory\",\n                workspace_id=self.workspace_id,\n                tool_names=tool_names,\n                top_k=limit,\n                **kwargs,\n            )\n\n            # Extract the answer from the result\n            answer = result.get(\"answer\", \"\")\n            if answer:\n                combined_text = answer\n            else:\n                combined_text = f\"No tool guidelines found for: {tool_names}\"\n\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=combined_text,\n                    ),\n                ],\n            )\n\n        except Exception as e:\n            logger.exception(\"Error retrieving tool memory: %s\", str(e))\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Error retrieving tool memory: {str(e)}\",\n                    ),\n                ],\n            )\n\n    def _extract_content_from_messages(self, msg_list: list[Msg]) -> list[str]:\n        \"\"\"Extract content strings from messages.\n\n        Args:\n            msg_list (`list[Msg]`):\n                List of messages to extract content from.\n\n        Returns:\n            `list[str]`:\n                List of extracted content strings.\n        \"\"\"\n        content_list = []\n        for msg in msg_list:\n            if isinstance(msg.content, str):\n                content_list.append(msg.content)\n            elif isinstance(msg.content, list):\n                content_list.extend(\n                    self._extract_text_from_blocks(msg.content),\n                )\n        return content_list\n\n    def _extract_text_from_blocks(self, blocks: list) -> list[str]:\n        \"\"\"Extract text from content blocks.\n\n        Args:\n            blocks (`list`):\n                List of content blocks.\n\n        Returns:\n            `list[str]`:\n                List of extracted text strings.\n        \"\"\"\n        texts = []\n        for block in blocks:\n            if isinstance(block, dict) and block.get(\"type\") == \"text\":\n                texts.append(block.get(\"text\", \"\"))\n            elif isinstance(block, str):\n                texts.append(block)\n        return texts\n\n    def _parse_tool_call_results(\n        self,\n        content_list: list[str],\n    ) -> tuple[list[dict], set[str]]:\n        \"\"\"Parse JSON content strings into tool call results.\n\n        Args:\n            content_list (`list[str]`):\n                List of JSON strings to parse.\n\n        Returns:\n            `tuple[list[dict], set[str]]`:\n                Tuple of (tool_call_results, tool_names_set).\n        \"\"\"\n        import json\n        import warnings\n\n        tool_call_results = []\n        tool_names_set = set()\n\n        for item in content_list:\n            try:\n                tool_call_result = json.loads(item)\n                tool_call_results.append(tool_call_result)\n                if \"tool_name\" in tool_call_result:\n                    tool_names_set.add(tool_call_result[\"tool_name\"])\n            except json.JSONDecodeError as e:\n                warnings.warn(\n                    f\"Failed to parse tool call result JSON: {item}. \"\n                    f\"Error: {str(e)}\",\n                )\n\n        return tool_call_results, tool_names_set\n\n    async def record(\n        self,\n        msgs: list[Msg | None],\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Record the content to the tool memory.\n\n        This method extracts content from messages and treats them as\n        JSON strings representing tool_call_results, similar to\n        record_to_memory.\n\n        Args:\n            msgs (`list[Msg | None]`):\n                The messages to record to memory. Each message's content\n                should be a JSON string or list of JSON strings\n                representing tool_call_results.\n            **kwargs (`Any`):\n                Additional keyword arguments for the recording.\n        \"\"\"\n        if isinstance(msgs, Msg):\n            msgs = [msgs]\n\n        # Filter out None\n        msg_list = [_ for _ in msgs if _]\n        if not msg_list:\n            return\n\n        if not all(isinstance(_, Msg) for _ in msg_list):\n            raise TypeError(\n                \"The input messages must be a list of Msg objects.\",\n            )\n\n        if not self._app_started:\n            raise RuntimeError(\n                \"ReMeApp context not started. \"\n                \"Please use 'async with' to initialize the app.\",\n            )\n\n        try:\n            # Extract content from messages and parse as tool_call_results\n            content_list = self._extract_content_from_messages(msg_list)\n            if not content_list:\n                return\n\n            # Parse each content item as a tool_call_result\n            tool_call_results, tool_names_set = self._parse_tool_call_results(\n                content_list,\n            )\n            if not tool_call_results:\n                return\n\n            # First, add the tool call results\n            await self.app.async_execute(\n                name=\"add_tool_call_result\",\n                workspace_id=self.workspace_id,\n                tool_call_results=tool_call_results,\n                **kwargs,\n            )\n\n            # Then, summarize the tool memory for the affected tools\n            if tool_names_set:\n                tool_names_list = list(tool_names_set)\n                await self.app.async_execute(\n                    name=\"summary_tool_memory\",\n                    workspace_id=self.workspace_id,\n                    tool_names=tool_names_list,\n                    **kwargs,\n                )\n\n        except Exception as e:\n            # Log the error but don't raise to maintain compatibility\n            logger.exception(\n                \"Error recording tool messages to memory: %s\",\n                str(e),\n            )\n            import warnings\n\n            warnings.warn(\n                f\"Error recording tool messages to memory: {str(e)}\",\n            )\n\n    def _extract_tool_names_from_message(self, msg: Msg) -> str:\n        \"\"\"Extract tool names from a message.\n\n        Args:\n            msg (`Msg`):\n                Message to extract tool names from.\n\n        Returns:\n            `str`:\n                Extracted tool names as a string.\n        \"\"\"\n        if isinstance(msg.content, str):\n            return msg.content\n\n        if isinstance(msg.content, list):\n            content_parts = []\n            for block in msg.content:\n                if isinstance(block, dict) and \"text\" in block:\n                    content_parts.append(block[\"text\"])\n            return \" \".join(content_parts)\n\n        return \"\"\n\n    def _format_retrieve_result(self, result: Any) -> str:\n        \"\"\"Format the retrieve result into a string.\n\n        Args:\n            result (`Any`):\n                Result from the retrieve operation.\n\n        Returns:\n            `str`:\n                Formatted result string.\n        \"\"\"\n        if isinstance(result, dict) and \"answer\" in result:\n            return result[\"answer\"]\n        if isinstance(result, str):\n            return result\n        return str(result)\n\n    async def retrieve(\n        self,\n        msg: Msg | list[Msg] | None,\n        limit: int = 5,\n        **kwargs: Any,\n    ) -> str:\n        \"\"\"Retrieve tool guidelines from memory.\n\n        Retrieve tool guidelines from memory based on message content.\n\n        Args:\n            msg (`Msg | list[Msg] | None`):\n                The message containing tool names or queries to\n                retrieve guidelines for.\n            limit (`int`, optional):\n                The maximum number of memories to retrieve per search, i.e.,\n                the number of memories to retrieve for the message. If the\n                message is a list of messages, the limit applies to each\n                message. If the message is a single message, the limit is the\n                total number of memories to retrieve for that message. Defaults\n                to 5.\n            **kwargs (`Any`):\n                Additional keyword arguments.\n\n        Returns:\n            `str`:\n                The retrieved tool guidelines as a string.\n        \"\"\"\n        if msg is None:\n            return \"\"\n\n        if isinstance(msg, Msg):\n            msg = [msg]\n\n        if not isinstance(msg, list) or not all(\n            isinstance(_, Msg) for _ in msg\n        ):\n            raise TypeError(\n                \"The input message must be a Msg or a list of Msg objects.\",\n            )\n\n        if not self._app_started:\n            raise RuntimeError(\n                \"ReMeApp context not started. \"\n                \"Please use 'async with' to initialize the app.\",\n            )\n\n        try:\n            # Extract tool names from the last message\n            last_msg = msg[-1]\n            tool_names = self._extract_tool_names_from_message(last_msg)\n\n            if not tool_names:\n                return \"\"\n\n            # Retrieve tool guidelines\n            result = await self.app.async_execute(\n                name=\"retrieve_tool_memory\",\n                workspace_id=self.workspace_id,\n                tool_names=tool_names,\n                top_k=limit,\n                **kwargs,\n            )\n\n            return self._format_retrieve_result(result)\n\n        except Exception as e:\n            logger.exception(\"Error retrieving tool guidelines: %s\", str(e))\n            import warnings\n\n            warnings.warn(f\"Error retrieving tool guidelines: {str(e)}\")\n            return \"\"\n"
  },
  {
    "path": "src/agentscope/memory/_working_memory/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The working memory module in AgentScope, which provides various memory\nstorage implementations. In AgentScope, such module is responsible for\nstoring and managing the short-term memory with specific marks.\"\"\"\n\nfrom ._base import MemoryBase\nfrom ._in_memory_memory import InMemoryMemory\nfrom ._redis_memory import RedisMemory\nfrom ._sqlalchemy_memory import AsyncSQLAlchemyMemory\n\n__all__ = [\n    \"MemoryBase\",\n    \"InMemoryMemory\",\n    \"RedisMemory\",\n    \"AsyncSQLAlchemyMemory\",\n]\n"
  },
  {
    "path": "src/agentscope/memory/_working_memory/_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The memory base class.\"\"\"\n\nfrom abc import abstractmethod\nfrom typing import Any\n\nfrom ...message import Msg\nfrom ...module import StateModule\n\n\nclass MemoryBase(StateModule):\n    \"\"\"The base class for memory in agentscope.\"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"Initialize the memory base.\"\"\"\n        super().__init__()\n\n        self._compressed_summary: str = \"\"\n\n        self.register_state(\"_compressed_summary\")\n\n    async def update_compressed_summary(self, summary: str) -> None:\n        \"\"\"Update the compressed summary of the memory.\n\n        Args:\n            summary (`str`):\n                The new compressed summary.\n        \"\"\"\n        self._compressed_summary = summary\n\n    @abstractmethod\n    async def add(\n        self,\n        memories: Msg | list[Msg] | None,\n        marks: str | list[str] | None = None,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Add message(s) into the memory storage with the given mark\n        (if provided).\n\n        Args:\n            memories (`Msg | list[Msg] | None`):\n                The message(s) to be added.\n            marks (`str | list[str] | None`, optional):\n                The mark(s) to associate with the message(s). If `None`, no\n                mark is associated.\n        \"\"\"\n\n    @abstractmethod\n    async def delete(\n        self,\n        msg_ids: list[str],\n        **kwargs: Any,\n    ) -> int:\n        \"\"\"Remove message(s) from the storage by their IDs.\n\n        Args:\n            msg_ids (`list[str]`):\n                The list of message IDs to be removed.\n\n        Returns:\n            `int`:\n                The number of messages removed.\n        \"\"\"\n\n    async def delete_by_mark(\n        self,\n        mark: str | list[str],\n        *args: Any,\n        **kwargs: Any,\n    ) -> int:\n        \"\"\"Remove messages from the memory by their marks.\n\n        Args:\n            mark (`str | list[str]`):\n                The mark(s) of the messages to be removed.\n\n        Raises:\n            `TypeError`:\n                If the provided mark is not a string or a list of strings.\n\n        Returns:\n            `int`:\n                The number of messages removed.\n        \"\"\"\n        raise NotImplementedError(\n            \"The delete_by_mark method is not implemented in \"\n            f\"{self.__class__.__name__} class.\",\n        )\n\n    @abstractmethod\n    async def size(self) -> int:\n        \"\"\"Get the number of messages in the storage.\n\n        Returns:\n            `int`:\n                The number of messages in the storage.\n        \"\"\"\n\n    @abstractmethod\n    async def clear(self) -> None:\n        \"\"\"Clear the memory content.\"\"\"\n\n    @abstractmethod\n    async def get_memory(\n        self,\n        mark: str | None = None,\n        exclude_mark: str | None = None,\n        prepend_summary: bool = True,\n        **kwargs: Any,\n    ) -> list[Msg]:\n        \"\"\"Get the messages from the memory by mark (if provided). Otherwise,\n        get all messages.\n\n        .. note:: If `mark` and `exclude_mark` are both provided, the messages\n         will be filtered by both arguments.\n\n        .. note:: `mark` and `exclude_mark` should not overlap.\n\n        Args:\n            mark (`str | None`, optional):\n                The mark to filter messages. If `None`, return all messages.\n            exclude_mark (`str | None`, optional):\n                The mark to exclude messages. If provided, messages with\n                this mark will be excluded from the results.\n            prepend_summary (`bool`, defaults to True):\n                Whether to prepend the compressed summary as a message\n\n        Returns:\n            `list[Msg]`:\n                The list of messages retrieved from the storage.\n        \"\"\"\n\n    async def update_messages_mark(\n        self,\n        new_mark: str | None,\n        old_mark: str | None = None,\n        msg_ids: list[str] | None = None,\n    ) -> int:\n        \"\"\"A unified method to update marks of messages in the storage (add,\n        remove, or change marks).\n\n        - If `msg_ids` is provided, the update will be applied to the messages\n         with the specified IDs.\n        - If `old_mark` is provided, the update will be applied to the\n         messages with the specified old mark. Otherwise, the `new_mark` will\n         be added to all messages (or those filtered by `msg_ids`).\n        - If `new_mark` is `None`, the mark will be removed from the messages.\n\n        Args:\n            new_mark (`str | None`, optional):\n                The new mark to set for the messages. If `None`, the mark\n                will be removed.\n            old_mark (`str | None`, optional):\n                The old mark to filter messages. If `None`, this constraint\n                is ignored.\n            msg_ids (`list[str] | None`, optional):\n                The list of message IDs to be updated. If `None`, this\n                constraint is ignored.\n\n        Returns:\n            `int`:\n                The number of messages updated.\n        \"\"\"\n        raise NotImplementedError(\n            \"The update_messages_mark method is not implemented in \"\n            f\"{self.__class__.__name__} class.\",\n        )\n"
  },
  {
    "path": "src/agentscope/memory/_working_memory/_in_memory_memory.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The in-memory storage module for memory storage.\"\"\"\nfrom copy import deepcopy\nfrom typing import Any\n\nfrom ...message import Msg\nfrom ._base import MemoryBase\n\n\nclass InMemoryMemory(MemoryBase):\n    \"\"\"The in-memory implementation of memory storage.\"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"Initialize the in-memory storage.\"\"\"\n        super().__init__()\n        # Use a list of tuples to store messages along with their marks\n        self.content: list[tuple[Msg, list[str]]] = []\n\n        # Register the state for serialization\n        self.register_state(\"content\")\n\n    async def get_memory(\n        self,\n        mark: str | None = None,\n        exclude_mark: str | None = None,\n        prepend_summary: bool = True,\n        **kwargs: Any,\n    ) -> list[Msg]:\n        \"\"\"Get the messages from the memory by mark (if provided). Otherwise,\n        get all messages.\n\n        .. note:: If `mark` and `exclude_mark` are both provided, the messages\n         will be filtered by both arguments.\n\n        .. note:: `mark` and `exclude_mark` should not overlap.\n\n        Args:\n            mark (`str | None`, optional):\n                The mark to filter messages. If `None`, return all messages.\n            exclude_mark (`str | None`, optional):\n                The mark to exclude messages. If provided, messages with\n                this mark will be excluded from the results.\n            prepend_summary (`bool`, defaults to True):\n                Whether to prepend the compressed summary as a message\n\n        Raises:\n            `TypeError`:\n                If the provided mark is not a string or None.\n\n        Returns:\n            `list[Msg]`:\n                The list of messages retrieved from the storage.\n        \"\"\"\n        # Type checks\n        if not (mark is None or isinstance(mark, str)):\n            raise TypeError(\n                f\"The mark should be a string or None, but got {type(mark)}.\",\n            )\n\n        if not (exclude_mark is None or isinstance(exclude_mark, str)):\n            raise TypeError(\n                f\"The exclude_mark should be a string or None, but got \"\n                f\"{type(exclude_mark)}.\",\n            )\n\n        # Filter messages based on mark\n        filtered_content = [\n            (msg, marks)\n            for msg, marks in self.content\n            if mark is None or mark in marks\n        ]\n\n        # Further filter messages based on exclude_mark\n        if exclude_mark is not None:\n            filtered_content = [\n                (msg, marks)\n                for msg, marks in filtered_content\n                if exclude_mark not in marks\n            ]\n\n        if prepend_summary and self._compressed_summary:\n            return [\n                Msg(\n                    \"user\",\n                    self._compressed_summary,\n                    \"user\",\n                ),\n                *[msg for msg, _ in filtered_content],\n            ]\n\n        return [msg for msg, _ in filtered_content]\n\n    async def add(\n        self,\n        memories: Msg | list[Msg] | None,\n        marks: str | list[str] | None = None,\n        allow_duplicates: bool = False,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Add message(s) into the memory storage with the given mark\n        (if provided).\n\n        Args:\n            memories (`Msg | list[Msg] | None`):\n                The message(s) to be added.\n            marks (`str | list[str] | None`, optional):\n                The mark(s) to associate with the message(s). If `None`, no\n                mark is associated.\n            allow_duplicates (`bool`, defaults to `False`):\n                Whether to allow duplicate messages in the storage.\n        \"\"\"\n        if memories is None:\n            return\n\n        if isinstance(memories, Msg):\n            memories = [memories]\n\n        if marks is None:\n            marks = []\n        elif isinstance(marks, str):\n            marks = [marks]\n        elif not isinstance(marks, list) or not all(\n            isinstance(m, str) for m in marks\n        ):\n            raise TypeError(\n                f\"The mark should be a string, a list of strings, or None, \"\n                f\"but got {type(marks)}.\",\n            )\n\n        if not allow_duplicates:\n            existing_ids = {msg.id for msg, _ in self.content}\n            memories = [msg for msg in memories if msg.id not in existing_ids]\n\n        for msg in memories:\n            self.content.append((deepcopy(msg), deepcopy(marks)))\n\n    async def delete(\n        self,\n        msg_ids: list[str],\n        **kwargs: Any,\n    ) -> int:\n        \"\"\"Remove message(s) from the storage by their IDs.\n\n        Args:\n            msg_ids (`list[str]`):\n                The list of message IDs to be removed.\n\n        Returns:\n            `int`:\n                The number of messages removed.\n        \"\"\"\n        initial_size = len(self.content)\n        self.content = [\n            (msg, marks)\n            for msg, marks in self.content\n            if msg.id not in msg_ids\n        ]\n        return initial_size - len(self.content)\n\n    async def delete_by_mark(\n        self,\n        mark: str | list[str],\n        **kwargs: Any,\n    ) -> int:\n        \"\"\"Remove messages from the memory by their marks.\n\n        Args:\n            mark (`str | list[str]`):\n                The mark(s) of the messages to be removed.\n\n        Raises:\n            `TypeError`:\n                If the provided mark is not a string or a list of strings.\n\n        Returns:\n            `int`:\n                The number of messages removed.\n        \"\"\"\n        if isinstance(mark, str):\n            mark = [mark]\n\n        if isinstance(mark, list) and not all(\n            isinstance(m, str) for m in mark\n        ):\n            raise TypeError(\n                f\"The mark should be a string or a list of strings, \"\n                f\"but got {type(mark)} with elements of types \"\n                f\"{[type(m) for m in mark]}.\",\n            )\n\n        initial_size = len(self.content)\n        for m in mark:\n            self.content = [\n                (msg, marks) for msg, marks in self.content if m not in marks\n            ]\n\n        return initial_size - len(self.content)\n\n    async def clear(self) -> None:\n        \"\"\"Clear all messages from the storage.\"\"\"\n        self.content.clear()\n\n    async def size(self) -> int:\n        \"\"\"Get the number of messages in the storage.\n\n        Returns:\n            `int`:\n                The number of messages in the storage.\n        \"\"\"\n        return len(self.content)\n\n    async def update_messages_mark(\n        self,\n        new_mark: str | None,\n        old_mark: str | None = None,\n        msg_ids: list[str] | None = None,\n    ) -> int:\n        \"\"\"A unified method to update marks of messages in the storage (add,\n        remove, or change marks).\n\n        - If `msg_ids` is provided, the update will be applied to the messages\n         with the specified IDs.\n        - If `old_mark` is provided, the update will be applied to the\n         messages with the specified old mark. Otherwise, the `new_mark` will\n         be added to all messages (or those filtered by `msg_ids`).\n        - If `new_mark` is `None`, the mark will be removed from the messages.\n\n        Args:\n            new_mark (`str | None`, optional):\n                The new mark to set for the messages. If `None`, the mark\n                will be removed.\n            old_mark (`str | None`, optional):\n                The old mark to filter messages. If `None`, this constraint\n                is ignored.\n            msg_ids (`list[str] | None`, optional):\n                The list of message IDs to be updated. If `None`, this\n                constraint is ignored.\n\n        Returns:\n            `int`:\n                The number of messages updated.\n        \"\"\"\n        updated_count = 0\n\n        for idx, (msg, marks) in enumerate(self.content):\n            # If msg_ids is provided, skip messages not in the list\n            if msg_ids is not None and msg.id not in msg_ids:\n                continue\n\n            # If old_mark is provided, skip messages that do not have the old\n            # mark\n            if old_mark is not None and old_mark not in marks:\n                continue\n\n            # If new_mark is None, remove the old_mark\n            if new_mark is None:\n                if old_mark in marks:\n                    marks.remove(old_mark)\n                    updated_count += 1\n\n            else:\n                # If new_mark is provided, add or replace the old_mark\n                if old_mark is not None and old_mark in marks:\n                    marks.remove(old_mark)\n                if new_mark not in marks:\n                    marks.append(new_mark)\n                    updated_count += 1\n\n            self.content[idx] = (msg, marks)\n\n        return updated_count\n\n    def state_dict(self) -> dict:\n        \"\"\"Get the state dictionary for serialization.\"\"\"\n        return {\n            **super().state_dict(),\n            \"content\": [[msg.to_dict(), marks] for msg, marks in self.content],\n        }\n\n    def load_state_dict(self, state_dict: dict, strict: bool = True) -> None:\n        \"\"\"Load the state dictionary for deserialization.\"\"\"\n        if strict and \"content\" not in state_dict:\n            raise KeyError(\n                \"The state_dict does not contain 'content' \"\n                \"keys required for InMemoryMemory.\",\n            )\n\n        self._compressed_summary = state_dict.get(\"_compressed_summary\", \"\")\n\n        self.content = []\n        for item in state_dict.get(\"content\", []):\n            if isinstance(item, (tuple, list)) and len(item) == 2:\n                msg_dict, marks = item\n                msg = Msg.from_dict(msg_dict)\n                self.content.append((msg, marks))\n\n            elif isinstance(item, dict):\n                # For compatibility with older versions\n                msg = Msg.from_dict(item)\n                self.content.append((msg, []))\n\n            else:\n                raise ValueError(\n                    \"Invalid item format in state_dict for InMemoryMemory.\",\n                )\n"
  },
  {
    "path": "src/agentscope/memory/_working_memory/_redis_memory.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The redis based memory storage implementation.\"\"\"\nimport json\nfrom typing import Any, TYPE_CHECKING\n\nfrom ._base import MemoryBase\nfrom ...message import Msg\n\nif TYPE_CHECKING:\n    from redis.asyncio import ConnectionPool, Redis\nelse:\n    ConnectionPool = Any\n    Redis = Any\n\n\nclass RedisMemory(MemoryBase):\n    \"\"\"Redis memory storage implementation, which supports session and user\n    context.\n\n    .. note:: All the operations in this class are within a specific session\n     and user context, identified by `session_id` and `user_id`. Cross-session\n     or cross-user operations are not supported. For example, the\n     `remove_messages` method will only remove messages that belong to the\n     specified `session_id` and `user_id`.\n\n    .. note:: All Redis keys used by this class will be prefixed by `prefix`\n    (if provided) to support multi-tenant / multi-app isolation.\n\n    **Mark Index Storage:**\n\n    This class maintains a `marks_index` (Redis Set) to efficiently track all\n    mark names within a session. When a mark is created via `add_mark()`, the\n    mark name is added to this set. This allows quick retrieval of all marks\n    without scanning all Redis keys. The marks_index key pattern is:\n    ``user_id:{user_id}:session:{session_id}:marks_index``\n\n    Each individual mark stores its associated message IDs in a separate Redis\n    List with the key pattern:\n    ``user_id:{user_id}:session:{session_id}:mark:{mark}``\n\n    \"\"\"\n\n    SESSION_KEY = \"user_id:{user_id}:session:{session_id}:messages\"\n    \"\"\"Redis key pattern (without prefix) for storing message IDs (ordered) for\n    a specific session.\n    \"\"\"\n\n    SESSION_PATTERN = \"user_id:{user_id}:session:{session_id}:*\"\n    \"\"\"Redis key pattern (without prefix) for scanning all keys belong to\n    a specific user and session.\"\"\"\n\n    MARK_KEY = \"user_id:{user_id}:session:{session_id}:mark:{mark}\"\n    \"\"\"Redis key pattern (without prefix) for storing message IDs that belong\n    to a specific mark.\n    \"\"\"\n\n    MESSAGE_KEY = \"user_id:{user_id}:session:{session_id}:msg:{msg_id}\"\n    \"\"\"Redis key pattern (without prefix) for storing message payload data.\"\"\"\n\n    MARKS_INDEX_KEY = \"user_id:{user_id}:session:{session_id}:marks_index\"\n    \"\"\"Redis key pattern (without prefix) for storing all mark names as a set.\n    This is used to avoid scanning all keys to find marks.\n    \"\"\"\n\n    def __init__(\n        self,\n        session_id: str = \"default_session\",\n        user_id: str = \"default_user\",\n        host: str = \"localhost\",\n        port: int = 6379,\n        db: int = 0,\n        password: str | None = None,\n        connection_pool: ConnectionPool | None = None,\n        key_prefix: str = \"\",\n        key_ttl: int | None = None,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Initialize the Redis based storage by connecting to the Redis\n        server. You can provide either the connection parameters or an\n        existing connection pool.\n\n        Args:\n            session_id (`str`, default to `\"default_session\"`):\n                The session ID for the storage.\n            user_id (`str`, default to `\"default_user\"`):\n                The user ID for the storage.\n            host (`str`, default to `\"localhost\"`):\n                The Redis server host.\n            port (`int`, default to `6379`):\n                The Redis server port.\n            db (`int`, default to `0`):\n                The Redis database index.\n            password (`str | None`, optional):\n                The password for the Redis server, if required.\n            connection_pool (`ConnectionPool | None`, optional):\n                An optional Redis connection pool. If provided, it will be used\n                instead of creating a new connection.\n            key_prefix (`str`, default to `\"\"`):\n                Optional Redis key prefix prepended to every key generated by\n                this storage. Useful for isolating keys across\n                apps/environments (e.g. `\"prod\"`, `\"staging\"`, `\"myapp\"`).\n            key_ttl (`int | None`, default to `None`):\n                The expired time in seconds for each key. If provided, the\n                expiration will be refreshed on every access (sliding TTL). If\n                `None`, the keys will not expire. **Note** the ttl will update\n                all session related keys, so it's not recommended to set for\n                large sessions.\n            **kwargs (`Any`):\n                Additional keyword arguments to pass to the Redis client.\n        \"\"\"\n        try:\n            import redis.asyncio as redis\n        except ImportError as e:\n            raise ImportError(\n                \"The 'redis' package is required for RedisStorage. \"\n                \"Please install it via 'pip install redis[async]'.\",\n            ) from e\n\n        super().__init__()\n\n        self.session_id = session_id\n        self.user_id = user_id\n        self.key_prefix = key_prefix or \"\"\n        self.key_ttl = key_ttl\n\n        self._client = redis.Redis(\n            host=host,\n            port=port,\n            db=db,\n            password=password,\n            connection_pool=connection_pool,\n            decode_responses=True,\n            **kwargs,\n        )\n\n    def get_client(self) -> Redis:\n        \"\"\"Get the underlying Redis client.\n\n        Returns:\n            `Redis`:\n                The Redis client instance.\n        \"\"\"\n        return self._client\n\n    def _decode_if_bytes(self, data: Any) -> Any:\n        \"\"\"Helper method to decode bytes to str if needed.\n\n        Args:\n            data (`Any`):\n                The data to decode, which may be bytes, bytearray, or str.\n\n        Returns:\n            `Any`:\n                The decoded string if input was bytes/bytearray, otherwise\n                the original data.\n        \"\"\"\n        if isinstance(data, (bytes, bytearray)):\n            return data.decode(\"utf-8\")\n        return data\n\n    def _decode_list(self, data_list: list) -> list:\n        \"\"\"Helper method to decode a list of potential bytes.\n\n        Args:\n            data_list (`list`):\n                A list that may contain bytes, bytearray, or str elements.\n\n        Returns:\n            `list`:\n                A list with all bytes/bytearray elements decoded to str.\n        \"\"\"\n        return [self._decode_if_bytes(item) for item in data_list]\n\n    def _get_session_key(self) -> str:\n        \"\"\"Get the Redis key for the current session.\n\n        Returns:\n            `str`:\n                The Redis key for storing messages in the current session.\n        \"\"\"\n        return self.key_prefix + self.SESSION_KEY.format(\n            user_id=self.user_id,\n            session_id=self.session_id,\n        )\n\n    def _get_session_pattern(self) -> str:\n        \"\"\"Get the Redis key pattern for all keys in the current session.\n\n        Returns:\n            `str`:\n                The Redis key pattern for all keys in the current session.\n        \"\"\"\n        return self.key_prefix + self.SESSION_PATTERN.format(\n            user_id=self.user_id,\n            session_id=self.session_id,\n        )\n\n    def _get_mark_key(self, mark: str) -> str:\n        \"\"\"Get the Redis key for a specific mark.\n\n        Args:\n            mark (`str`):\n                The mark name.\n\n        Returns:\n            `str`:\n                The Redis key for storing message IDs with the given mark.\n        \"\"\"\n        return self.key_prefix + self.MARK_KEY.format(\n            user_id=self.user_id,\n            session_id=self.session_id,\n            mark=mark,\n        )\n\n    def _get_mark_pattern(self) -> str:\n        \"\"\"Get the Redis key pattern for all marks in the current session.\n\n        Returns:\n            `str`:\n                The Redis key pattern for all mark keys.\n        \"\"\"\n        return self.key_prefix + self.MARK_KEY.format(\n            user_id=self.user_id,\n            session_id=self.session_id,\n            mark=\"*\",\n        )\n\n    def _get_marks_index_key(self) -> str:\n        \"\"\"Get the Redis key for the marks index set.\n\n        Returns:\n            `str`:\n                The Redis key for storing all mark names as a set.\n        \"\"\"\n        return self.key_prefix + self.MARKS_INDEX_KEY.format(\n            user_id=self.user_id,\n            session_id=self.session_id,\n        )\n\n    def _extract_mark_from_key(self, mark_key: str) -> str:\n        \"\"\"Extract the mark name from a full mark key.\n\n        Args:\n            mark_key (`str`):\n                The full Redis key for a mark.\n\n        Returns:\n            `str`:\n                The mark name extracted from the key.\n        \"\"\"\n        # Remove the prefix and the base pattern to get the mark name\n        # Example: \"prefix:user_id:xxx:session:yyy:mark:my_mark\" -> \"my_mark\"\n        prefix_pattern = self.key_prefix + self.MARK_KEY.format(\n            user_id=self.user_id,\n            session_id=self.session_id,\n            mark=\"\",\n        )\n        return mark_key.replace(prefix_pattern, \"\")\n\n    def _get_message_key(self, msg_id: str) -> str:\n        \"\"\"Get the Redis key for a specific message.\n\n        Args:\n            msg_id (`str`):\n                The message ID.\n\n        Returns:\n            `str`:\n                The Redis key for storing the message data.\n        \"\"\"\n        return self.key_prefix + self.MESSAGE_KEY.format(\n            user_id=self.user_id,\n            session_id=self.session_id,\n            msg_id=msg_id,\n        )\n\n    async def _refresh_session_ttl(\n        self,\n        pipe: Any | None = None,\n    ) -> None:\n        \"\"\"Refresh the TTL for the session keys (if `key_ttl` is set).\n\n        Args:\n            pipe (`Any | None`, optional):\n                An optional Redis pipeline to use. If `None`, a new pipeline\n                will be created and executed immediately. If provided, the\n                expired commands will be added to the pipeline without\n                executing it.\n        \"\"\"\n        if self.key_ttl is None:\n            return\n\n        # Create a new pipeline if not provided\n        should_execute = pipe is None\n        if pipe is None:\n            pipe = self._client.pipeline()\n\n        cursor = 0\n        while True:\n            cursor, keys = await self._client.scan(\n                cursor,\n                match=self._get_session_pattern(),\n                count=100,\n            )\n            # Decode keys if they are bytes\n            keys = self._decode_list(keys)\n            for key in keys:\n                await pipe.expire(key, self.key_ttl)\n            if cursor == 0:\n                break\n\n        if should_execute:\n            await pipe.execute()\n\n    async def _scan_and_migrate_marks(self) -> list[str]:\n        \"\"\"Scan all mark keys and migrate them to the marks index.\n\n        This method is only called once for old data that doesn't have\n        a marks index yet. After migration, the marks index will be\n        maintained automatically.\n\n        Returns:\n            `list[str]`:\n                The list of all mark keys found.\n        \"\"\"\n        mark_keys = []\n        cursor = 0\n        while True:\n            cursor, keys = await self._client.scan(\n                cursor,\n                match=self._get_mark_pattern(),\n                count=50,\n            )\n            keys = self._decode_list(keys)\n            mark_keys.extend(keys)\n            if cursor == 0:\n                break\n\n        # Build the marks index\n        if mark_keys:\n            pipe = self._client.pipeline()\n            for mark_key in mark_keys:\n                mark = self._extract_mark_from_key(mark_key)\n                await pipe.sadd(self._get_marks_index_key(), mark)\n            await pipe.execute()\n\n        return mark_keys\n\n    async def _get_all_mark_keys(self) -> list[str]:\n        \"\"\"Get all mark keys, compatible with both old and new data.\n\n        For new data (with marks index), this method uses the index directly.\n        For old data (without marks index), this method scans once and\n        migrates to the new structure.\n\n        Returns:\n            `list[str]`:\n                The list of all mark keys.\n        \"\"\"\n        marks_index_key = self._get_marks_index_key()\n\n        # Try to read from the index first\n        marks = await self._client.smembers(marks_index_key)\n        if marks:\n            # Index exists, use it\n            marks = self._decode_list(list(marks))\n            return [self._get_mark_key(mark) for mark in marks]\n\n        # Index doesn't exist, check if this is a new session\n        session_exists = await self._client.exists(self._get_session_key())\n        if not session_exists:\n            # New session, no data at all, return empty\n            return []\n\n        # Old session without index, need to scan and migrate (only once)\n        mark_keys = await self._scan_and_migrate_marks()\n        return mark_keys\n\n    async def get_memory(\n        self,\n        mark: str | None = None,\n        exclude_mark: str | None = None,\n        prepend_summary: bool = True,\n        **kwargs: Any,\n    ) -> list[Msg]:\n        \"\"\"Get the messages from the memory by mark (if provided). Otherwise,\n        get all messages.\n\n        .. note:: If `mark` and `exclude_mark` are both provided, the messages\n         will be filtered by both arguments.\n\n        .. note:: `mark` and `exclude_mark` should not overlap.\n\n        Args:\n            mark (`str | None`, optional):\n                The mark to filter messages. If `None`, return all messages.\n            exclude_mark (`str | None`, optional):\n                The mark to exclude messages. If provided, messages with\n                this mark will be excluded from the results.\n            prepend_summary (`bool`, defaults to True):\n                Whether to prepend the compressed summary as a message\n\n        Returns:\n            `list[Msg]`:\n                The list of messages retrieved from the storage.\n        \"\"\"\n        # Type checks\n        if not (mark is None or isinstance(mark, str)):\n            raise TypeError(\n                f\"The mark should be a string or None, but got {type(mark)}.\",\n            )\n\n        if not (exclude_mark is None or isinstance(exclude_mark, str)):\n            raise TypeError(\n                f\"The exclude_mark should be a string or None, but got \"\n                f\"{type(exclude_mark)}.\",\n            )\n\n        if mark is None:\n            # Obtain the message IDs from the session list\n            msg_ids = await self._client.lrange(self._get_session_key(), 0, -1)\n\n        else:\n            # Obtain the message IDs from the mark list\n            msg_ids = await self._client.lrange(\n                self._get_mark_key(mark),\n                0,\n                -1,\n            )\n\n        msg_ids = self._decode_list(msg_ids)\n\n        # Exclude messages by exclude_mark\n        if exclude_mark:\n            exclude_msg_ids = await self._client.lrange(\n                self._get_mark_key(exclude_mark),\n                0,\n                -1,\n            )\n            exclude_msg_ids = self._decode_list(exclude_msg_ids)\n            msg_ids = [_ for _ in msg_ids if _ not in exclude_msg_ids]\n\n        # Use mget for batch retrieval to avoid N+1 queries\n        messages: list[Msg] = []\n        if msg_ids:\n            msg_keys = [self._get_message_key(msg_id) for msg_id in msg_ids]\n            msg_data_list = await self._client.mget(msg_keys)\n\n            for msg_data in msg_data_list:\n                if msg_data is not None:\n                    # Decode if bytes\n                    msg_data = self._decode_if_bytes(msg_data)\n                    msg_dict = json.loads(msg_data)\n                    messages.append(Msg.from_dict(msg_dict))\n\n        # Refresh TTLs\n        await self._refresh_session_ttl()\n\n        if prepend_summary and self._compressed_summary:\n            return [\n                Msg(\n                    \"user\",\n                    self._compressed_summary,\n                    \"user\",\n                ),\n                *messages,\n            ]\n\n        return messages\n\n    async def add(\n        self,\n        memories: Msg | list[Msg] | None,\n        marks: str | list[str] | None = None,\n        skip_duplicated: bool = True,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Add message into the storage with the given mark (if provided).\n\n        Args:\n            memories (`Msg | list[Msg]`):\n                The message(s) to be added.\n            marks (`str | list[str] | None`, optional):\n                The mark(s) to associate with the message(s). If `None`, no\n                mark is associated.\n            skip_duplicated (`bool`, defaults to `True`):\n                If `True`, skip messages with duplicate IDs that already exist\n                in the storage. If `False`, allow duplicate message IDs to be\n                added to the session list (though the message data will be\n                overwritten).\n        \"\"\"\n        if memories is None:\n            return\n\n        if isinstance(memories, Msg):\n            memories = [memories]\n\n        # Normalize marks to a list\n        if marks is None:\n            mark_list = []\n        elif isinstance(marks, str):\n            mark_list = [marks]\n        else:\n            mark_list = marks\n\n        # Filter out existing messages if skip_duplicated is True\n        messages_to_add = memories\n        if skip_duplicated:\n            # Get all existing message IDs in the current session\n            existing_msg_ids = await self._client.lrange(\n                self._get_session_key(),\n                0,\n                -1,\n            )\n            existing_msg_ids = self._decode_list(existing_msg_ids)\n            existing_msg_ids_set = set(existing_msg_ids)\n\n            # Filter out messages that already exist\n            messages_to_add = [\n                m for m in memories if m.id not in existing_msg_ids_set\n            ]\n\n            # If all messages are duplicates, return early\n            if not messages_to_add:\n                return\n\n        # Use pipeline for atomic operations\n        pipe = self._client.pipeline()\n\n        # Push message ids into the session list\n        if messages_to_add:\n            await pipe.rpush(\n                self._get_session_key(),\n                *[m.id for m in messages_to_add],\n            )\n\n        # Store message data and marks\n        for m in messages_to_add:\n            # Record the message data\n            await pipe.set(\n                self._get_message_key(m.id),\n                json.dumps(m.to_dict(), ensure_ascii=False),\n            )\n\n            # Record the marks if provided\n            for mark in mark_list:\n                await pipe.rpush(self._get_mark_key(mark), m.id)\n                # Maintain the marks index\n                await pipe.sadd(self._get_marks_index_key(), mark)\n\n        # Refresh TTLs\n        await self._refresh_session_ttl(pipe=pipe)\n\n        await pipe.execute()\n\n    async def delete(\n        self,\n        msg_ids: list[str],\n        **kwargs: Any,\n    ) -> int:\n        \"\"\"Remove message(s) from the storage by their IDs.\n\n        Args:\n            msg_ids (`list[str]`):\n                The list of message IDs to be removed.\n\n        Returns:\n            `int`:\n                The number of messages removed.\n        \"\"\"\n        if not msg_ids:\n            return 0\n\n        # Get all mark keys using the new method (compatible with old data)\n        mark_keys = await self._get_all_mark_keys()\n\n        pipe = self._client.pipeline()\n        for msg_id in msg_ids:\n            # Remove from the session (0 means remove all occurrences)\n            await pipe.lrem(self._get_session_key(), 0, msg_id)\n\n            # Remove the message data\n            await pipe.delete(self._get_message_key(msg_id))\n\n            # Remove from all marks\n            for mark_key in mark_keys:\n                await pipe.lrem(mark_key, 0, msg_id)\n\n        # Refresh TTLs\n        await self._refresh_session_ttl(pipe=pipe)\n\n        results = await pipe.execute()\n\n        # Count actual deletions from lrem results (every 3rd result\n        # starting from 0)\n        removed_count = sum(\n            1\n            for i in range(\n                0,\n                len(msg_ids) * (2 + len(mark_keys)),\n                2 + len(mark_keys),\n            )\n            if results[i] > 0\n        )\n\n        return removed_count\n\n    async def delete_by_mark(\n        self,\n        mark: str | list[str],\n        **kwargs: Any,\n    ) -> int:\n        \"\"\"Remove messages from the storage by their marks.\n\n        Args:\n            mark (`str | list[str]`):\n                The mark(s) of the messages to be removed.\n\n        Returns:\n            `int`:\n                The number of messages removed.\n        \"\"\"\n        if isinstance(mark, str):\n            mark = [mark]\n\n        total_removed = 0\n\n        for m in mark:\n            mark_key = self._get_mark_key(m)\n            msg_ids = await self._client.lrange(mark_key, 0, -1)\n            msg_ids = self._decode_list(msg_ids)\n\n            if not msg_ids:\n                continue\n\n            # Remove messages by IDs\n            removed_count = await self.delete(\n                msg_ids,\n            )\n            total_removed += removed_count\n\n            # Delete the mark list\n            await self._client.delete(mark_key)\n\n            # Remove from the marks index\n            await self._client.srem(self._get_marks_index_key(), m)\n\n        # Refresh TTLs\n        await self._refresh_session_ttl()\n\n        return total_removed\n\n    async def clear(self) -> None:\n        \"\"\"Clear all messages belong to this session from the storage.\"\"\"\n        msg_ids = await self._client.lrange(self._get_session_key(), 0, -1)\n        msg_ids = self._decode_list(msg_ids)\n\n        # Get all mark keys using the new method (compatible with old data)\n        mark_keys = await self._get_all_mark_keys()\n\n        pipe = self._client.pipeline()\n\n        for msg_id in msg_ids:\n            # Remove the message data\n            await pipe.delete(self._get_message_key(msg_id))\n\n        # Delete the session list\n        await pipe.delete(self._get_session_key())\n\n        # Delete all mark lists\n        for mark_key in mark_keys:\n            await pipe.delete(mark_key)\n\n        # Delete the marks index\n        await pipe.delete(self._get_marks_index_key())\n\n        await pipe.execute()\n\n    async def size(self) -> int:\n        \"\"\"Get the number of messages in the storage.\n\n        Returns:\n            `int`:\n                The number of messages in the storage.\n        \"\"\"\n        size = await self._client.llen(self._get_session_key())\n        await self._refresh_session_ttl()\n        return size\n\n    async def update_messages_mark(\n        self,\n        new_mark: str | None,\n        old_mark: str | None = None,\n        msg_ids: list[str] | None = None,\n    ) -> int:\n        \"\"\"A unified method to update marks of messages in the storage (add,\n        remove, or change marks).\n\n        - If `msg_ids` is provided, the update will be applied to the messages\n         with the specified IDs.\n        - If `old_mark` is provided, the update will be applied to the\n         messages with the specified old mark. Otherwise, the `new_mark` will\n         be added to all messages (or those filtered by `msg_ids`).\n        - If `new_mark` is `None`, the mark will be removed from the messages.\n\n        Args:\n            new_mark (`str | None`, optional):\n                The new mark to set for the messages. If `None`, the mark\n                will be removed.\n            old_mark (`str | None`, optional):\n                The old mark to filter messages. If `None`, this constraint\n                is ignored.\n            msg_ids (`list[str] | None`, optional):\n                The list of message IDs to be updated. If `None`, this\n                constraint is ignored.\n\n        Returns:\n            `int`:\n                The number of messages updated.\n        \"\"\"\n        # Determine which message IDs to update\n        # Get source key based on old_mark\n        source_key = (\n            self._get_mark_key(old_mark)\n            if old_mark is not None\n            else self._get_session_key()\n        )\n        mark_msg_ids = await self._client.lrange(source_key, 0, -1)\n        mark_msg_ids = self._decode_list(mark_msg_ids)\n\n        # Check if we're removing all messages from old_mark\n        removing_all_from_old_mark = old_mark is not None and (\n            msg_ids is None or all(mid in set(msg_ids) for mid in mark_msg_ids)\n        )\n\n        # Filter by msg_ids if provided\n        if msg_ids is not None:\n            msg_ids_set = set(msg_ids)\n            mark_msg_ids = [mid for mid in mark_msg_ids if mid in msg_ids_set]\n\n        if not mark_msg_ids:\n            return 0\n\n        # Get existing IDs in new_mark list once (if needed)\n        existing_ids_set = set()\n        new_mark_key = None\n        if new_mark is not None:\n            new_mark_key = self._get_mark_key(new_mark)\n            existing_ids = await self._client.lrange(new_mark_key, 0, -1)\n            existing_ids = self._decode_list(existing_ids)\n            existing_ids_set = set(existing_ids)\n\n        # Use pipeline for batch operations\n        pipe = self._client.pipeline()\n        updated_count = 0\n\n        for msg_id in mark_msg_ids:\n            # Remove from old_mark list if applicable\n            if old_mark is not None:\n                await pipe.lrem(\n                    self._get_mark_key(old_mark),\n                    0,\n                    msg_id,\n                )\n\n            # Add to new_mark list only if not already present\n            if new_mark is not None and msg_id not in existing_ids_set:\n                await pipe.rpush(new_mark_key, msg_id)\n                existing_ids_set.add(msg_id)\n                # Maintain the marks index\n                await pipe.sadd(self._get_marks_index_key(), new_mark)\n\n            # Count update only if we actually did something\n            if old_mark is not None or new_mark is not None:\n                updated_count += 1\n\n        # Clean up old_mark only if we removed ALL messages from it\n        if old_mark is not None and removing_all_from_old_mark:\n            old_mark_key = self._get_mark_key(old_mark)\n            # After lrem operations, the old mark list will be empty\n            # Delete the mark key and remove from index\n            await pipe.delete(old_mark_key)\n            await pipe.srem(self._get_marks_index_key(), old_mark)\n\n        await self._refresh_session_ttl(pipe=pipe)\n\n        await pipe.execute()\n        return updated_count\n\n    async def close(self, close_connection_pool: bool | None = None) -> None:\n        \"\"\"Close the Redis client connection.\n\n        Args:\n            close_connection_pool (`bool | None`, optional):\n                Decides whether to close the connection pool used by this\n                Redis client, overriding Redis.auto_close_connection_pool.\n                By default, let Redis.auto_close_connection_pool decide\n                whether to close the connection pool\n        \"\"\"\n        await self._client.aclose(close_connection_pool=close_connection_pool)\n\n    async def __aenter__(self) -> \"RedisMemory\":\n        \"\"\"Enter the async context manager.\n\n        Returns:\n            `RedisMemory`:\n                The memory instance itself.\n        \"\"\"\n        return self\n\n    async def __aexit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_value: BaseException | None,\n        traceback: Any,\n    ) -> None:\n        \"\"\"Exit the async context manager and close the session.\n\n        Args:\n            exc_type (`type[BaseException] | None`):\n                The exception type if an exception was raised.\n            exc_value (`BaseException | None`):\n                The exception instance if an exception was raised.\n            traceback (`Any`):\n                The traceback object if an exception was raised.\n        \"\"\"\n        await self.close()\n"
  },
  {
    "path": "src/agentscope/memory/_working_memory/_sqlalchemy_memory.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The SQLAlchemy database storage module, which supports storing messages in\na SQL database using SQLAlchemy ORM (e.g., SQLite, PostgreSQL, MySQL).\"\"\"\nfrom typing import Any\n\nfrom sqlalchemy import (\n    Column,\n    String,\n    JSON,\n    BigInteger,\n    ForeignKey,\n    select,\n    delete,\n    update,\n    func,\n)\nfrom sqlalchemy.ext.asyncio import (\n    AsyncEngine,\n    AsyncSession,\n    async_sessionmaker,\n)\nfrom sqlalchemy.orm import declarative_base, relationship\n\nfrom ._base import MemoryBase\nfrom ...message import Msg\n\nBase: Any = declarative_base()\n\n\nclass AsyncSQLAlchemyMemory(MemoryBase):\n    \"\"\"The SQLAlchemy memory storage class for storing messages in a SQL\n    database using SQLAlchemy ORM, such as SQLite, PostgreSQL, MySQL, etc.\n\n    .. note:: All the operations in this class are within a specific session\n     and user context, identified by `session_id` and `user_id`. Cross-session\n     or cross-user operations are not supported. For example, the\n     `remove_messages` method will only remove messages that belong to the\n     specified `session_id` and `user_id`.\n\n    \"\"\"\n\n    class MessageTable(Base):\n        \"\"\"The default message table definition.\"\"\"\n\n        __tablename__ = \"message\"\n        \"\"\"The table name\"\"\"\n\n        id = Column(String(255), primary_key=True)\n        \"\"\"The id column, we use the f\"{user_id}-{session_id}-{message_id}\"\n        as the primary key to ensure uniqueness across users and sessions.\"\"\"\n\n        msg = Column(JSON, nullable=False)\n        \"\"\"The message JSON content column\"\"\"\n\n        session = relationship(\n            \"SessionTable\",\n            back_populates=\"messages\",\n        )\n        \"\"\"The foreign key to the session id relationship\"\"\"\n\n        session_id = Column(\n            String(255),\n            ForeignKey(\"session.id\"),\n            nullable=False,\n        )\n        \"\"\"The foreign key to the session id\"\"\"\n\n        index = Column(BigInteger, nullable=False, index=True)\n        \"\"\"The index column for ordering messages, so that we can retrieve\n        messages in the order they were added.\"\"\"\n\n    class MessageMarkTable(Base):\n        \"\"\"The default message mark table definition.\"\"\"\n\n        __tablename__ = \"message_mark\"\n        \"\"\"The table name\"\"\"\n\n        msg_id = Column(\n            String(255),\n            ForeignKey(\"message.id\", ondelete=\"CASCADE\"),\n            primary_key=True,\n        )\n        \"\"\"The message id column\"\"\"\n\n        mark = Column(String(255), primary_key=True)\n        \"\"\"The mark column\"\"\"\n\n    class SessionTable(Base):\n        \"\"\"The default session table definition.\"\"\"\n\n        __tablename__ = \"session\"\n        \"\"\"The table name\"\"\"\n\n        id = Column(String(255), primary_key=True)\n        \"\"\"The session id column\"\"\"\n\n        user = relationship(\"UserTable\", back_populates=\"sessions\")\n        \"\"\"The foreign key to the user id relationship\"\"\"\n\n        user_id = Column(String(255), ForeignKey(\"users.id\"), nullable=False)\n        \"\"\"The foreign key to the user id\"\"\"\n\n        messages = relationship(\"MessageTable\", back_populates=\"session\")\n        \"\"\"The relationship to messages\"\"\"\n\n    class UserTable(Base):\n        \"\"\"The default user table definition.\"\"\"\n\n        __tablename__ = \"users\"\n        \"\"\"The table name\"\"\"\n\n        id = Column(String(255), primary_key=True)\n        \"\"\"The user id column\"\"\"\n\n        sessions = relationship(\"SessionTable\", back_populates=\"user\")\n        \"\"\"The relationship to sessions\"\"\"\n\n    def __init__(\n        self,\n        engine_or_session: AsyncEngine | AsyncSession,\n        session_id: str | None = None,\n        user_id: str | None = None,\n    ) -> None:\n        \"\"\"Initialize the SqlAlchemyDBStorage with a SQLAlchemy session.\n\n        Args:\n            engine_or_session (`AsyncEngine | AsyncSession`):\n                The SQLAlchemy asynchronous engine or session to use for\n                database operations. If you're using a connection pool, maybe\n                you want to pass in an `AsyncSession` instance.\n            session_id (`str | None`, optional):\n                The session ID for the messages. If `None`, a default session\n                ID will be used.\n            user_id (`str | None`, optional):\n                The user ID for the messages. If `None`, a default user ID\n                will be used.\n\n        Raises:\n            `ValueError`:\n                If the `engine` parameter is not an instance of\n                `sqlalchemy.ext.asyncio.AsyncEngine` or `sqlalchemy.\n                ext.asyncio.AsyncSession`.\n        \"\"\"\n        super().__init__()\n\n        self._db_session: AsyncSession | None = None\n\n        if isinstance(engine_or_session, AsyncEngine):\n            self._session_factory = async_sessionmaker(\n                bind=engine_or_session,\n                expire_on_commit=False,\n            )\n\n        elif isinstance(engine_or_session, AsyncSession):\n            self._session_factory = None\n            self._db_session = engine_or_session\n\n        else:\n            raise ValueError(\n                \"The 'engine_or_session' parameter must be an instance of \"\n                \"sqlalchemy.ext.asyncio.AsyncEngine.\",\n            )\n\n        self.session_id = session_id or \"default_session\"\n        self.user_id = user_id or \"default_user\"\n\n        # Flag to track if tables and records have been initialized\n        self._initialized = False\n\n    def _make_message_id(self, msg_id: str) -> str:\n        \"\"\"Generate a composite primary key for a message.\n\n        Args:\n            msg_id (`str`):\n                The original message ID.\n\n        Returns:\n            `str`:\n                The composite primary key in the format\n                \"{user_id}-{session_id}-{message_id}\".\n        \"\"\"\n        return f\"{self.user_id}-{self.session_id}-{msg_id}\"\n\n    @property\n    def session(self) -> AsyncSession:\n        \"\"\"Get the current database session, creating one if it doesn't exist.\n\n        Returns:\n            `AsyncSession`:\n                The current database session.\n\n        Note:\n            - If an external session was provided, it will be returned as-is\n            - If using internal session factory, a new session will be created\n              if the current one is None or inactive, and _initialized flag\n              will be reset to ensure proper re-initialization\n        \"\"\"\n        # External session: return as-is (managed by caller)\n        if self._session_factory is None:\n            return self._db_session\n\n        # Internal session: check validity and recreate if needed\n        if self._db_session is None or not self._db_session.is_active:\n            self._db_session = self._session_factory()\n            # Reset initialized flag when creating new session\n            self._initialized = False\n\n        return self._db_session\n\n    async def _create_table(self) -> None:\n        \"\"\"Create tables in database.\"\"\"\n        # Skip if already initialized\n        if self._initialized:\n            return\n\n        # Obtain the engine first\n        engine: AsyncEngine = self.session.bind\n\n        async with engine.begin() as conn:\n            await conn.run_sync(Base.metadata.create_all)\n\n        # Track if we need to commit\n        needs_commit = False\n\n        # Create user record if not exists\n        result = await self.session.execute(\n            select(self.UserTable).filter(\n                self.UserTable.id == self.user_id,\n            ),\n        )\n        user_record = result.scalar_one_or_none()\n\n        if user_record is None:\n            user_record = self.UserTable(\n                id=self.user_id,\n            )\n            self.session.add(user_record)\n            needs_commit = True\n\n        # Create session record if not exists\n        result = await self.session.execute(\n            select(self.SessionTable).filter(\n                self.SessionTable.id == self.session_id,\n            ),\n        )\n        session_record = result.scalar_one_or_none()\n\n        if session_record is None:\n            session_record = self.SessionTable(\n                id=self.session_id,\n                user_id=self.user_id,\n            )\n            self.session.add(session_record)\n            needs_commit = True\n\n        # Commit once if any records were added\n        if needs_commit:\n            await self.session.commit()\n\n        # Mark as initialized\n        self._initialized = True\n\n    async def get_memory(\n        self,\n        mark: str | None = None,\n        exclude_mark: str | None = None,\n        prepend_summary: bool = True,\n        **kwargs: Any,\n    ) -> list[Msg]:\n        \"\"\"Get the messages from the memory by mark (if provided). Otherwise,\n        get all messages.\n\n        .. note:: If `mark` and `exclude_mark` are both provided, the messages\n         will be filtered by both arguments.\n\n        .. note:: `mark` and `exclude_mark` should not overlap.\n\n        Args:\n            mark (`str | None`, optional):\n                The mark to filter messages. If `None`, return all messages.\n            exclude_mark (`str | None`, optional):\n                The mark to exclude messages. If provided, messages with\n                this mark will be excluded from the results.\n            prepend_summary (`bool`, defaults to True):\n                Whether to prepend the compressed summary as a message\n\n        Raises:\n            `TypeError`:\n                If the provided mark is not a string or None.\n\n        Returns:\n            `list[Msg]`:\n                The list of messages retrieved from the storage.\n        \"\"\"\n        # Type checks\n        if mark is not None and not isinstance(mark, str):\n            raise TypeError(\n                f\"The mark should be a string or None, but got {type(mark)}.\",\n            )\n\n        if exclude_mark is not None and not isinstance(exclude_mark, str):\n            raise TypeError(\n                f\"The exclude_mark should be a string or None, but got \"\n                f\"{type(exclude_mark)}.\",\n            )\n\n        await self._create_table()\n\n        # Step 1: First filter by session_id to narrow down the dataset\n        # This ensures the database uses the session_id index first\n        base_query = select(self.MessageTable).filter(\n            self.MessageTable.session_id == self.session_id,\n        )\n\n        # Step 2: Apply mark filtering if provided\n        if mark:\n            # Join with mark table only on the session-filtered messages\n            base_query = base_query.join(\n                self.MessageMarkTable,\n                self.MessageTable.id == self.MessageMarkTable.msg_id,\n            ).filter(\n                self.MessageMarkTable.mark == mark,\n            )\n\n        # Step 3: Apply exclude_mark filtering if provided\n        if exclude_mark:\n            # Use a subquery to find message IDs with the exclude_mark\n            # within the current session only\n            exclude_subquery = (\n                select(self.MessageMarkTable.msg_id)\n                .filter(\n                    self.MessageMarkTable.msg_id.in_(\n                        select(self.MessageTable.id).filter(\n                            self.MessageTable.session_id == self.session_id,\n                        ),\n                    ),\n                    self.MessageMarkTable.mark == exclude_mark,\n                )\n                .scalar_subquery()\n            )\n            # Exclude messages whose IDs are in the subquery\n            base_query = base_query.filter(\n                self.MessageTable.id.notin_(exclude_subquery),\n            )\n\n        # Step 4: Order by index to maintain message order\n        query = base_query.order_by(self.MessageTable.index)\n\n        result = await self.session.execute(query)\n        results = result.scalars().all()\n\n        msgs = [Msg.from_dict(result.msg) for result in results]\n        if prepend_summary and self._compressed_summary:\n            return [\n                Msg(\n                    \"user\",\n                    self._compressed_summary,\n                    \"user\",\n                ),\n                *msgs,\n            ]\n\n        return msgs\n\n    async def add(\n        self,\n        memories: Msg | list[Msg] | None,\n        marks: str | list[str] | None = None,\n        skip_duplicated: bool = True,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Add message into the storage with the given mark (if provided).\n\n        Args:\n            memories (`Msg | list[Msg] | None`):\n                The message(s) to be added.\n            marks (`str | list[str] | None`, optional):\n                The mark(s) to associate with the message(s). If `None`, no\n                mark is associated.\n            skip_duplicated (`bool`, defaults to `True`):\n                If `True`, skip messages with duplicate IDs that already exist\n                in the storage. If `False`, raise an `IntegrityError` when\n                attempting to add a message with an existing ID.\n\n        Raises:\n            `IntegrityError`:\n                If a message with the same ID already exists in the storage\n                and `skip_duplicated` is set to `False`.\n        \"\"\"\n        if memories is None:\n            return\n\n        # Type checking\n        if isinstance(memories, Msg):\n            memories = [memories]\n        elif not (\n            isinstance(memories, list)\n            and all(isinstance(_, Msg) for _ in memories)\n        ):\n            raise TypeError(\n                \"The 'memories' parameter must be a Msg instance or a list of \"\n                f\"Msg instances, but got {type(memories)}.\",\n            )\n\n        if isinstance(marks, str):\n            marks = [marks]\n        elif marks is not None and not (\n            isinstance(marks, list) and all(isinstance(m, str) for m in marks)\n        ):\n            raise TypeError(\n                \"The 'marks' parameter must be a string or a list of strings, \"\n                f\"but got {type(marks)}.\",\n            )\n\n        # Create table if not exists\n        await self._create_table()\n\n        # If skip_duplicated is True, filter out existing messages\n        messages_to_add = memories\n        if skip_duplicated:\n            existing_msg_ids = set()\n            result = await self.session.execute(\n                select(self.MessageTable.id).filter(\n                    self.MessageTable.id.in_(\n                        [self._make_message_id(m.id) for m in memories],\n                    ),\n                ),\n            )\n            existing_msg_ids = {row[0] for row in result.fetchall()}\n\n            messages_to_add = [\n                m\n                for m in memories\n                if self._make_message_id(m.id) not in existing_msg_ids\n            ]\n\n            # If all messages are duplicates, return early\n            if not messages_to_add:\n                return\n\n        # Get the starting index once to avoid race conditions\n        start_index = await self._get_next_index()\n\n        # Add messages to message table\n        for i, m in enumerate(messages_to_add):\n            message_record = self.MessageTable(\n                id=self._make_message_id(m.id),\n                msg=m.to_dict(),\n                session_id=self.session_id,\n                index=start_index + i,\n            )\n            self.session.add(message_record)\n\n        # Create mark records if marks are provided (use bulk insert)\n        if marks:\n            mark_records = [\n                {\"msg_id\": self._make_message_id(msg.id), \"mark\": mark}\n                for msg in messages_to_add\n                for mark in marks\n            ]\n            if mark_records:\n                if skip_duplicated:\n                    # Query existing mark combinations to avoid duplicates\n                    result = await self.session.execute(\n                        select(\n                            self.MessageMarkTable.msg_id,\n                            self.MessageMarkTable.mark,\n                        ),\n                    )\n                    existing_marks = {\n                        (row[0], row[1]) for row in result.fetchall()\n                    }\n\n                    # Filter out existing mark combinations\n                    mark_records = [\n                        r\n                        for r in mark_records\n                        if (r[\"msg_id\"], r[\"mark\"]) not in existing_marks\n                    ]\n\n                if mark_records:\n                    await self.session.run_sync(\n                        lambda session: session.bulk_insert_mappings(\n                            self.MessageMarkTable,\n                            mark_records,\n                        ),\n                    )\n\n        await self.session.commit()\n\n    async def _get_next_index(self) -> int:\n        \"\"\"Get the next index for a new message in the current session.\n\n        Returns:\n            `int`:\n                The next index value.\n        \"\"\"\n        result = await self.session.execute(\n            select(self.MessageTable.index)\n            .filter(self.MessageTable.session_id == self.session_id)\n            .order_by(self.MessageTable.index.desc())\n            .limit(1),\n        )\n        max_index = result.scalar_one_or_none()\n        return (max_index + 1) if max_index is not None else 0\n\n    async def size(self) -> int:\n        \"\"\"Get the size of the messages in the storage.\"\"\"\n        result = await self.session.execute(\n            select(func.count(self.MessageTable.id)).filter(\n                self.MessageTable.session_id == self.session_id,\n            ),\n        )\n        return result.scalar_one()\n\n    async def clear(self) -> None:\n        \"\"\"Clear all messages from the storage.\"\"\"\n        # Delete all marks for messages in this session\n        await self.session.execute(\n            delete(self.MessageMarkTable).where(\n                self.MessageMarkTable.msg_id.in_(\n                    select(self.MessageTable.id).filter(\n                        self.MessageTable.session_id == self.session_id,\n                    ),\n                ),\n            ),\n        )\n\n        # Then delete all messages\n        await self.session.execute(\n            delete(self.MessageTable).filter(\n                self.MessageTable.session_id == self.session_id,\n            ),\n        )\n\n        await self.session.commit()\n\n    async def delete_by_mark(\n        self,\n        mark: str | list[str],\n        **kwargs: Any,\n    ) -> int:\n        \"\"\"Remove messages from the storage by their marks.\n\n        Args:\n            mark (`str | list[str]`):\n                The mark(s) of the messages to be removed.\n\n        Returns:\n            `int`:\n                The number of messages removed.\n        \"\"\"\n        if isinstance(mark, str):\n            mark = [mark]\n\n        # First, find message IDs that have the specified marks\n        query = (\n            select(self.MessageTable.id)\n            .join(\n                self.MessageMarkTable,\n                self.MessageTable.id == self.MessageMarkTable.msg_id,\n            )\n            .filter(\n                self.MessageTable.session_id == self.session_id,\n                self.MessageMarkTable.mark.in_(mark),\n            )\n        )\n\n        result = await self.session.execute(query)\n        msg_ids = [row[0] for row in result.all()]\n\n        if not msg_ids:\n            return 0\n\n        # Store the count before deletion\n        deleted_count = len(msg_ids)\n\n        # Delete marks first\n        await self.session.execute(\n            delete(self.MessageMarkTable).filter(\n                self.MessageMarkTable.msg_id.in_(msg_ids),\n            ),\n        )\n\n        # Then delete the messages\n        await self.session.execute(\n            delete(self.MessageTable).filter(\n                self.MessageTable.session_id == self.session_id,\n                self.MessageTable.id.in_(msg_ids),\n            ),\n        )\n\n        await self.session.commit()\n        return deleted_count\n\n    async def delete(\n        self,\n        msg_ids: list[str],\n        **kwargs: Any,\n    ) -> int:\n        \"\"\"Remove message(s) from the storage by their IDs.\n\n        .. note:: Although MessageMarkTable has CASCADE delete on foreign key,\n         we explicitly delete marks first for reliability across all database\n         engines and configurations. SQLAlchemy's bulk delete bypasses\n         ORM-level cascades, and SQLite requires foreign keys to be\n         explicitly enabled.\n\n        Args:\n            msg_ids (`list[str]`):\n                The list of message IDs to be removed.\n\n        Returns:\n            `int`:\n                The number of messages removed.\n        \"\"\"\n        # Convert to composite keys\n        composite_ids = [self._make_message_id(msg_id) for msg_id in msg_ids]\n\n        if not composite_ids:\n            return 0\n\n        # Store the count before deletion\n        deleted_count = len(composite_ids)\n\n        # Delete related marks first (explicit cleanup for reliability)\n        await self.session.execute(\n            delete(self.MessageMarkTable).filter(\n                self.MessageMarkTable.msg_id.in_(composite_ids),\n            ),\n        )\n\n        # Then delete the messages\n        await self.session.execute(\n            delete(self.MessageTable).filter(\n                self.MessageTable.session_id == self.session_id,\n                self.MessageTable.id.in_(composite_ids),\n            ),\n        )\n\n        await self.session.commit()\n        return deleted_count\n\n    async def update_messages_mark(\n        self,\n        new_mark: str | None,\n        old_mark: str | None = None,\n        msg_ids: list[str] | None = None,\n    ) -> int:\n        \"\"\"A unified method to update marks of messages in the storage (add,\n        remove, or change marks).\n\n        - If `msg_ids` is provided, the update will be applied to the messages\n         with the specified IDs.\n        - If `old_mark` is provided, the update will be applied to the\n         messages with the specified old mark. Otherwise, the `new_mark` will\n         be added to all messages (or those filtered by `msg_ids`).\n        - If `new_mark` is `None`, the mark will be removed from the messages.\n\n        Args:\n            new_mark (`str | None`, optional):\n                The new mark to set for the messages. If `None`, the mark\n                will be removed.\n            old_mark (`str | None`, optional):\n                The old mark to filter messages. If `None`, this constraint\n                is ignored.\n            msg_ids (`list[str] | None`, optional):\n                The list of message IDs to be updated. If `None`, this\n                constraint is ignored.\n\n        Returns:\n            `int`:\n                The number of messages updated.\n        \"\"\"\n\n        # Type checking\n        if new_mark is not None and not isinstance(new_mark, str):\n            raise ValueError(\n                f\"The 'new_mark' parameter must be a string or None, \"\n                f\"but got {type(new_mark)}.\",\n            )\n\n        if old_mark is not None and not isinstance(old_mark, str):\n            raise ValueError(\n                f\"The 'old_mark' parameter must be a string or None, \"\n                f\"but got {type(old_mark)}.\",\n            )\n\n        if msg_ids is not None and not (\n            isinstance(msg_ids, list)\n            and all(isinstance(_, str) for _ in msg_ids)\n        ):\n            raise ValueError(\n                f\"The 'msg_ids' parameter must be a list of strings or None, \"\n                f\"but got {type(msg_ids)}.\",\n            )\n\n        # First obtain the message ids that belong to this session\n        query = select(self.MessageTable).filter(\n            self.MessageTable.session_id == self.session_id,\n        )\n\n        # Filter by msg_ids if provided\n        if msg_ids is not None:\n            # Convert to composite keys\n            composite_ids = [\n                self._make_message_id(msg_id) for msg_id in msg_ids\n            ]\n            query = query.filter(self.MessageTable.id.in_(composite_ids))\n\n        # Filter by old_mark if provided\n        if old_mark is not None:\n            query = query.join(\n                self.MessageMarkTable,\n                self.MessageTable.id == self.MessageMarkTable.msg_id,\n            ).filter(self.MessageMarkTable.mark == old_mark)\n\n        # Obtain the message records\n        result = await self.session.execute(query)\n        msg_ids = [str(_.id) for _ in result.scalars().all()]\n\n        # Return early if no messages found\n        if not msg_ids:\n            return 0\n\n        if new_mark:\n            if old_mark:\n                # Replace old_mark with new_mark\n                return await self._replace_message_mark(\n                    msg_ids=msg_ids,\n                    old_mark=old_mark,\n                    new_mark=new_mark,\n                )\n\n            # Add new_mark to the messages\n            return await self._add_message_mark(\n                msg_ids=msg_ids,\n                mark=new_mark,\n            )\n\n        # Remove all marks from the messages\n        return await self._remove_message_mark(\n            msg_ids=msg_ids,\n            old_mark=old_mark,\n        )\n\n    async def _replace_message_mark(\n        self,\n        msg_ids: list[str],\n        old_mark: str,\n        new_mark: str,\n    ) -> int:\n        \"\"\"Replace the old mark with the new mark for the given messages by\n        updating records in the message_mark table.\n\n        Args:\n            msg_ids (`list[str]`):\n                The list of message IDs to be updated.\n            old_mark (`str`):\n                The old mark to be replaced.\n            new_mark (`str`):\n                The new mark to be set.\n\n        Returns:\n            `int`:\n                The number of messages updated.\n        \"\"\"\n\n        await self.session.execute(\n            update(self.MessageMarkTable)\n            .filter(\n                self.MessageMarkTable.msg_id.in_(msg_ids),\n                self.MessageMarkTable.mark == old_mark,\n            )\n            .values(mark=new_mark),\n        )\n        await self.session.commit()\n        return len(msg_ids)\n\n    async def _add_message_mark(self, msg_ids: list[str], mark: str) -> int:\n        \"\"\"Mark the messages with the given mark by adding records to the\n        message_mark table.\n\n        Args:\n            msg_ids (`list[str]`):\n                The list of message IDs to be marked.\n            mark (`str`):\n                The mark to be added to the messages.\n\n        Returns:\n            `int`:\n                The number of messages marked.\n        \"\"\"\n        # Use bulk insert for better performance\n        mark_records = [{\"msg_id\": msg_id, \"mark\": mark} for msg_id in msg_ids]\n\n        if mark_records:\n            await self.session.run_sync(\n                lambda session: session.bulk_insert_mappings(\n                    self.MessageMarkTable,\n                    mark_records,\n                ),\n            )\n\n        await self.session.commit()\n        return len(msg_ids)\n\n    async def _remove_message_mark(\n        self,\n        msg_ids: list[str],\n        old_mark: str | None,\n    ) -> int:\n        \"\"\"Remove marks from the messages by deleting records from the\n        message_mark table.\n\n        Args:\n            msg_ids (`list[str]`):\n                The list of message IDs to be unmarked.\n            old_mark (`str | None`):\n                The old mark to be removed. If `None`, all marks will be\n                removed from the messages.\n\n        Returns:\n            `int`:\n                The number of messages unmarked.\n        \"\"\"\n        delete_query = delete(self.MessageMarkTable).filter(\n            self.MessageMarkTable.msg_id.in_(msg_ids),\n        )\n\n        if old_mark:\n            delete_query = delete_query.filter(\n                self.MessageMarkTable.mark == old_mark,\n            )\n\n        await self.session.execute(delete_query)\n        await self.session.commit()\n        return len(msg_ids)\n\n    async def close(self) -> None:\n        \"\"\"Close the database session.\"\"\"\n        if self._db_session and self._db_session.is_active:\n            await self._db_session.close()\n\n        self._db_session = None\n        self._initialized = False\n\n    async def __aenter__(self) -> \"AsyncSQLAlchemyMemory\":\n        \"\"\"Enter the async context manager.\n\n        Returns:\n            `AsyncSQLAlchemyMemory`:\n                The memory instance itself.\n        \"\"\"\n        return self\n\n    async def __aexit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_value: BaseException | None,\n        traceback: Any,\n    ) -> None:\n        \"\"\"Exit the async context manager and close the session.\n\n        Args:\n            exc_type (`type[BaseException] | None`):\n                The exception type if an exception was raised.\n            exc_value (`BaseException | None`):\n                The exception instance if an exception was raised.\n            traceback (`Any`):\n                The traceback object if an exception was raised.\n        \"\"\"\n        await self.close()\n"
  },
  {
    "path": "src/agentscope/message/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The message module in agentscope.\"\"\"\n\nfrom ._message_block import (\n    ContentBlock,\n    TextBlock,\n    ThinkingBlock,\n    ToolUseBlock,\n    ToolResultBlock,\n    ImageBlock,\n    AudioBlock,\n    VideoBlock,\n    Base64Source,\n    URLSource,\n)\nfrom ._message_base import Msg\n\n\n__all__ = [\n    \"TextBlock\",\n    \"ThinkingBlock\",\n    \"Base64Source\",\n    \"URLSource\",\n    \"ImageBlock\",\n    \"AudioBlock\",\n    \"VideoBlock\",\n    \"ToolUseBlock\",\n    \"ToolResultBlock\",\n    \"ContentBlock\",\n    \"Msg\",\n]\n"
  },
  {
    "path": "src/agentscope/message/_message_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The message class in agentscope.\"\"\"\nfrom datetime import datetime\nfrom typing import Literal, List, overload, Sequence\n\nimport shortuuid\n\nfrom ._message_block import (\n    TextBlock,\n    ToolUseBlock,\n    ImageBlock,\n    AudioBlock,\n    ContentBlock,\n    VideoBlock,\n    ToolResultBlock,\n    ContentBlockTypes,\n)\nfrom ..types import JSONSerializableObject\n\n\nclass Msg:\n    \"\"\"The message class in agentscope.\"\"\"\n\n    def __init__(\n        self,\n        name: str,\n        content: str | Sequence[ContentBlock],\n        role: Literal[\"user\", \"assistant\", \"system\"],\n        metadata: dict[str, JSONSerializableObject] | None = None,\n        timestamp: str | None = None,\n        invocation_id: str | None = None,\n    ) -> None:\n        \"\"\"Initialize the Msg object.\n\n        Args:\n            name (`str`):\n                The name of the message sender.\n            content (`str | list[ContentBlock]`):\n                The content of the message.\n            role (`Literal[\"user\", \"assistant\", \"system\"]`):\n                The role of the message sender.\n            metadata (`dict[str, JSONSerializableObject] | None`, optional):\n                The metadata of the message, e.g. structured output.\n            timestamp (`str | None`, optional):\n                The created timestamp of the message. If not given, the\n                timestamp will be set automatically.\n            invocation_id (`str | None`, optional):\n                The related API invocation id, if any. This is useful for\n                tracking the message in the context of an API call.\n        \"\"\"\n\n        self.name = name\n\n        assert isinstance(\n            content,\n            (list, str),\n        ), \"The content must be a string or a list of content blocks.\"\n\n        self.content = content\n\n        assert role in [\"user\", \"assistant\", \"system\"]\n        self.role = role\n\n        self.metadata = metadata or {}\n\n        self.id = shortuuid.uuid()\n        self.timestamp = (\n            timestamp\n            or datetime.now().strftime(\n                \"%Y-%m-%d %H:%M:%S.%f\",\n            )[:-3]\n        )\n        self.invocation_id = invocation_id\n\n    def to_dict(self) -> dict:\n        \"\"\"Convert the message into JSON dict data.\"\"\"\n        return {\n            \"id\": self.id,\n            \"name\": self.name,\n            \"role\": self.role,\n            \"content\": self.content,\n            \"metadata\": self.metadata,\n            \"timestamp\": self.timestamp,\n        }\n\n    @classmethod\n    def from_dict(cls, json_data: dict) -> \"Msg\":\n        \"\"\"Load a message object from the given JSON data.\"\"\"\n        new_obj = cls(\n            name=json_data[\"name\"],\n            content=json_data[\"content\"],\n            role=json_data[\"role\"],\n            metadata=json_data.get(\"metadata\", None),\n            timestamp=json_data.get(\"timestamp\", None),\n            invocation_id=json_data.get(\"invocation_id\", None),\n        )\n\n        new_obj.id = json_data.get(\"id\", new_obj.id)\n        return new_obj\n\n    def has_content_blocks(\n        self,\n        block_type: Literal[\n            \"text\",\n            \"tool_use\",\n            \"tool_result\",\n            \"image\",\n            \"audio\",\n            \"video\",\n        ]\n        | None = None,\n    ) -> bool:\n        \"\"\"Check if the message has content blocks of the given type.\n\n        Args:\n            block_type (Literal[\"text\", \"tool_use\", \"tool_result\", \"image\", \\\n            \"audio\", \"video\"] | None, defaults to None):\n                The type of the block to be checked. If `None`, it will\n                check if there are any content blocks.\n        \"\"\"\n        return len(self.get_content_blocks(block_type)) > 0\n\n    def get_text_content(self, separator: str = \"\\n\") -> str | None:\n        \"\"\"Get the pure text blocks from the message content.\n\n        Args:\n            separator (`str`, defaults to `\\n`):\n                The separator to use when concatenating multiple text blocks.\n                Defaults to newline character.\n\n        Returns:\n            `str | None`:\n                The concatenated text content, or `None` if there is no text\n                content.\n        \"\"\"\n        if isinstance(self.content, str):\n            return self.content\n\n        gathered_text = []\n        for block in self.content:\n            if block.get(\"type\") == \"text\":\n                gathered_text.append(block[\"text\"])\n\n        if gathered_text:\n            return separator.join(gathered_text)\n\n        return None\n\n    @overload\n    def get_content_blocks(\n        self,\n        block_type: Literal[\"text\"],\n    ) -> Sequence[TextBlock]:\n        ...\n\n    @overload\n    def get_content_blocks(\n        self,\n        block_type: Literal[\"tool_use\"],\n    ) -> Sequence[ToolUseBlock]:\n        ...\n\n    @overload\n    def get_content_blocks(\n        self,\n        block_type: Literal[\"tool_result\"],\n    ) -> Sequence[ToolResultBlock]:\n        ...\n\n    @overload\n    def get_content_blocks(\n        self,\n        block_type: Literal[\"image\"],\n    ) -> Sequence[ImageBlock]:\n        ...\n\n    @overload\n    def get_content_blocks(\n        self,\n        block_type: Literal[\"audio\"],\n    ) -> Sequence[AudioBlock]:\n        ...\n\n    @overload\n    def get_content_blocks(\n        self,\n        block_type: Literal[\"video\"],\n    ) -> Sequence[VideoBlock]:\n        ...\n\n    @overload\n    def get_content_blocks(\n        self,\n        block_type: None = None,\n    ) -> Sequence[ContentBlock]:\n        ...\n\n    def get_content_blocks(\n        self,\n        block_type: ContentBlockTypes | List[ContentBlockTypes] | None = None,\n    ) -> Sequence[ContentBlock]:\n        \"\"\"Get the content in block format. If the content is a string,\n        it will be converted to a text block.\n\n        Args:\n            block_type (`ContentBlockTypes | List[ContentBlockTypes] | None`, \\\n            optional):\n                The type of the block to be extracted. If `None`, all blocks\n                will be returned.\n\n        Returns:\n            `List[ContentBlock]`:\n                The content blocks.\n        \"\"\"\n        blocks = []\n        if isinstance(self.content, str):\n            blocks.append(\n                TextBlock(type=\"text\", text=self.content),\n            )\n        else:\n            blocks = self.content or []\n\n        if isinstance(block_type, str):\n            blocks = [_ for _ in blocks if _[\"type\"] == block_type]\n\n        elif isinstance(block_type, list):\n            blocks = [_ for _ in blocks if _[\"type\"] in block_type]\n\n        return blocks\n\n    def __repr__(self) -> str:\n        \"\"\"Get the string representation of the message.\"\"\"\n        return (\n            f\"Msg(id='{self.id}', \"\n            f\"name='{self.name}', \"\n            f\"content={repr(self.content)}, \"\n            f\"role='{self.role}', \"\n            f\"metadata={repr(self.metadata)}, \"\n            f\"timestamp='{self.timestamp}', \"\n            f\"invocation_id='{self.invocation_id}')\"\n        )\n"
  },
  {
    "path": "src/agentscope/message/_message_block.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=R0901\n\"\"\"The content blocks of messages\"\"\"\n\nfrom typing import Literal, List\nfrom typing_extensions import TypedDict, Required\n\n\nclass TextBlock(TypedDict, total=False):\n    \"\"\"The text block.\"\"\"\n\n    type: Required[Literal[\"text\"]]\n    \"\"\"The type of the block\"\"\"\n    text: str\n    \"\"\"The text content\"\"\"\n\n\nclass ThinkingBlock(TypedDict, total=False):\n    \"\"\"The thinking block.\"\"\"\n\n    type: Required[Literal[\"thinking\"]]\n    \"\"\"The type of the block\"\"\"\n    thinking: str\n\n\nclass Base64Source(TypedDict, total=False):\n    \"\"\"The base64 source\"\"\"\n\n    type: Required[Literal[\"base64\"]]\n    \"\"\"The type of the src, must be `base64`\"\"\"\n\n    media_type: Required[str]\n    \"\"\"The media type of the data, e.g. `image/jpeg` or `audio/mpeg`\"\"\"\n\n    data: Required[str]\n    \"\"\"The base64 data, in format of RFC 2397\"\"\"\n\n\nclass URLSource(TypedDict, total=False):\n    \"\"\"The URL source\"\"\"\n\n    type: Required[Literal[\"url\"]]\n    \"\"\"The type of the src, must be `url`\"\"\"\n\n    url: Required[str]\n    \"\"\"The URL of the image or audio\"\"\"\n\n\nclass ImageBlock(TypedDict, total=False):\n    \"\"\"The image block\"\"\"\n\n    type: Required[Literal[\"image\"]]\n    \"\"\"The type of the block\"\"\"\n\n    source: Required[Base64Source | URLSource]\n    \"\"\"The src of the image\"\"\"\n\n\nclass AudioBlock(TypedDict, total=False):\n    \"\"\"The audio block\"\"\"\n\n    type: Required[Literal[\"audio\"]]\n    \"\"\"The type of the block\"\"\"\n\n    source: Required[Base64Source | URLSource]\n    \"\"\"The src of the audio\"\"\"\n\n\nclass VideoBlock(TypedDict, total=False):\n    \"\"\"The video block\"\"\"\n\n    type: Required[Literal[\"video\"]]\n    \"\"\"The type of the block\"\"\"\n\n    source: Required[Base64Source | URLSource]\n    \"\"\"The src of the audio\"\"\"\n\n\nclass ToolUseBlock(TypedDict, total=False):\n    \"\"\"The tool use block.\"\"\"\n\n    type: Required[Literal[\"tool_use\"]]\n    \"\"\"The type of the block, must be `tool_use`\"\"\"\n    id: Required[str]\n    \"\"\"The identity of the tool call\"\"\"\n    name: Required[str]\n    \"\"\"The name of the tool\"\"\"\n    input: Required[dict[str, object]]\n    \"\"\"The input of the tool\"\"\"\n    raw_input: str\n    \"\"\"The raw string input of the tool from the model API\"\"\"\n\n\nclass ToolResultBlock(TypedDict, total=False):\n    \"\"\"The tool result block.\"\"\"\n\n    type: Required[Literal[\"tool_result\"]]\n    \"\"\"The type of the block\"\"\"\n    id: Required[str]\n    \"\"\"The identity of the tool call result\"\"\"\n    output: Required[\n        str | List[TextBlock | ImageBlock | AudioBlock | VideoBlock]\n    ]\n    \"\"\"The output of the tool function\"\"\"\n    name: Required[str]\n    \"\"\"The name of the tool function\"\"\"\n\n\n# The content block\nContentBlock = (\n    ToolUseBlock\n    | ToolResultBlock\n    | TextBlock\n    | ThinkingBlock\n    | ImageBlock\n    | AudioBlock\n    | VideoBlock\n)\n\nContentBlockTypes = Literal[\n    \"text\",\n    \"thinking\",\n    \"tool_use\",\n    \"tool_result\",\n    \"image\",\n    \"audio\",\n    \"video\",\n]\n"
  },
  {
    "path": "src/agentscope/model/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The model module.\"\"\"\n\nfrom ._model_base import ChatModelBase\nfrom ._model_response import ChatResponse\nfrom ._dashscope_model import DashScopeChatModel\nfrom ._openai_model import OpenAIChatModel\nfrom ._anthropic_model import AnthropicChatModel\nfrom ._ollama_model import OllamaChatModel\nfrom ._gemini_model import GeminiChatModel\nfrom ._trinity_model import TrinityChatModel\n\n__all__ = [\n    \"ChatModelBase\",\n    \"ChatResponse\",\n    \"DashScopeChatModel\",\n    \"OpenAIChatModel\",\n    \"AnthropicChatModel\",\n    \"OllamaChatModel\",\n    \"GeminiChatModel\",\n    \"TrinityChatModel\",\n]\n"
  },
  {
    "path": "src/agentscope/model/_anthropic_model.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=too-many-branches, too-many-statements\n\"\"\"The Anthropic API model classes.\"\"\"\nimport copy\nimport json\nimport warnings\nfrom datetime import datetime\nfrom typing import (\n    Any,\n    AsyncGenerator,\n    TYPE_CHECKING,\n    List,\n    Literal,\n    Type,\n)\nfrom collections import OrderedDict\n\nfrom pydantic import BaseModel\n\nfrom ._model_base import ChatModelBase\nfrom ._model_response import ChatResponse\nfrom ._model_usage import ChatUsage\nfrom .._logging import logger\nfrom .._utils._common import (\n    _json_loads_with_repair,\n    _create_tool_from_base_model,\n)\nfrom ..message import TextBlock, ToolUseBlock, ThinkingBlock\nfrom ..tracing import trace_llm\nfrom ..types._json import JSONSerializableObject\n\nif TYPE_CHECKING:\n    from anthropic.types.message import Message\n    from anthropic import AsyncStream\nelse:\n    Message = \"anthropic.types.message.Message\"\n    AsyncStream = \"anthropic.AsyncStream\"\n\n\nclass AnthropicChatModel(ChatModelBase):\n    \"\"\"The Anthropic model wrapper for AgentScope.\"\"\"\n\n    def __init__(\n        self,\n        model_name: str,\n        api_key: str | None = None,\n        max_tokens: int = 2048,\n        stream: bool = True,\n        thinking: dict | None = None,\n        stream_tool_parsing: bool = True,\n        client_kwargs: dict[str, JSONSerializableObject] | None = None,\n        generate_kwargs: dict[str, JSONSerializableObject] | None = None,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Initialize the Anthropic chat model.\n\n        Args:\n            model_name (`str`):\n                The model names.\n            api_key (`str`):\n                The anthropic API key.\n            stream (`bool`):\n                The streaming output or not\n            max_tokens (`int`):\n                Limit the maximum token count the model can generate.\n            thinking (`dict | None`, default `None`):\n                Configuration for Claude's internal reasoning process.\n\n                .. code-block:: python\n                    :caption: Example of thinking\n\n                    {\n                        \"type\": \"enabled\" | \"disabled\",\n                        \"budget_tokens\": 1024\n                    }\n\n            stream_tool_parsing (`bool`, default to `True`):\n                Whether to parse incomplete tool use JSON during streaming\n                with auto-repair. If True, partial JSON (e.g., `'{\"a\": \"x'`)\n                is repaired to valid dicts ({\"a\": \"x\"}) in real-time for\n                immediate tool function input. Otherwise, the input field\n                remains {} until the final chunk arrives.\n            client_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n             optional):\n                The extra keyword arguments to initialize the Anthropic client.\n            generate_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n             optional):\n                The extra keyword arguments used in Anthropic API generation,\n                e.g. `temperature`, `seed`.\n            **kwargs (`Any`):\n                Additional keyword arguments.\n        \"\"\"\n\n        # Handle deprecated client_args parameter from kwargs\n        client_args = kwargs.pop(\"client_args\", None)\n        if client_args is not None and client_kwargs is not None:\n            raise ValueError(\n                \"Cannot specify both 'client_args' and 'client_kwargs'. \"\n                \"Please use only 'client_kwargs' (client_args is deprecated).\",\n            )\n\n        if client_args is not None:\n            logger.warning(\n                \"The parameter 'client_args' is deprecated and will be \"\n                \"removed in a future version. Please use 'client_kwargs' \"\n                \"instead. Automatically converting 'client_args' to \"\n                \"'client_kwargs'.\",\n            )\n            client_kwargs = client_args\n\n        if kwargs:\n            logger.warning(\n                \"Unknown keyword arguments: %s. These will be ignored.\",\n                list(kwargs.keys()),\n            )\n\n        try:\n            import anthropic\n        except ImportError as e:\n            raise ImportError(\n                \"Please install the `anthropic` package by running \"\n                \"`pip install anthropic`.\",\n            ) from e\n\n        super().__init__(model_name, stream)\n\n        self.client = anthropic.AsyncAnthropic(\n            api_key=api_key,\n            **(client_kwargs or {}),\n        )\n        self.max_tokens = max_tokens\n        self.thinking = thinking\n        self.stream_tool_parsing = stream_tool_parsing\n        self.generate_kwargs = generate_kwargs or {}\n\n    @trace_llm\n    async def __call__(\n        self,\n        messages: list[dict[str, Any]],\n        tools: list[dict] | None = None,\n        tool_choice: Literal[\"auto\", \"none\", \"required\"] | str | None = None,\n        structured_model: Type[BaseModel] | None = None,\n        **generate_kwargs: Any,\n    ) -> ChatResponse | AsyncGenerator[ChatResponse, None]:\n        \"\"\"Get the response from Anthropic chat completions API by the given\n        arguments.\n\n        Args:\n            messages (`list[dict]`):\n                A list of dictionaries, where `role` and `content` fields are\n                required, and `name` field is optional.\n            tools (`list[dict]`, default `None`):\n                The tools JSON schemas that in format of:\n\n                .. code-block:: python\n                    :caption: Example of tools JSON schemas\n\n                    [\n                        {\n                            \"type\": \"function\",\n                            \"function\": {\n                                \"name\": \"xxx\",\n                                \"description\": \"xxx\",\n                                \"parameters\": {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"param1\": {\n                                            \"type\": \"string\",\n                                            \"description\": \"...\"\n                                        },\n                                        # Add more parameters as needed\n                                    },\n                                    \"required\": [\"param1\"]\n                            }\n                        },\n                        # More schemas here\n                    ]\n\n            tool_choice (`Literal[\"auto\", \"none\", \"required\"] | str \\\n            | None`, default `None`):\n                Controls which (if any) tool is called by the model.\n                 Can be \"auto\", \"none\", \"required\", or specific tool\n                 name. For more details, please refer to\n                 https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/implement-tool-use\n            structured_model (`Type[BaseModel] | None`, default `None`):\n                A Pydantic BaseModel class that defines the expected structure\n                for the model's output. When provided, the model will be forced\n                to return data that conforms to this schema by automatically\n                converting the BaseModel to a tool function and setting\n                `tool_choice` to enforce its usage. This enables structured\n                output generation.\n\n                .. note:: When `structured_model` is specified,\n                    both `tools` and `tool_choice` parameters are ignored,\n                    and the model will only perform structured output\n                    generation without calling any other tools.\n\n            **generate_kwargs (`Any`):\n                The keyword arguments for Anthropic chat completions API,\n                e.g. `temperature`, `top_p`, etc. Please\n                refer to the Anthropic API documentation for more details.\n\n        Returns:\n            `ChatResponse | AsyncGenerator[ChatResponse, None]`:\n                The response from the Anthropic chat completions API.\"\"\"\n\n        kwargs: dict[str, Any] = {\n            \"model\": self.model_name,\n            \"max_tokens\": self.max_tokens,\n            \"stream\": self.stream,\n            **self.generate_kwargs,\n            **generate_kwargs,\n        }\n        if self.thinking and \"thinking\" not in kwargs:\n            kwargs[\"thinking\"] = self.thinking\n\n        if tools:\n            kwargs[\"tools\"] = self._format_tools_json_schemas(tools)\n\n        if tool_choice:\n            # Handle deprecated \"any\" option with warning\n            if tool_choice == \"any\":\n                warnings.warn(\n                    '\"any\" is deprecated and will be removed in a future '\n                    \"version.\",\n                    DeprecationWarning,\n                )\n                tool_choice = \"required\"\n            self._validate_tool_choice(tool_choice, tools)\n            kwargs[\"tool_choice\"] = self._format_tool_choice(tool_choice)\n\n        if structured_model:\n            if tools or tool_choice:\n                logger.warning(\n                    \"structured_model is provided. Both 'tools' and \"\n                    \"'tool_choice' parameters will be overridden and \"\n                    \"ignored. The model will only perform structured output \"\n                    \"generation without calling any other tools.\",\n                )\n            format_tool = _create_tool_from_base_model(structured_model)\n            kwargs[\"tools\"] = self._format_tools_json_schemas(\n                [format_tool],\n            )\n            kwargs[\"tool_choice\"] = self._format_tool_choice(\n                format_tool[\"function\"][\"name\"],\n            )\n\n        # Extract the system message\n        if messages[0][\"role\"] == \"system\":\n            kwargs[\"system\"] = messages[0][\"content\"]\n            messages = messages[1:]\n\n        kwargs[\"messages\"] = messages\n\n        start_datetime = datetime.now()\n\n        response = await self.client.messages.create(**kwargs)\n\n        if self.stream:\n            return self._parse_anthropic_stream_completion_response(\n                start_datetime,\n                response,\n                structured_model,\n            )\n\n        # Non-streaming response\n        parsed_response = await self._parse_anthropic_completion_response(\n            start_datetime,\n            response,\n            structured_model,\n        )\n\n        return parsed_response\n\n    async def _parse_anthropic_completion_response(\n        self,\n        start_datetime: datetime,\n        response: Message,\n        structured_model: Type[BaseModel] | None = None,\n    ) -> ChatResponse:\n        \"\"\"Given an Anthropic Message object, extract the content blocks and\n        usages from it.\n\n        Args:\n            start_datetime (`datetime`):\n                The start datetime of the response generation.\n            response (`Message`):\n                Anthropic Message object to parse.\n            structured_model (`Type[BaseModel] | None`, default `None`):\n                A Pydantic BaseModel class that defines the expected structure\n                for the model's output.\n\n        Returns:\n            ChatResponse (`ChatResponse`):\n                A ChatResponse object containing the content blocks and usage.\n\n        .. note::\n            If `structured_model` is not `None`, the expected structured output\n            will be stored in the metadata of the `ChatResponse`.\n        \"\"\"\n        content_blocks: List[ThinkingBlock | TextBlock | ToolUseBlock] = []\n        metadata = None\n\n        if hasattr(response, \"content\") and response.content:\n            for content_block in response.content:\n                if (\n                    hasattr(content_block, \"type\")\n                    and content_block.type == \"thinking\"\n                ):\n                    thinking_block = ThinkingBlock(\n                        type=\"thinking\",\n                        thinking=content_block.thinking,\n                    )\n                    thinking_block[\"signature\"] = content_block.signature\n                    content_blocks.append(thinking_block)\n\n                elif (\n                    hasattr(content_block, \"type\")\n                    and content_block.type == \"text\"\n                ):\n                    content_blocks.append(\n                        TextBlock(\n                            type=\"text\",\n                            text=content_block.text,\n                        ),\n                    )\n\n                elif (\n                    hasattr(content_block, \"type\")\n                    and content_block.type == \"tool_use\"\n                ):\n                    content_blocks.append(\n                        ToolUseBlock(\n                            type=\"tool_use\",\n                            id=content_block.id,\n                            name=content_block.name,\n                            input=content_block.input,\n                        ),\n                    )\n                    if structured_model:\n                        metadata = content_block.input\n\n        usage = None\n        if response.usage:\n            usage = ChatUsage(\n                input_tokens=response.usage.input_tokens,\n                output_tokens=response.usage.output_tokens,\n                time=(datetime.now() - start_datetime).total_seconds(),\n            )\n\n        parsed_response = ChatResponse(\n            content=content_blocks,\n            usage=usage,\n            metadata=metadata,\n        )\n\n        return parsed_response\n\n    async def _parse_anthropic_stream_completion_response(\n        self,\n        start_datetime: datetime,\n        response: AsyncStream,\n        structured_model: Type[BaseModel] | None = None,\n    ) -> AsyncGenerator[ChatResponse, None]:\n        \"\"\"Given an Anthropic streaming response, extract the content blocks\n        and usages from it and yield ChatResponse objects.\n\n        Args:\n            start_datetime (`datetime`):\n                The start datetime of the response generation.\n            response (`AsyncStream`):\n                Anthropic AsyncStream object to parse.\n            structured_model (`Type[BaseModel] | None`, default `None`):\n                A Pydantic BaseModel class that defines the expected structure\n                for the model's output.\n\n        Returns:\n            `AsyncGenerator[ChatResponse, None]`:\n                An async generator that yields ChatResponse objects containing\n                the content blocks and usage information for each chunk in\n                the streaming response.\n\n        .. note::\n            If `structured_model` is not `None`, the expected structured output\n            will be stored in the metadata of the `ChatResponse`.\n        \"\"\"\n\n        usage = None\n        text_buffer = \"\"\n        thinking_buffer = \"\"\n        thinking_signature = \"\"\n        tool_calls = OrderedDict()\n        tool_call_buffers = {}\n        last_input_objs = {}  # Store last input_obj for each tool_call\n        res = None\n        metadata = None\n\n        # Record the last yielded content to parse the tools' input\n        last_content = None\n\n        async for event in response:\n            content_changed = False\n            thinking_changed = False\n\n            if event.type == \"message_start\":\n                message = event.message\n                if message.usage:\n                    usage = ChatUsage(\n                        input_tokens=message.usage.input_tokens,\n                        output_tokens=getattr(\n                            message.usage,\n                            \"output_tokens\",\n                            0,\n                        ),\n                        time=(datetime.now() - start_datetime).total_seconds(),\n                    )\n\n            elif event.type == \"content_block_start\":\n                if event.content_block.type == \"tool_use\":\n                    block_index = event.index\n                    tool_block = event.content_block\n                    tool_calls[block_index] = {\n                        \"type\": \"tool_use\",\n                        \"id\": tool_block.id,\n                        \"name\": tool_block.name,\n                        \"input\": \"\",\n                    }\n                    tool_call_buffers[block_index] = \"\"\n                    content_changed = True\n\n            elif event.type == \"content_block_delta\":\n                block_index = event.index\n                delta = event.delta\n                if delta.type == \"text_delta\":\n                    text_buffer += delta.text\n                    content_changed = True\n                elif delta.type == \"thinking_delta\":\n                    thinking_buffer += delta.thinking\n                    thinking_changed = True\n                elif delta.type == \"signature_delta\":\n                    thinking_signature = delta.signature\n                elif (\n                    delta.type == \"input_json_delta\"\n                    and block_index in tool_calls\n                ):\n                    tool_call_buffers[block_index] += delta.partial_json or \"\"\n                    tool_calls[block_index][\"input\"] = tool_call_buffers[\n                        block_index\n                    ]\n                    content_changed = True\n\n            elif event.type == \"message_delta\":\n                if event.usage and usage:\n                    usage.output_tokens = event.usage.output_tokens\n\n            if (thinking_changed or content_changed) and usage:\n                contents: list = []\n                if thinking_buffer:\n                    thinking_block = ThinkingBlock(\n                        type=\"thinking\",\n                        thinking=thinking_buffer,\n                    )\n                    thinking_block[\"signature\"] = thinking_signature\n                    contents.append(thinking_block)\n                if text_buffer:\n                    contents.append(\n                        TextBlock(\n                            type=\"text\",\n                            text=text_buffer,\n                        ),\n                    )\n                for block_index, tool_call in tool_calls.items():\n                    input_str = tool_call[\"input\"]\n                    tool_id = tool_call[\"id\"]\n\n                    # If parsing the tool input in streaming mode\n                    if self.stream_tool_parsing:\n                        repaired_input = _json_loads_with_repair(\n                            input_str or \"{}\",\n                        )\n                        # If the new repaired input is shorter than one in the\n                        # last chunk, use the last one to avoid regression\n                        last_input = last_input_objs.get(tool_id, {})\n                        if len(json.dumps(last_input)) > len(\n                            json.dumps(repaired_input),\n                        ):\n                            repaired_input = last_input\n                        last_input_objs[tool_id] = repaired_input\n\n                    else:\n                        repaired_input = {}\n\n                    contents.append(\n                        ToolUseBlock(\n                            type=tool_call[\"type\"],\n                            id=tool_call[\"id\"],\n                            name=tool_call[\"name\"],\n                            input=repaired_input,\n                            raw_input=input_str,\n                        ),\n                    )\n\n                    if structured_model:\n                        metadata = repaired_input\n\n                if contents:\n                    res = ChatResponse(\n                        content=contents,\n                        usage=usage,\n                        metadata=metadata,\n                    )\n                    yield res\n                    last_content = copy.deepcopy(contents)\n\n        # If stream_tool_parsing is False, yield last contents\n        if not self.stream_tool_parsing and last_content and tool_calls:\n            metadata = None\n            # Update tool use blocks in last_contents inplace\n            for block in last_content:\n                if block.get(\"type\") == \"tool_use\":\n                    block[\"input\"] = input_obj = _json_loads_with_repair(\n                        block.get(\"raw_input\") or \"{}\",\n                    )\n\n                    if structured_model:\n                        metadata = input_obj\n\n            yield ChatResponse(\n                content=last_content,\n                usage=usage,\n                metadata=metadata,\n            )\n\n    def _format_tools_json_schemas(\n        self,\n        schemas: list[dict[str, Any]],\n    ) -> list[dict[str, Any]]:\n        \"\"\"Format the JSON schemas of the tool functions to the format that\n        Anthropic API expects.\"\"\"\n        formatted_schemas = []\n        for schema in schemas:\n            assert (\n                \"function\" in schema\n            ), f\"Invalid schema: {schema}, expect key 'function'.\"\n\n            assert \"name\" in schema[\"function\"], (\n                f\"Invalid schema: {schema}, \"\n                \"expect key 'name' in 'function' field.\"\n            )\n\n            formatted_schemas.append(\n                {\n                    \"name\": schema[\"function\"][\"name\"],\n                    \"description\": schema[\"function\"].get(\"description\", \"\"),\n                    \"input_schema\": schema[\"function\"].get(\"parameters\", {}),\n                },\n            )\n\n        return formatted_schemas\n\n    def _format_tool_choice(\n        self,\n        tool_choice: Literal[\"auto\", \"none\", \"required\"] | str | None,\n    ) -> dict | None:\n        \"\"\"Format tool_choice parameter for API compatibility.\n\n        Args:\n            tool_choice (`Literal[\"auto\", \"none\", \"required\"] | str \\\n                | None`, default `None`):\n                Controls which (if any) tool is called by the model.\n                 Can be \"auto\", \"none\", \"required\", or specific tool\n                 name. For more details, please refer to\n                 https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/implement-tool-use\n        Returns:\n            `dict | None`:\n                The formatted tool choice configuration dict, or None if\n                tool_choice is None.\n        \"\"\"\n        if tool_choice is None:\n            return None\n\n        type_mapping = {\n            \"auto\": {\"type\": \"auto\"},\n            \"none\": {\"type\": \"none\"},\n            \"required\": {\"type\": \"any\"},\n        }\n        if tool_choice in type_mapping:\n            return type_mapping[tool_choice]\n\n        return {\"type\": \"tool\", \"name\": tool_choice}\n"
  },
  {
    "path": "src/agentscope/model/_dashscope_model.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The dashscope API model classes.\"\"\"\nimport copy\nimport collections\nimport json\nimport os\nimport warnings\nfrom datetime import datetime\nfrom http import HTTPStatus\nfrom typing import (\n    Any,\n    AsyncGenerator,\n    Generator,\n    Union,\n    TYPE_CHECKING,\n    List,\n    Literal,\n    Type,\n)\nfrom pydantic import BaseModel\nfrom aioitertools import iter as giter\n\nfrom ._model_base import ChatModelBase\nfrom ._model_response import ChatResponse\nfrom ._model_usage import ChatUsage\nfrom .._utils._common import (\n    _json_loads_with_repair,\n    _create_tool_from_base_model,\n)\nfrom ..message import TextBlock, ToolUseBlock, ThinkingBlock\nfrom ..tracing import trace_llm\nfrom ..types import JSONSerializableObject\nfrom .._logging import logger\n\nif TYPE_CHECKING:\n    from dashscope.api_entities.dashscope_response import GenerationResponse\n    from dashscope.api_entities.dashscope_response import (\n        MultiModalConversationResponse,\n    )\nelse:\n    GenerationResponse = (\n        \"dashscope.api_entities.dashscope_response.GenerationResponse\"\n    )\n    MultiModalConversationResponse = (\n        \"dashscope.api_entities.dashscope_response.\"\n        \"MultiModalConversationResponse\"\n    )\n\n\nclass DashScopeChatModel(ChatModelBase):\n    \"\"\"The DashScope chat model class, which unifies the Generation and\n    MultimodalConversation APIs into one method.\n\n    This class provides a unified interface for DashScope API by automatically\n    selecting between text-only (Generation API) and multimodal\n    (MultiModalConversation API) endpoints. The `multimodality` parameter\n    allows explicit control over API selection:\n\n    - When `multimodality=True`: Forces use of MultiModalConversation API\n      for handling images, videos, and other multimodal inputs\n    - When `multimodality=False`: Forces use of Generation API for\n      text-only processing\n    - When `multimodality=None` (default): Automatically selects the API\n      based on model name (e.g., models with \"-vl\" suffix or starting\n      with \"qvq\" will use MultiModalConversation API)\n\n    This design enables seamless switching between text and multimodal\n    models without changing code structure, making it easier to work with\n    DashScope's diverse model offerings.\n    \"\"\"\n\n    def __init__(\n        self,\n        model_name: str,\n        api_key: str,\n        stream: bool = True,\n        enable_thinking: bool | None = None,\n        multimodality: bool | None = None,\n        generate_kwargs: dict[str, JSONSerializableObject] | None = None,\n        base_http_api_url: str | None = None,\n        stream_tool_parsing: bool = True,\n        **_kwargs: Any,\n    ) -> None:\n        \"\"\"Initialize the DashScope chat model.\n\n        Args:\n            model_name (`str`):\n                The model names.\n            api_key (`str`):\n                The dashscope API key.\n            stream (`bool`):\n                The streaming output or not\n            enable_thinking (`bool | None`, optional):\n                Enable thinking or not, only support Qwen3, QwQ, DeepSeek-R1.\n                Refer to `DashScope documentation\n                <https://help.aliyun.com/zh/model-studio/deep-thinking>`_\n                for more details.\n            multimodality (`bool | None`, optional):\n                Whether to use multimodal conversation API. If `True`,\n                it will use `dashscope.AioMultiModalConversation.call`\n                to process multimodal inputs such as images and text. If\n                `False`, it will use\n                `dashscope.aigc.generation.AioGeneration.call` to process\n                text inputs. If `None` (default), the choice is based on\n                the model name.\n            generate_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n            optional):\n               The extra keyword arguments used in DashScope API generation,\n               e.g. `temperature`, `seed`.\n            base_http_api_url (`str | None`, optional):\n                The base URL for DashScope API requests. If not provided,\n                the default base URL from the DashScope SDK will be used.\n            stream_tool_parsing (`bool`, default to `True`):\n                Whether to parse incomplete tool use JSON in streaming mode\n                with auto-repair. If True, partial JSON (e.g., `'{\"a\": \"x'`)\n                is repaired to valid dicts (`{\"a\": \"x\"}`) in real-time for\n                immediate tool function input. Otherwise, the input field\n                remains {} until the final chunk arrives.\n            **_kwargs (`Any`):\n                Additional keyword arguments.\n        \"\"\"\n        if enable_thinking and not stream:\n            logger.info(\n                \"In DashScope API, `stream` must be True when \"\n                \"`enable_thinking` is True. \",\n            )\n            stream = True\n\n        super().__init__(model_name, stream)\n\n        self.api_key = api_key\n        self.enable_thinking = enable_thinking\n        self.multimodality = multimodality\n        self.generate_kwargs = generate_kwargs or {}\n        self.stream_tool_parsing = stream_tool_parsing\n\n        if base_http_api_url is not None:\n            import dashscope\n\n            dashscope.base_http_api_url = base_http_api_url\n\n        # Load headers from environment variable if exists\n        headers = os.getenv(\"DASHSCOPE_API_HEADERS\")\n        if headers:\n            try:\n                headers = json.loads(str(headers))\n                if not isinstance(headers, dict):\n                    raise json.JSONDecodeError(\"\", \"\", 0)\n\n                if self.generate_kwargs.get(\"headers\"):\n                    headers.update(self.generate_kwargs[\"headers\"])\n\n                self.generate_kwargs[\"headers\"] = headers\n\n            except json.JSONDecodeError:\n                logger.warning(\n                    \"Failed to parse DASHSCOPE_API_HEADERS environment \"\n                    \"variable as JSON. It should be a JSON object.\",\n                )\n\n    @trace_llm\n    async def __call__(\n        self,\n        messages: list[dict[str, Any]],\n        tools: list[dict] | None = None,\n        tool_choice: Literal[\"auto\", \"none\", \"required\"] | str | None = None,\n        structured_model: Type[BaseModel] | None = None,\n        **kwargs: Any,\n    ) -> ChatResponse | AsyncGenerator[ChatResponse, None]:\n        \"\"\"Get the response from the dashscope\n        Generation/MultimodalConversation API by the given arguments.\n\n        .. note:: We unify the dashscope generation and multimodal conversation\n         APIs into one method, since they support similar arguments and share\n         the same functionality.\n\n        Args:\n            messages (`list[dict[str, Any]]`):\n                A list of dictionaries, where `role` and `content` fields are\n                required.\n            tools (`list[dict] | None`, default `None`):\n                The tools JSON schemas that the model can use.\n            tool_choice (`Literal[\"auto\", \"none\", \"required\"] | str \\\n             |  None`,  default `None`):\n                Controls which (if any) tool is called by the model.\n                 Can be \"auto\", \"none\", \"required\", or specific tool name.\n                 Note: DashScope API only supports \"auto\" and \"none\", so\n                 \"required\" will be converted to \"auto\".\n                 For more details, please refer to\n                 https://help.aliyun.com/zh/model-studio/qwen-function-calling\n            structured_model (`Type[BaseModel] | None`, default `None`):\n                A Pydantic BaseModel class that defines the expected structure\n                for the model's output. When provided, the model will be forced\n                to return data that conforms to this schema by automatically\n                converting the BaseModel to a tool function and setting\n                `tool_choice` to enforce its usage. This enables structured\n                output generation.\n\n                .. note:: When `structured_model` is specified,\n                    both `tools` and `tool_choice` parameters are ignored,\n                    and the model will only perform structured output\n                    generation without calling any other tools.\n\n            **kwargs (`Any`):\n                The keyword arguments for DashScope chat completions API,\n                e.g. `temperature`, `max_tokens`, `top_p`, etc. Please\n                refer to `DashScope documentation\n                <https://help.aliyun.com/zh/dashscope/developer-reference/api-details>`_\n                for more detailed arguments.\n        \"\"\"\n        import dashscope\n\n        kwargs = {\n            \"messages\": messages,\n            \"model\": self.model_name,\n            \"stream\": self.stream,\n            \"result_format\": \"message\",\n            # In agentscope, the `incremental_output` must be `True` when\n            # `self.stream` is True\n            \"incremental_output\": self.stream,\n            **self.generate_kwargs,\n            **kwargs,\n        }\n\n        if tools:\n            kwargs[\"tools\"] = self._format_tools_json_schemas(tools)\n\n        if tool_choice:\n            # Handle deprecated \"any\" option with warning\n            if tool_choice in [\"any\", \"required\"]:\n                warnings.warn(\n                    f\"'{tool_choice}' is not supported by DashScope API. \"\n                    \"It will be converted to 'auto'.\",\n                    DeprecationWarning,\n                )\n                tool_choice = \"auto\"\n\n            self._validate_tool_choice(tool_choice, tools)\n            kwargs[\"tool_choice\"] = self._format_tool_choice(tool_choice)\n\n        if (\n            self.enable_thinking is not None\n            and \"enable_thinking\" not in kwargs\n        ):\n            kwargs[\"enable_thinking\"] = self.enable_thinking\n\n        if structured_model:\n            if tools or tool_choice:\n                logger.warning(\n                    \"structured_model is provided. Both 'tools' and \"\n                    \"'tool_choice' parameters will be overridden and \"\n                    \"ignored. The model will only perform structured output \"\n                    \"generation without calling any other tools.\",\n                )\n            format_tool = _create_tool_from_base_model(structured_model)\n            kwargs[\"tools\"] = self._format_tools_json_schemas(\n                [format_tool],\n            )\n            kwargs[\"tool_choice\"] = self._format_tool_choice(\n                format_tool[\"function\"][\"name\"],\n            )\n\n        start_datetime = datetime.now()\n        if self.multimodality or (\n            self.multimodality is None\n            and (\n                self.model_name.startswith(\n                    \"qvq\",\n                )\n                or \"-vl\" in self.model_name\n            )\n        ):\n            response = await dashscope.AioMultiModalConversation.call(\n                api_key=self.api_key,\n                **kwargs,\n            )\n\n        else:\n            response = await dashscope.aigc.generation.AioGeneration.call(\n                api_key=self.api_key,\n                **kwargs,\n            )\n\n        if self.stream:\n            return self._parse_dashscope_stream_response(\n                start_datetime,\n                response,\n                structured_model,\n            )\n\n        parsed_response = await self._parse_dashscope_generation_response(\n            start_datetime,\n            response,\n            structured_model,\n        )\n\n        return parsed_response\n\n    # pylint: disable=too-many-branches, too-many-statements\n    async def _parse_dashscope_stream_response(\n        self,\n        start_datetime: datetime,\n        response: Union[\n            AsyncGenerator[GenerationResponse, None],\n            AsyncGenerator[MultiModalConversationResponse, None],\n            Generator[MultiModalConversationResponse, None, None],\n        ],\n        structured_model: Type[BaseModel] | None = None,\n    ) -> AsyncGenerator[ChatResponse, Any]:\n        \"\"\"Given a DashScope streaming response generator, extract the content\n            blocks and usages from it and yield ChatResponse objects.\n\n        Args:\n            start_datetime (`datetime`):\n                The start datetime of the response generation.\n            response (\n                `Union[AsyncGenerator[GenerationResponse, None], \\\n                AsyncGenerator[MultiModalConversationResponse, None], \\\n                Generator[MultiModalConversationResponse, None, None]]`\n            ):\n                DashScope streaming response (async) generator\n                (GenerationResponse or MultiModalConversationResponse).\n            structured_model (`Type[BaseModel] | None`, default `None`):\n                A Pydantic BaseModel class that defines the expected structure\n                for the model's output.\n\n        Returns:\n            AsyncGenerator[ChatResponse, Any]:\n                An async generator that yields ChatResponse objects containing\n                the content blocks and usage information for each chunk in the\n                streaming response.\n\n        .. note::\n            If `structured_model` is not `None`, the expected structured output\n            will be stored in the metadata of the `ChatResponse`.\n        \"\"\"\n        acc_content, acc_thinking_content = \"\", \"\"\n        acc_tool_calls = collections.defaultdict(dict)\n        last_input_objs = {}  # Store last input_obj for each tool_call\n        metadata = None\n        last_content = None\n        usage = None\n\n        async for chunk in giter(response):\n            if chunk.status_code != HTTPStatus.OK:\n                raise RuntimeError(\n                    f\"Failed to get response from _ API: {chunk}\",\n                )\n\n            message = chunk.output.choices[0].message\n\n            # Update reasoning content\n            if isinstance(message.get(\"reasoning_content\"), str):\n                acc_thinking_content += message[\"reasoning_content\"]\n\n            # Update text content\n            if isinstance(message.content, str):\n                acc_content += message.content\n            elif isinstance(message.content, list):\n                for item in message.content:\n                    if isinstance(item, dict) and \"text\" in item:\n                        acc_content += item[\"text\"]\n\n            # Update tool calls\n            for tool_call in message.get(\"tool_calls\", []):\n                index = tool_call.get(\"index\", 0)\n\n                if \"id\" in tool_call and tool_call[\"id\"] != acc_tool_calls[\n                    index\n                ].get(\"id\"):\n                    acc_tool_calls[index][\"id\"] = (\n                        acc_tool_calls[index].get(\"id\", \"\") + tool_call[\"id\"]\n                    )\n\n                if \"function\" in tool_call:\n                    func = tool_call[\"function\"]\n                    if \"name\" in func:\n                        acc_tool_calls[index][\"name\"] = (\n                            acc_tool_calls[index].get(\"name\", \"\")\n                            + func[\"name\"]\n                        )\n\n                    if \"arguments\" in func:\n                        acc_tool_calls[index][\"arguments\"] = (\n                            acc_tool_calls[index].get(\"arguments\", \"\")\n                            + func[\"arguments\"]\n                        )\n\n            # Build content blocks (always include thinking and text)\n            content_blocks: list[TextBlock | ToolUseBlock | ThinkingBlock] = []\n\n            if acc_thinking_content:\n                content_blocks.append(\n                    ThinkingBlock(\n                        type=\"thinking\",\n                        thinking=acc_thinking_content,\n                    ),\n                )\n\n            if acc_content:\n                content_blocks.append(\n                    TextBlock(\n                        type=\"text\",\n                        text=acc_content,\n                    ),\n                )\n\n            for tool_call in acc_tool_calls.values():\n                # Only add intermediate tool use blocks if\n                # stream_tool_parsing is True\n                tool_id = tool_call.get(\"id\", \"\")\n                input_str = tool_call.get(\"arguments\")\n\n                # If parsing the tool input in streaming mode\n                if self.stream_tool_parsing:\n                    repaired_input = _json_loads_with_repair(\n                        input_str or \"{}\",\n                    )\n                    # If the new repaired input is shorter than one in the last\n                    # chunk, use the last one to avoid regression\n                    last_input = last_input_objs.get(tool_id, {})\n                    if len(json.dumps(last_input)) > len(\n                        json.dumps(repaired_input),\n                    ):\n                        repaired_input = last_input\n                    last_input_objs[tool_id] = repaired_input\n\n                else:\n                    # Otherwise, keep input as empty dict until the final chunk\n                    repaired_input = {}\n\n                content_blocks.append(\n                    ToolUseBlock(\n                        type=\"tool_use\",\n                        id=tool_id,\n                        name=tool_call.get(\"name\", \"\"),\n                        input=repaired_input,\n                        raw_input=input_str,\n                    ),\n                )\n\n                if structured_model:\n                    metadata = repaired_input\n\n            if chunk.usage:\n                usage = ChatUsage(\n                    input_tokens=chunk.usage.input_tokens,\n                    output_tokens=chunk.usage.output_tokens,\n                    time=(datetime.now() - start_datetime).total_seconds(),\n                    metadata=chunk.usage,\n                )\n\n            if content_blocks:\n                parsed_chunk = ChatResponse(\n                    content=content_blocks,\n                    usage=usage,\n                    metadata=metadata,\n                )\n                yield parsed_chunk\n                last_content = copy.deepcopy(content_blocks)\n\n        # If stream_tool_parsing is False, we need to parse the final tool\n        # use inputs here\n        if not self.stream_tool_parsing and last_content and acc_tool_calls:\n            metadata = None\n            # Update tool use blocks in last_contents inplace\n            for block in last_content:\n                if block.get(\"type\") == \"tool_use\":\n                    block[\"input\"] = input_obj = _json_loads_with_repair(\n                        str(block.get(\"raw_input\") or \"{}\"),\n                    )\n\n                    if structured_model:\n                        metadata = input_obj\n\n            yield ChatResponse(\n                content=last_content,\n                usage=usage,\n                metadata=metadata,\n            )\n\n    async def _parse_dashscope_generation_response(\n        self,\n        start_datetime: datetime,\n        response: Union[\n            GenerationResponse,\n            MultiModalConversationResponse,\n        ],\n        structured_model: Type[BaseModel] | None = None,\n    ) -> ChatResponse:\n        \"\"\"Given a DashScope GenerationResponse object, extract the content\n        blocks and usages from it.\n\n        Args:\n            start_datetime (`datetime`):\n                The start datetime of the response generation.\n            response (\n                `Union[GenerationResponse, MultiModalConversationResponse]`\n            ):\n                Dashscope GenerationResponse | MultiModalConversationResponse\n                object to parse.\n            structured_model (`Type[BaseModel] | None`, default `None`):\n                A Pydantic BaseModel class that defines the expected structure\n                for the model's output.\n\n        Returns:\n            ChatResponse (`ChatResponse`):\n                A ChatResponse object containing the content blocks and usage.\n\n        .. note::\n            If `structured_model` is not `None`, the expected structured output\n            will be stored in the metadata of the `ChatResponse`.\n        \"\"\"\n        # Collect the content blocks from the response.\n        if response.status_code != 200:\n            raise RuntimeError(response)\n\n        content_blocks: List[TextBlock | ToolUseBlock] = []\n        metadata: dict | None = None\n\n        message = response.output.choices[0].message\n        content = message.get(\"content\")\n\n        if response.output.choices[0].message.get(\"content\") not in [\n            None,\n            \"\",\n            [],\n        ]:\n            if isinstance(content, list):\n                for item in content:\n                    if isinstance(item, dict) and \"text\" in item:\n                        content_blocks.append(\n                            TextBlock(\n                                type=\"text\",\n                                text=item[\"text\"],\n                            ),\n                        )\n            else:\n                content_blocks.append(\n                    TextBlock(\n                        type=\"text\",\n                        text=content,\n                    ),\n                )\n\n        if message.get(\"tool_calls\"):\n            for tool_call in message[\"tool_calls\"]:\n                input_ = _json_loads_with_repair(\n                    tool_call[\"function\"].get(\n                        \"arguments\",\n                        \"{}\",\n                    )\n                    or \"{}\",\n                )\n                content_blocks.append(\n                    ToolUseBlock(\n                        type=\"tool_use\",\n                        name=tool_call[\"function\"][\"name\"],\n                        input=input_,\n                        id=tool_call[\"id\"],\n                    ),\n                )\n\n                if structured_model:\n                    metadata = input_\n\n        # Usage information\n        usage = None\n        if response.usage:\n            usage = ChatUsage(\n                input_tokens=response.usage.input_tokens,\n                output_tokens=response.usage.output_tokens,\n                time=(datetime.now() - start_datetime).total_seconds(),\n                metadata=response.usage,\n            )\n\n        parsed_response = ChatResponse(\n            content=content_blocks,\n            usage=usage,\n            metadata=metadata,\n        )\n\n        return parsed_response\n\n    def _format_tools_json_schemas(\n        self,\n        schemas: list[dict[str, Any]],\n    ) -> list[dict[str, Any]]:\n        \"\"\"Format the tools JSON schema into required format for DashScope API.\n\n        Args:\n            schemas (`dict[str, dict[str, Any]]`):\n                The tools JSON schemas.\n        \"\"\"\n        # Check schemas format\n        for value in schemas:\n            if (\n                not isinstance(value, dict)\n                or \"type\" not in value\n                or value[\"type\"] != \"function\"\n                or \"function\" not in value\n            ):\n                raise ValueError(\n                    f\"Each schema must be a dict with 'type' as 'function' \"\n                    f\"and 'function' key, got {value}\",\n                )\n\n        return schemas\n\n    def _format_tool_choice(\n        self,\n        tool_choice: Literal[\"auto\", \"none\", \"required\"] | str | None,\n    ) -> str | dict | None:\n        \"\"\"Format tool_choice parameter for API compatibility.\n\n        Args:\n            tool_choice (`Literal[\"auto\", \"none\", \"required\"] | str \\\n            | None`, default  `None`):\n                Controls which (if any) tool is called by the model. For more\n                details, please refer to\n                https://help.aliyun.com/zh/model-studio/qwen-function-calling\n\n        Returns:\n            `dict | None`:\n                The formatted tool choice configuration dict, or None if\n                    tool_choice is None.\n        \"\"\"\n        if tool_choice is None:\n            return None\n        if tool_choice in [\"auto\", \"none\"]:\n            return tool_choice\n        if tool_choice == \"required\":\n            return \"auto\"\n        return {\"type\": \"function\", \"function\": {\"name\": tool_choice}}\n"
  },
  {
    "path": "src/agentscope/model/_gemini_model.py",
    "content": "# -*- coding: utf-8 -*-\n# mypy: disable-error-code=\"dict-item\"\n\"\"\"The Google Gemini model in agentscope.\"\"\"\nimport base64\nimport copy\nimport warnings\nfrom datetime import datetime\nimport json\nfrom typing import (\n    AsyncGenerator,\n    Any,\n    TYPE_CHECKING,\n    AsyncIterator,\n    Literal,\n    Type,\n    List,\n)\n\nfrom pydantic import BaseModel\n\nfrom .._logging import logger\nfrom .._utils._common import _json_loads_with_repair\nfrom ..message import ToolUseBlock, TextBlock, ThinkingBlock\nfrom ._model_usage import ChatUsage\nfrom ._model_base import ChatModelBase\nfrom ._model_response import ChatResponse\nfrom ..tracing import trace_llm\nfrom ..types import JSONSerializableObject\n\nif TYPE_CHECKING:\n    from google.genai.types import GenerateContentResponse\nelse:\n    GenerateContentResponse = \"google.genai.types.GenerateContentResponse\"\n\n\ndef _flatten_json_schema(schema: dict) -> dict:\n    \"\"\"Flatten a JSON schema by resolving all $ref references.\n\n    .. note::\n        Gemini API does not support `$defs` and `$ref` in JSON schemas.\n        This function resolves all `$ref` references by inlining the\n        referenced definitions, producing a self-contained schema without\n        any references.\n\n    Args:\n        schema (`dict`):\n            The JSON schema that may contain `$defs` and `$ref` references.\n\n    Returns:\n        `dict`:\n            A flattened JSON schema with all references resolved inline.\n    \"\"\"\n    # Deep copy to avoid modifying the original schema\n    schema = copy.deepcopy(schema)\n\n    # Extract $defs if present\n    defs = schema.pop(\"$defs\", {})\n\n    def _resolve_ref(obj: Any, visited: set | None = None) -> Any:\n        \"\"\"Recursively resolve $ref references in the schema.\"\"\"\n        if visited is None:\n            visited = set()\n\n        if not isinstance(obj, dict):\n            if isinstance(obj, list):\n                return [_resolve_ref(item, visited.copy()) for item in obj]\n            return obj\n\n        # Handle $ref\n        if \"$ref\" in obj:\n            ref_path = obj[\"$ref\"]\n            # Extract definition name from \"#/$defs/DefinitionName\"\n            if ref_path.startswith(\"#/$defs/\"):\n                def_name = ref_path[len(\"#/$defs/\") :]\n\n                # Prevent infinite recursion for circular references\n                if def_name in visited:\n                    logger.warning(\n                        \"Circular reference detected for '%s' in tool schema\",\n                        def_name,\n                    )\n                    return {\n                        \"type\": \"object\",\n                        \"description\": f\"(circular: {def_name})\",\n                    }\n\n                visited.add(def_name)\n\n                if def_name in defs:\n                    # Recursively resolve any nested refs in the definition\n                    resolved = _resolve_ref(\n                        defs[def_name],\n                        visited.copy(),\n                    )\n                    # Merge any additional properties from the original object\n                    # (excluding $ref itself)\n                    for key, value in obj.items():\n                        if key != \"$ref\":\n                            resolved[key] = _resolve_ref(value, visited.copy())\n                    return resolved\n\n            # If we can't resolve the ref, return as-is (shouldn't happen)\n            return obj\n\n        # Recursively process all nested objects\n        result = {}\n        for key, value in obj.items():\n            result[key] = _resolve_ref(value, visited.copy())\n\n        return result\n\n    return _resolve_ref(schema)\n\n\nclass GeminiChatModel(ChatModelBase):\n    \"\"\"The Google Gemini chat model class in agentscope.\"\"\"\n\n    def __init__(\n        self,\n        model_name: str,\n        api_key: str,\n        stream: bool = True,\n        thinking_config: dict | None = None,\n        client_kwargs: dict[str, JSONSerializableObject] | None = None,\n        generate_kwargs: dict[str, JSONSerializableObject] | None = None,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Initialize the Gemini chat model.\n\n        Args:\n            model_name (`str`):\n                The name of the Gemini model to use, e.g. \"gemini-2.5-flash\".\n            api_key (`str`):\n                The API key for Google Gemini.\n            stream (`bool`, default `True`):\n                Whether to use streaming output or not.\n            thinking_config (`dict | None`, optional):\n                Thinking config, supported models are 2.5 Pro, 2.5 Flash, etc.\n                Refer to https://ai.google.dev/gemini-api/docs/thinking for\n                more details.\n\n                .. code-block:: python\n                    :caption: Example of thinking_config\n\n                    {\n                        \"include_thoughts\": True, # enable thoughts or not\n                        \"thinking_budget\": 1024   # Max tokens for reasoning\n                    }\n\n            client_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n             optional):\n                The extra keyword arguments to initialize the Gemini client.\n            generate_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n             optional):\n               The extra keyword arguments used in Gemini API generation,\n               e.g. `temperature`, `seed`.\n            **kwargs (`Any`):\n                Additional keyword arguments.\n        \"\"\"\n\n        # Handle deprecated client_args parameter from kwargs\n        client_args = kwargs.pop(\"client_args\", None)\n        if client_args is not None and client_kwargs is not None:\n            raise ValueError(\n                \"Cannot specify both 'client_args' and 'client_kwargs'. \"\n                \"Please use only 'client_kwargs' (client_args is deprecated).\",\n            )\n\n        if client_args is not None:\n            logger.warning(\n                \"The parameter 'client_args' is deprecated and will be \"\n                \"removed in a future version. Please use 'client_kwargs' \"\n                \"instead. Automatically converting 'client_args' to \"\n                \"'client_kwargs'.\",\n            )\n            client_kwargs = client_args\n\n        if kwargs:\n            logger.warning(\n                \"Unknown keyword arguments: %s. These will be ignored.\",\n                list(kwargs.keys()),\n            )\n\n        try:\n            from google import genai\n        except ImportError as e:\n            raise ImportError(\n                \"Please install gemini Python sdk with \"\n                \"`pip install -q -U google-genai`\",\n            ) from e\n\n        super().__init__(model_name, stream)\n\n        self.client = genai.Client(\n            api_key=api_key,\n            **(client_kwargs or {}),\n        )\n        self.thinking_config = thinking_config\n        self.generate_kwargs = generate_kwargs or {}\n\n    @trace_llm\n    async def __call__(\n        self,\n        messages: list[dict],\n        tools: list[dict] | None = None,\n        tool_choice: Literal[\"auto\", \"none\", \"required\"] | str | None = None,\n        structured_model: Type[BaseModel] | None = None,\n        **config_kwargs: Any,\n    ) -> ChatResponse | AsyncGenerator[ChatResponse, None]:\n        \"\"\"Call the Gemini model with the provided arguments.\n\n        Args:\n            messages (`list[dict[str, Any]]`):\n                A list of dictionaries, where `role` and `content` fields are\n                required.\n            tools (`list[dict] | None`, default `None`):\n                The tools JSON schemas that the model can use.\n            tool_choice (`Literal[\"auto\", \"none\", \"required\"] | str \\\n            | None`, default `None`):\n                Controls which (if any) tool is called by the model.\n                 Can be \"auto\", \"none\", \"required\", or specific tool name.\n                 For more details, please refer to\n                 https://ai.google.dev/gemini-api/docs/function-calling?hl=en&example=meeting#function_calling_modes\n            structured_model (`Type[BaseModel] | None`, default `None`):\n                A Pydantic BaseModel class that defines the expected structure\n                for the model's output.\n\n                .. note:: When `structured_model` is specified,\n                    both `tools` and `tool_choice` parameters are ignored,\n                    and the model will only perform structured output\n                    generation without calling any other tools.\n\n                For more details, please refer to\n                    https://ai.google.dev/gemini-api/docs/structured-output\n\n            **config_kwargs (`Any`):\n                The keyword arguments for Gemini chat completions API.\n        \"\"\"\n\n        config: dict = {\n            \"thinking_config\": self.thinking_config,\n            **self.generate_kwargs,\n            **config_kwargs,\n        }\n\n        if tools:\n            config[\"tools\"] = self._format_tools_json_schemas(tools)\n\n        if tool_choice:\n            # Handle deprecated \"any\" option with warning\n            if tool_choice == \"any\":\n                warnings.warn(\n                    '\"any\" is deprecated and will be removed in a future '\n                    \"version.\",\n                    DeprecationWarning,\n                )\n                tool_choice = \"required\"\n            self._validate_tool_choice(tool_choice, tools)\n            config[\"tool_config\"] = self._format_tool_choice(tool_choice)\n\n        if structured_model:\n            if tools or tool_choice:\n                logger.warning(\n                    \"structured_model is provided. Both 'tools' and \"\n                    \"'tool_choice' parameters will be overridden and \"\n                    \"ignored. The model will only perform structured output \"\n                    \"generation without calling any other tools.\",\n                )\n            config.pop(\"tools\", None)\n            config.pop(\"tool_config\", None)\n            config[\"response_mime_type\"] = \"application/json\"\n            config[\"response_schema\"] = structured_model\n\n        # Prepare the arguments for the Gemini API call\n        kwargs: dict[str, JSONSerializableObject] = {\n            \"model\": self.model_name,\n            \"contents\": messages,\n            \"config\": config,\n        }\n\n        start_datetime = datetime.now()\n        if self.stream:\n            response = await self.client.aio.models.generate_content_stream(\n                **kwargs,\n            )\n\n            return self._parse_gemini_stream_generation_response(\n                start_datetime,\n                response,\n                structured_model,\n            )\n\n        # non-streaming\n        response = await self.client.aio.models.generate_content(\n            **kwargs,\n        )\n\n        parsed_response = self._parse_gemini_generation_response(\n            start_datetime,\n            response,\n            structured_model,\n        )\n\n        return parsed_response\n\n    def _extract_usage(\n        self,\n        usage_metadata: Any,\n        start_datetime: datetime,\n    ) -> ChatUsage | None:\n        \"\"\"Extract ChatUsage from usage_metadata safely, returning None if\n        unavailable or if token counts are None.\n\n        Args:\n            usage_metadata:\n                The usage metadata object from the Gemini response.\n            start_datetime (`datetime`):\n                The start datetime of the generation.\n\n        Returns:\n            `ChatUsage | None`:\n                A ChatUsage object, or None if data is unavailable.\n        \"\"\"\n        if not usage_metadata:\n            return None\n        prompt_tokens = usage_metadata.prompt_token_count\n        total_tokens = usage_metadata.total_token_count\n        if prompt_tokens is not None and total_tokens is not None:\n            return ChatUsage(\n                input_tokens=prompt_tokens,\n                output_tokens=total_tokens - prompt_tokens,\n                time=(datetime.now() - start_datetime).total_seconds(),\n            )\n        return None\n\n    async def _parse_gemini_stream_generation_response(\n        self,\n        start_datetime: datetime,\n        response: AsyncIterator[GenerateContentResponse],\n        structured_model: Type[BaseModel] | None = None,\n    ) -> AsyncGenerator[ChatResponse, None]:\n        \"\"\"Given a Gemini streaming generation response, extract the\n        content blocks and usages from it and yield ChatResponse objects.\n\n        Args:\n            start_datetime (`datetime`):\n                The start datetime of the response generation.\n            response (`AsyncIterator[GenerateContentResponse]`):\n                Gemini GenerateContentResponse async iterator to parse.\n            structured_model (`Type[BaseModel] | None`, default `None`):\n                A Pydantic BaseModel class that defines the expected structure\n                for the model's output.\n\n        Returns:\n            `AsyncGenerator[ChatResponse, None]`:\n                An async generator that yields ChatResponse objects containing\n                the content blocks and usage information for each chunk in the\n                streaming response.\n\n        .. note::\n            If `structured_model` is not `None`, the expected structured output\n            will be stored in the metadata of the `ChatResponse`.\n        \"\"\"\n\n        text = \"\"\n        thinking = \"\"\n        tool_calls: list[ToolUseBlock] = []\n        metadata: dict | None = None\n        async for chunk in response:\n            if (\n                chunk.candidates\n                and chunk.candidates[0].content\n                and chunk.candidates[0].content.parts\n            ):\n                for part in chunk.candidates[0].content.parts:\n                    if part.text:\n                        if part.thought:\n                            thinking += part.text\n                        else:\n                            text += part.text\n\n                    if part.function_call:\n                        keyword_args = part.function_call.args or {}\n                        # .. note:: Gemini API always returns None for\n                        # function_call.id, so we use thought_signature\n                        # as the unique identifier for tool\n                        # calls when available. That maybe\n                        # infeasible someday, but Gemini\n                        # requires the thought_signature for some\n                        # llms like gemini-3-pro\n\n                        if part.thought_signature:\n                            call_id = base64.b64encode(\n                                part.thought_signature,\n                            ).decode(\"utf-8\")\n                        else:\n                            call_id = part.function_call.id\n\n                        tool_calls.append(\n                            ToolUseBlock(\n                                type=\"tool_use\",\n                                id=call_id,\n                                name=part.function_call.name,\n                                input=keyword_args,\n                                raw_input=json.dumps(\n                                    keyword_args,\n                                    ensure_ascii=False,\n                                ),\n                            ),\n                        )\n\n            # Text parts\n            if text and structured_model:\n                metadata = _json_loads_with_repair(text)\n\n            usage = self._extract_usage(chunk.usage_metadata, start_datetime)\n\n            # The content blocks for the current chunk\n            content_blocks: list = []\n\n            if thinking:\n                content_blocks.append(\n                    ThinkingBlock(\n                        type=\"thinking\",\n                        thinking=thinking,\n                    ),\n                )\n\n            if text:\n                content_blocks.append(\n                    TextBlock(\n                        type=\"text\",\n                        text=text,\n                    ),\n                )\n\n            yield ChatResponse(\n                content=content_blocks + tool_calls,\n                usage=usage,\n                metadata=metadata,\n            )\n\n    def _parse_gemini_generation_response(\n        self,\n        start_datetime: datetime,\n        response: GenerateContentResponse,\n        structured_model: Type[BaseModel] | None = None,\n    ) -> ChatResponse:\n        \"\"\"Given a Gemini chat completion response object, extract the content\n           blocks and usages from it.\n\n        Args:\n            start_datetime (`datetime`):\n                The start datetime of the response generation.\n            response (`GenerateContentResponse`):\n                The Gemini generation response object to parse.\n            structured_model (`Type[BaseModel] | None`, default `None`):\n                A Pydantic BaseModel class that defines the expected structure\n                for the model's output.\n\n        Returns:\n            ChatResponse (`ChatResponse`):\n                A ChatResponse object containing the content blocks and usage.\n\n        .. note::\n            If `structured_model` is not `None`, the expected structured output\n            will be stored in the metadata of the `ChatResponse`.\n        \"\"\"\n        content_blocks: List[TextBlock | ToolUseBlock | ThinkingBlock] = []\n        metadata: dict | None = None\n        tool_calls: list = []\n\n        if (\n            response.candidates\n            and response.candidates[0].content\n            and response.candidates[0].content.parts\n        ):\n            for part in response.candidates[0].content.parts:\n                if part.text:\n                    if part.thought:\n                        content_blocks.append(\n                            ThinkingBlock(\n                                type=\"thinking\",\n                                thinking=part.text,\n                            ),\n                        )\n                    else:\n                        content_blocks.append(\n                            TextBlock(\n                                type=\"text\",\n                                text=part.text,\n                            ),\n                        )\n\n                if part.function_call:\n                    keyword_args = part.function_call.args or {}\n                    # .. note:: Gemini API always returns None for\n                    # function_call.id, so we use thought_signature\n                    # as the unique identifier for tool\n                    # calls when available. That maybe infeasible\n                    # someday, but Gemini requires the thought_signature\n                    # for some llms like gemini-3-pro\n\n                    if part.thought_signature:\n                        call_id = base64.b64encode(\n                            part.thought_signature,\n                        ).decode(\"utf-8\")\n                    else:\n                        call_id = part.function_call.id\n\n                    tool_calls.append(\n                        ToolUseBlock(\n                            type=\"tool_use\",\n                            id=call_id,\n                            name=part.function_call.name,\n                            input=keyword_args,\n                            raw_input=json.dumps(\n                                keyword_args,\n                                ensure_ascii=False,\n                            ),\n                        ),\n                    )\n\n        # For the structured output case\n        if response.text and structured_model:\n            metadata = _json_loads_with_repair(response.text)\n\n        usage = self._extract_usage(response.usage_metadata, start_datetime)\n\n        return ChatResponse(\n            content=content_blocks + tool_calls,\n            usage=usage,\n            metadata=metadata,\n        )\n\n    def _format_tools_json_schemas(\n        self,\n        schemas: list[dict[str, Any]],\n    ) -> list[dict[str, Any]]:\n        \"\"\"Format the tools JSON schema into required format for Gemini API.\n\n        .. note:: Gemini API does not support `$defs` and `$ref` in JSON\n         schemas. This function resolves all `$ref` references by inlining the\n         referenced definitions, producing a self-contained schema without\n         any references.\n\n        Args:\n            schemas (`dict[str, Any]`):\n                The tools JSON schemas.\n\n        Returns:\n            List[Dict[str, Any]]:\n                A list containing a dictionary with the\n                \"function_declarations\" key, which maps to a list of\n                function definitions.\n\n        Example:\n            .. code-block:: python\n                :caption: Example tool schemas of Gemini API\n\n                # Input JSON schema\n                schemas = [\n                    {\n                        'type': 'function',\n                        'function': {\n                            'name': 'execute_shell_command',\n                            'description': 'xxx',\n                            'parameters': {\n                                'type': 'object',\n                                'properties': {\n                                    'command': {\n                                        'type': 'string',\n                                        'description': 'xxx.'\n                                    },\n                                    'timeout': {\n                                        'type': 'integer',\n                                        'default': 300\n                                    }\n                                },\n                                'required': ['command']\n                            }\n                        }\n                    }\n                ]\n\n                # Output format (Gemini API expected):\n                [\n                    {\n                        'function_declarations': [\n                            {\n                                'name': 'execute_shell_command',\n                                'description': 'xxx.',\n                                'parameters': {\n                                    'type': 'object',\n                                    'properties': {\n                                        'command': {\n                                            'type': 'string',\n                                            'description': 'xxx.'\n                                        },\n                                        'timeout': {\n                                            'type': 'integer',\n                                            'default': 300\n                                        }\n                                    },\n                                    'required': ['command']\n                                }\n                            }\n                        ]\n                    }\n                ]\n\n        \"\"\"\n        function_declarations = []\n        for schema in schemas:\n            if \"function\" not in schema:\n                continue\n            func = schema[\"function\"].copy()\n            # Flatten the parameters schema to resolve $ref references\n            if \"parameters\" in func:\n                func[\"parameters\"] = _flatten_json_schema(func[\"parameters\"])\n            function_declarations.append(func)\n\n        return [{\"function_declarations\": function_declarations}]\n\n    def _format_tool_choice(\n        self,\n        tool_choice: Literal[\"auto\", \"none\", \"required\"] | str | None,\n    ) -> dict | None:\n        \"\"\"Format tool_choice parameter for API compatibility.\n\n        Args:\n            tool_choice (`Literal[\"auto\", \"none\", \"required\"] | str | None`, \\\n            default `None`):\n                Controls which (if any) tool is called by the model.\n                 Can be \"auto\", \"none\", \"required\", or specific tool name.\n                 For more details, please refer to\n                 https://ai.google.dev/gemini-api/docs/function-calling?hl=en&example=meeting#function_calling_modes\n\n        Returns:\n            `dict | None`:\n                The formatted tool choice configuration dict, or None if\n                    tool_choice is None.\n        \"\"\"\n        if tool_choice is None:\n            return None\n\n        mode_mapping = {\n            \"auto\": \"AUTO\",\n            \"none\": \"NONE\",\n            \"required\": \"ANY\",\n        }\n        mode = mode_mapping.get(tool_choice)\n        if mode:\n            return {\"function_calling_config\": {\"mode\": mode}}\n        return {\n            \"function_calling_config\": {\n                \"mode\": \"ANY\",\n                \"allowed_function_names\": [tool_choice],\n            },\n        }\n"
  },
  {
    "path": "src/agentscope/model/_model_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The chat model base class.\"\"\"\n\nfrom abc import abstractmethod\nfrom typing import AsyncGenerator, Any\n\nfrom ._model_response import ChatResponse\n\n\n_TOOL_CHOICE_MODES = [\"auto\", \"none\", \"required\"]\n\n\nclass ChatModelBase:\n    \"\"\"Base class for chat models.\"\"\"\n\n    model_name: str\n    \"\"\"The model name\"\"\"\n\n    stream: bool\n    \"\"\"Is the model output streaming or not\"\"\"\n\n    def __init__(\n        self,\n        model_name: str,\n        stream: bool,\n    ) -> None:\n        \"\"\"Initialize the chat model base class.\n\n        Args:\n            model_name (`str`):\n                The name of the model\n            stream (`bool`):\n                Whether the model output is streaming or not\n        \"\"\"\n        self.model_name = model_name\n        self.stream = stream\n\n    @abstractmethod\n    async def __call__(\n        self,\n        *args: Any,\n        **kwargs: Any,\n    ) -> ChatResponse | AsyncGenerator[ChatResponse, None]:\n        pass\n\n    def _validate_tool_choice(\n        self,\n        tool_choice: str,\n        tools: list[dict] | None,\n    ) -> None:\n        \"\"\"\n        Validate tool_choice parameter.\n\n        Args:\n            tool_choice (`str`):\n                Tool choice mode or function name\n            tools (`list[dict] | None`):\n                Available tools list\n        Raises:\n            TypeError: If tool_choice is not string\n            ValueError: If tool_choice is invalid\n        \"\"\"\n        if not isinstance(tool_choice, str):\n            raise TypeError(\n                f\"tool_choice must be str, got {type(tool_choice)}\",\n            )\n        if tool_choice in _TOOL_CHOICE_MODES:\n            return\n\n        available_functions = [tool[\"function\"][\"name\"] for tool in tools]\n\n        if tool_choice not in available_functions:\n            all_options = _TOOL_CHOICE_MODES + available_functions\n            raise ValueError(\n                f\"Invalid tool_choice '{tool_choice}'. \"\n                f\"Available options: {', '.join(sorted(all_options))}\",\n            )\n"
  },
  {
    "path": "src/agentscope/model/_model_response.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The model response module.\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import Literal, Sequence\n\nfrom ._model_usage import ChatUsage\nfrom .._utils._common import _get_timestamp\nfrom .._utils._mixin import DictMixin\nfrom ..message import (\n    TextBlock,\n    ToolUseBlock,\n    ThinkingBlock,\n    AudioBlock,\n)\nfrom ..types import JSONSerializableObject\n\n\n@dataclass\nclass ChatResponse(DictMixin):\n    \"\"\"The response of chat models.\"\"\"\n\n    content: Sequence[TextBlock | ToolUseBlock | ThinkingBlock | AudioBlock]\n    \"\"\"The content of the chat response, which can include text blocks,\n    tool use blocks, or thinking blocks.\"\"\"\n\n    id: str = field(default_factory=lambda: _get_timestamp(True))\n    \"\"\"The unique identifier formatter \"\"\"\n\n    created_at: str = field(default_factory=_get_timestamp)\n    \"\"\"When the response was created\"\"\"\n\n    type: Literal[\"chat\"] = field(default_factory=lambda: \"chat\")\n    \"\"\"The type of the response, which is always 'chat'.\"\"\"\n\n    usage: ChatUsage | None = field(default_factory=lambda: None)\n    \"\"\"The usage information of the chat response, if available.\"\"\"\n\n    metadata: dict[str, JSONSerializableObject] | None = field(\n        default_factory=lambda: None,\n    )\n    \"\"\"The metadata of the chat response\"\"\"\n"
  },
  {
    "path": "src/agentscope/model/_model_usage.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The model usage class in agentscope.\"\"\"\nfrom dataclasses import dataclass, field\nfrom typing import Literal, Any\n\nfrom .._utils._mixin import DictMixin\n\n\n@dataclass\nclass ChatUsage(DictMixin):\n    \"\"\"The usage of a chat model API invocation.\"\"\"\n\n    input_tokens: int\n    \"\"\"The number of input tokens.\"\"\"\n\n    output_tokens: int\n    \"\"\"The number of output tokens.\"\"\"\n\n    time: float\n    \"\"\"The time used in seconds.\"\"\"\n\n    type: Literal[\"chat\"] = field(default_factory=lambda: \"chat\")\n    \"\"\"The type of the usage, must be `chat`.\"\"\"\n\n    metadata: dict[str, Any] | None = field(default_factory=lambda: None)\n    \"\"\"The metadata of the usage.\"\"\"\n"
  },
  {
    "path": "src/agentscope/model/_ollama_model.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Model wrapper for Ollama models.\"\"\"\nimport json\nfrom datetime import datetime\nfrom typing import (\n    Any,\n    TYPE_CHECKING,\n    List,\n    AsyncGenerator,\n    AsyncIterator,\n    Literal,\n    Type,\n)\nfrom collections import OrderedDict\n\nfrom pydantic import BaseModel\n\nfrom . import ChatResponse\nfrom ._model_base import ChatModelBase\nfrom ._model_usage import ChatUsage\nfrom .._logging import logger\nfrom .._utils._common import _json_loads_with_repair\nfrom ..message import ToolUseBlock, TextBlock, ThinkingBlock\nfrom ..tracing import trace_llm\nfrom ..types import JSONSerializableObject\n\nif TYPE_CHECKING:\n    from ollama._types import ChatResponse as OllamaChatResponse\nelse:\n    OllamaChatResponse = \"ollama._types.ChatResponse\"\n\n\nclass OllamaChatModel(ChatModelBase):\n    \"\"\"The Ollama chat model class in agentscope.\"\"\"\n\n    def __init__(\n        self,\n        model_name: str,\n        stream: bool = False,\n        options: dict = None,\n        keep_alive: str = \"5m\",\n        enable_thinking: bool | None = None,\n        host: str | None = None,\n        client_kwargs: dict[str, JSONSerializableObject] | None = None,\n        generate_kwargs: dict[str, JSONSerializableObject] | None = None,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Initialize the Ollama chat model.\n\n        Args:\n            model_name (`str`):\n                The name of the model.\n            stream (`bool`, default `True`):\n                Streaming mode or not.\n            options (`dict`, default `None`):\n                Additional parameters to pass to the Ollama API. These can\n                include temperature etc.\n            keep_alive (`str`, default `\"5m\"`):\n                Duration to keep the model loaded in memory. The format is a\n                number followed by a unit suffix (s for seconds, m for minutes\n                , h for hours).\n            enable_thinking (`bool | None`, default `None`)\n                Whether enable thinking or not, only for models such as qwen3,\n                deepseek-r1, etc. For more details, please refer to\n                https://ollama.com/search?c=thinking\n            host (`str | None`, default `None`):\n                The host address of the Ollama server. If None, uses the\n                default address (typically http://localhost:11434).\n            client_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n             optional):\n                The extra keyword arguments to initialize the Ollama client.\n            generate_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n             optional):\n                The extra keyword arguments used in Ollama API generation.\n            **kwargs (`Any`):\n                Additional keyword arguments to pass to the base chat model\n                class.\n        \"\"\"\n\n        try:\n            import ollama\n        except ImportError as e:\n            raise ImportError(\n                \"The package ollama is not found. Please install it by \"\n                'running command `pip install \"ollama>=0.1.7\"`',\n            ) from e\n\n        super().__init__(model_name, stream)\n\n        self.client = ollama.AsyncClient(\n            host=host,\n            **(client_kwargs or {}),\n            **kwargs,\n        )\n        self.options = options\n        self.keep_alive = keep_alive\n        self.think = enable_thinking\n        self.generate_kwargs = generate_kwargs or {}\n\n    @trace_llm\n    async def __call__(\n        self,\n        messages: list[dict[str, Any]],\n        tools: list[dict] | None = None,\n        tool_choice: Literal[\"auto\", \"none\", \"required\"] | str | None = None,\n        structured_model: Type[BaseModel] | None = None,\n        **kwargs: Any,\n    ) -> ChatResponse | AsyncGenerator[ChatResponse, None]:\n        \"\"\"Get the response from Ollama chat completions API by the given\n        arguments.\n\n        Args:\n            messages (`list[dict]`):\n                A list of dictionaries, where `role` and `content` fields are\n                required, and `name` field is optional.\n            tools (`list[dict]`, default `None`):\n                The tools JSON schemas that the model can use.\n            tool_choice (`Literal[\"auto\", \"none\", \"required\"] | str \\\n                | None`, default `None`):\n                Ollama doesn't support `tool_choice` argument yet.\n            structured_model (`Type[BaseModel] | None`, default `None`):\n                A Pydantic BaseModel class that defines the expected structure\n                for the model's output.\n            **kwargs (`Any`):\n                The keyword arguments for Ollama chat completions API,\n                e.g. `think`etc. Please refer to the Ollama API\n                documentation for more details.\n\n        Returns:\n            `ChatResponse | AsyncGenerator[ChatResponse, None]`:\n                The response from the Ollama chat completions API.\n        \"\"\"\n\n        kwargs = {\n            \"model\": self.model_name,\n            \"messages\": messages,\n            \"stream\": self.stream,\n            \"options\": self.options,\n            \"keep_alive\": self.keep_alive,\n            **self.generate_kwargs,\n            **kwargs,\n        }\n\n        if self.think is not None and \"think\" not in kwargs:\n            kwargs[\"think\"] = self.think\n\n        if tools:\n            kwargs[\"tools\"] = self._format_tools_json_schemas(tools)\n\n        if tool_choice:\n            logger.warning(\"Ollama does not support tool_choice yet, ignored.\")\n\n        if structured_model:\n            kwargs[\"format\"] = structured_model.model_json_schema()\n\n        start_datetime = datetime.now()\n        response = await self.client.chat(**kwargs)\n\n        if self.stream:\n            return self._parse_ollama_stream_completion_response(\n                start_datetime,\n                response,\n                structured_model,\n            )\n\n        parsed_response = await self._parse_ollama_completion_response(\n            start_datetime,\n            response,\n            structured_model,\n        )\n\n        return parsed_response\n\n    async def _parse_ollama_stream_completion_response(\n        self,\n        start_datetime: datetime,\n        response: AsyncIterator[OllamaChatResponse],\n        structured_model: Type[BaseModel] | None = None,\n    ) -> AsyncGenerator[ChatResponse, None]:\n        \"\"\"Given an Ollama streaming completion response, extract the\n        content blocks and usages from it and yield ChatResponse objects.\n\n        Args:\n            start_datetime (`datetime`):\n                The start datetime of the response generation.\n            response (`AsyncIterator[OllamaChatResponse]`):\n                Ollama streaming response async iterator to parse.\n            structured_model (`Type[BaseModel] | None`, default `None`):\n                A Pydantic BaseModel class that defines the expected structure\n                for the model's output.\n\n        Returns:\n            AsyncGenerator[ChatResponse, None]:\n                An async generator that yields ChatResponse objects containing\n                the content blocks and usage information for each chunk in the\n                streaming response.\n\n        .. note::\n            If `structured_model` is not `None`, the expected structured output\n            will be stored in the metadata of the `ChatResponse`.\n\n        \"\"\"\n        accumulated_text = \"\"\n        acc_thinking_content = \"\"\n        tool_calls = OrderedDict()  # Store tool calls\n        metadata: dict | None = None\n\n        async for chunk in response:\n            # Handle text content\n            msg = chunk.message\n            acc_thinking_content += msg.thinking or \"\"\n            accumulated_text += msg.content or \"\"\n\n            # Handle tool calls\n            for idx, tool_call in enumerate(msg.tool_calls or []):\n                function = tool_call.function\n                tool_id = f\"{idx}_{function.name}\"\n                tool_calls[tool_id] = {\n                    \"type\": \"tool_use\",\n                    \"id\": tool_id,\n                    \"name\": function.name,\n                    \"input\": function.arguments,\n                    \"raw_input\": json.dumps(function.arguments),\n                }\n            # Calculate usage statistics\n            current_time = (datetime.now() - start_datetime).total_seconds()\n            usage = ChatUsage(\n                input_tokens=getattr(chunk, \"prompt_eval_count\", 0) or 0,\n                output_tokens=getattr(chunk, \"eval_count\", 0) or 0,\n                time=current_time,\n            )\n            # Create content blocks\n            contents: list = []\n\n            if acc_thinking_content:\n                contents.append(\n                    ThinkingBlock(\n                        type=\"thinking\",\n                        thinking=acc_thinking_content,\n                    ),\n                )\n\n            if accumulated_text:\n                contents.append(TextBlock(type=\"text\", text=accumulated_text))\n                if structured_model:\n                    metadata = _json_loads_with_repair(accumulated_text)\n\n            # Add tool call blocks\n            for tool_call in tool_calls.values():\n                try:\n                    input_data = tool_call[\"input\"]\n                    if isinstance(input_data, str):\n                        input_data = _json_loads_with_repair(input_data)\n                    contents.append(\n                        ToolUseBlock(\n                            type=tool_call[\"type\"],\n                            id=tool_call[\"id\"],\n                            name=tool_call[\"name\"],\n                            input=input_data,\n                        ),\n                    )\n                except Exception as e:\n                    print(f\"Error parsing tool call input: {e}\")\n\n            # Generate response when there's new content or at final chunk\n            if chunk.done or contents:\n                res = ChatResponse(\n                    content=contents,\n                    usage=usage,\n                    metadata=metadata,\n                )\n                yield res\n\n    async def _parse_ollama_completion_response(\n        self,\n        start_datetime: datetime,\n        response: OllamaChatResponse,\n        structured_model: Type[BaseModel] | None = None,\n    ) -> ChatResponse:\n        \"\"\"Given an Ollama chat completion response object, extract the content\n        blocks and usages from it.\n\n        Args:\n            start_datetime (`datetime`):\n                The start datetime of the response generation.\n            response (`OllamaChatResponse`):\n                Ollama OllamaChatResponse object to parse.\n            structured_model (`Type[BaseModel] | None`, default `None`):\n                A Pydantic BaseModel class that defines the expected structure\n                for the model's output.\n\n        Returns:\n            `ChatResponse`:\n                A ChatResponse object containing the content blocks and usage.\n\n        .. note::\n            If `structured_model` is not `None`, the expected structured output\n            will be stored in the metadata of the `ChatResponse`.\n        \"\"\"\n        content_blocks: List[TextBlock | ToolUseBlock | ThinkingBlock] = []\n        metadata: dict | None = None\n\n        if response.message.thinking:\n            content_blocks.append(\n                ThinkingBlock(\n                    type=\"thinking\",\n                    thinking=response.message.thinking,\n                ),\n            )\n\n        if response.message.content:\n            content_blocks.append(\n                TextBlock(\n                    type=\"text\",\n                    text=response.message.content,\n                ),\n            )\n            if structured_model:\n                metadata = _json_loads_with_repair(\n                    response.message.content,\n                )\n\n        for idx, tool_call in enumerate(response.message.tool_calls or []):\n            content_blocks.append(\n                ToolUseBlock(\n                    type=\"tool_use\",\n                    id=f\"{idx}_{tool_call.function.name}\",\n                    name=tool_call.function.name,\n                    input=tool_call.function.arguments,\n                    raw_input=json.dumps(tool_call.function.arguments),\n                ),\n            )\n\n        usage = None\n        if \"prompt_eval_count\" in response and \"eval_count\" in response:\n            usage = ChatUsage(\n                input_tokens=response.get(\"prompt_eval_count\", 0),\n                output_tokens=response.get(\"eval_count\", 0),\n                time=(datetime.now() - start_datetime).total_seconds(),\n            )\n\n        parsed_response = ChatResponse(\n            content=content_blocks,\n            usage=usage,\n            metadata=metadata,\n        )\n\n        return parsed_response\n\n    def _format_tools_json_schemas(\n        self,\n        schemas: list[dict[str, Any]],\n    ) -> list[dict[str, Any]]:\n        \"\"\"Format the tools JSON schemas to the Ollama format.\"\"\"\n        return schemas\n"
  },
  {
    "path": "src/agentscope/model/_openai_model.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=too-many-branches\n\"\"\"OpenAI Chat model class.\"\"\"\nimport copy\nimport json\nimport warnings\nfrom datetime import datetime\nfrom typing import (\n    Any,\n    TYPE_CHECKING,\n    List,\n    AsyncGenerator,\n    Literal,\n    Type,\n)\nfrom collections import OrderedDict\n\nfrom pydantic import BaseModel\n\nfrom . import ChatResponse\nfrom ._model_base import ChatModelBase\nfrom ._model_usage import ChatUsage\nfrom .._logging import logger\nfrom .._utils._common import _json_loads_with_repair\nfrom ..message import (\n    ToolUseBlock,\n    TextBlock,\n    ThinkingBlock,\n    AudioBlock,\n    Base64Source,\n)\nfrom ..tracing import trace_llm\nfrom ..types import JSONSerializableObject\n\nif TYPE_CHECKING:\n    from openai.types.chat import ChatCompletion\n    from openai import AsyncStream\nelse:\n    ChatCompletion = \"openai.types.chat.ChatCompletion\"\n    AsyncStream = \"openai.types.chat.AsyncStream\"\n\n\ndef _format_audio_data_for_qwen_omni(messages: list[dict]) -> None:\n    \"\"\"Qwen-omni uses OpenAI-compatible API but requires different audio\n    data format than OpenAI with \"data:;base64,\" prefix.\n    Refer to `Qwen-omni documentation\n    <https://bailian.console.aliyun.com/?tab=doc#/doc/?type=model&url=2867839>`_\n    for more details.\n\n    Args:\n        messages (`list[dict]`):\n            The list of message dictionaries from OpenAI formatter.\n    \"\"\"\n    for msg in messages:\n        if isinstance(msg.get(\"content\"), list):\n            for block in msg[\"content\"]:\n                if (\n                    isinstance(block, dict)\n                    and \"input_audio\" in block\n                    and isinstance(block[\"input_audio\"].get(\"data\"), str)\n                ):\n                    if not block[\"input_audio\"][\"data\"].startswith(\"http\"):\n                        block[\"input_audio\"][\"data\"] = (\n                            \"data:;base64,\" + block[\"input_audio\"][\"data\"]\n                        )\n\n\nclass OpenAIChatModel(ChatModelBase):\n    \"\"\"The OpenAI chat model class.\"\"\"\n\n    def __init__(\n        self,\n        model_name: str,\n        api_key: str | None = None,\n        stream: bool = True,\n        reasoning_effort: Literal[\"low\", \"medium\", \"high\"] | None = None,\n        organization: str = None,\n        stream_tool_parsing: bool = True,\n        client_type: Literal[\"openai\", \"azure\"] = \"openai\",\n        client_kwargs: dict[str, JSONSerializableObject] | None = None,\n        generate_kwargs: dict[str, JSONSerializableObject] | None = None,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Initialize the openai client.\n\n        Args:\n            model_name (`str`, default `None`):\n                The name of the model to use in OpenAI API.\n            api_key (`str`, default `None`):\n                The API key for OpenAI API. If not specified, it will\n                be read from the environment variable `OPENAI_API_KEY`.\n            stream (`bool`, default `True`):\n                Whether to use streaming output or not.\n            reasoning_effort (`Literal[\"low\", \"medium\", \"high\"] | None`, \\\n            optional):\n                Reasoning effort, supported for o3, o4, etc. Please refer to\n                `OpenAI documentation\n                <https://platform.openai.com/docs/guides/reasoning?api-mode=chat>`_\n                for more details.\n            organization (`str`, default `None`):\n                The organization ID for OpenAI API. If not specified, it will\n                be read from the environment variable `OPENAI_ORGANIZATION`.\n            stream_tool_parsing (`bool`, default to `True`):\n                Whether to parse incomplete tool use JSON during streaming\n                with auto-repair. If True, partial JSON (e.g., `'{\"a\": \"x'`)\n                is repaired to valid dicts ({\"a\": \"x\"}) in real-time for\n                immediate tool function input. Otherwise, the input field\n                remains {} until the final chunk arrives.\n            client_type (`Literal[\"openai\", \"azure\"]`, default `openai`):\n                Selects which OpenAI-compatible client to initialize.\n            client_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n             optional):\n                The extra keyword arguments to initialize the OpenAI client.\n            generate_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n             optional):\n                The extra keyword arguments used in OpenAI API generation,\n                e.g. `temperature`, `seed`.\n            **kwargs (`Any`):\n                Additional keyword arguments.\n        \"\"\"\n\n        # Handle deprecated client_args parameter from kwargs\n        client_args = kwargs.pop(\"client_args\", None)\n        if client_args is not None and client_kwargs is not None:\n            raise ValueError(\n                \"Cannot specify both 'client_args' and 'client_kwargs'. \"\n                \"Please use only 'client_kwargs' (client_args is deprecated).\",\n            )\n\n        if client_args is not None:\n            logger.warning(\n                \"The parameter 'client_args' is deprecated and will be \"\n                \"removed in a future version. Please use 'client_kwargs' \"\n                \"instead. Automatically converting 'client_args' to \"\n                \"'client_kwargs'.\",\n            )\n            client_kwargs = client_args\n\n        if kwargs:\n            logger.warning(\n                \"Unknown keyword arguments: %s. These will be ignored.\",\n                list(kwargs.keys()),\n            )\n\n        super().__init__(model_name, stream)\n\n        import openai\n\n        if client_type not in (\"openai\", \"azure\"):\n            raise ValueError(\n                \"Invalid client_type. Supported values: 'openai', 'azure'.\",\n            )\n\n        if client_type == \"azure\":\n            self.client = openai.AsyncAzureOpenAI(\n                api_key=api_key,\n                organization=organization,\n                **(client_kwargs or {}),\n            )\n        else:\n            self.client = openai.AsyncClient(\n                api_key=api_key,\n                organization=organization,\n                **(client_kwargs or {}),\n            )\n\n        self.reasoning_effort = reasoning_effort\n        self.stream_tool_parsing = stream_tool_parsing\n        self.generate_kwargs = generate_kwargs or {}\n\n    @trace_llm\n    async def __call__(\n        self,\n        messages: list[dict],\n        tools: list[dict] | None = None,\n        tool_choice: Literal[\"auto\", \"none\", \"required\"] | str | None = None,\n        structured_model: Type[BaseModel] | None = None,\n        **kwargs: Any,\n    ) -> ChatResponse | AsyncGenerator[ChatResponse, None]:\n        \"\"\"Get the response from OpenAI chat completions API by the given\n        arguments.\n\n        Args:\n            messages (`list[dict]`):\n                A list of dictionaries, where `role` and `content` fields are\n                required, and `name` field is optional.\n            tools (`list[dict]`, default `None`):\n                The tools JSON schemas that the model can use.\n            tool_choice (`Literal[\"auto\", \"none\", \"required\"] | str \\\n            | None`, default `None`):\n                Controls which (if any) tool is called by the model.\n                 Can be \"auto\", \"none\", \"required\", or specific tool\n                 name. For more details, please refer to\n                 https://platform.openai.com/docs/api-reference/responses/create#responses_create-tool_choice\n            structured_model (`Type[BaseModel] | None`, default `None`):\n                A Pydantic BaseModel class that defines the expected structure\n                for the model's output. When provided, the model will be forced\n                to return data that conforms to this schema by automatically\n                converting the BaseModel to a tool function and setting\n                `tool_choice` to enforce its usage. This enables structured\n                output generation.\n\n                .. note:: When `structured_model` is specified,\n                    both `tools` and `tool_choice` parameters are ignored,\n                    and the model will only perform structured output\n                    generation without calling any other tools.\n\n                For more details, please refer to the `official document\n                <https://platform.openai.com/docs/guides/structured-outputs>`_\n\n            **kwargs (`Any`):\n                The keyword arguments for OpenAI chat completions API,\n                e.g. `temperature`, `max_tokens`, `top_p`, etc. Please\n                refer to the OpenAI API documentation for more details.\n\n        Returns:\n            `ChatResponse | AsyncGenerator[ChatResponse, None]`:\n                The response from the OpenAI chat completions API.\n        \"\"\"\n\n        # checking messages\n        if not isinstance(messages, list):\n            raise ValueError(\n                \"OpenAI `messages` field expected type `list`, \"\n                f\"got `{type(messages)}` instead.\",\n            )\n        if not all(\"role\" in msg and \"content\" in msg for msg in messages):\n            raise ValueError(\n                \"Each message in the 'messages' list must contain a 'role' \"\n                \"and 'content' key for OpenAI API.\",\n            )\n\n        # Qwen-omni requires different base64 audio format from openai\n        if \"omni\" in self.model_name.lower():\n            _format_audio_data_for_qwen_omni(messages)\n\n        kwargs = {\n            \"model\": self.model_name,\n            \"messages\": messages,\n            \"stream\": self.stream,\n            **self.generate_kwargs,\n            **kwargs,\n        }\n        if self.reasoning_effort and \"reasoning_effort\" not in kwargs:\n            kwargs[\"reasoning_effort\"] = self.reasoning_effort\n\n        if tools:\n            kwargs[\"tools\"] = self._format_tools_json_schemas(tools)\n\n        if tool_choice:\n            # Handle deprecated \"any\" option with warning\n            if tool_choice == \"any\":\n                warnings.warn(\n                    '\"any\" is deprecated and will be removed in a future '\n                    \"version.\",\n                    DeprecationWarning,\n                )\n                tool_choice = \"required\"\n            self._validate_tool_choice(tool_choice, tools)\n            kwargs[\"tool_choice\"] = self._format_tool_choice(tool_choice)\n\n        if self.stream:\n            kwargs[\"stream_options\"] = {\"include_usage\": True}\n\n        start_datetime = datetime.now()\n\n        if structured_model:\n            if tools or tool_choice:\n                logger.warning(\n                    \"structured_model is provided. Both 'tools' and \"\n                    \"'tool_choice' parameters will be overridden and \"\n                    \"ignored. The model will only perform structured output \"\n                    \"generation without calling any other tools.\",\n                )\n            kwargs.pop(\"stream\", None)\n            kwargs.pop(\"tools\", None)\n            kwargs.pop(\"tool_choice\", None)\n            kwargs[\"response_format\"] = structured_model\n            if not self.stream:\n                response = await self.client.chat.completions.parse(**kwargs)\n            else:\n                response = self.client.chat.completions.stream(**kwargs)\n                return self._parse_openai_stream_response(\n                    start_datetime,\n                    response,\n                    structured_model,\n                )\n        else:\n            response = await self.client.chat.completions.create(**kwargs)\n\n        if self.stream:\n            return self._parse_openai_stream_response(\n                start_datetime,\n                response,\n                structured_model,\n            )\n\n        # Non-streaming response\n        parsed_response = self._parse_openai_completion_response(\n            start_datetime,\n            response,\n            structured_model,\n        )\n\n        return parsed_response\n\n    # pylint: disable=too-many-statements\n    async def _parse_openai_stream_response(\n        self,\n        start_datetime: datetime,\n        response: AsyncStream,\n        structured_model: Type[BaseModel] | None = None,\n    ) -> AsyncGenerator[ChatResponse, None]:\n        \"\"\"Given an OpenAI streaming completion response, extract the content\n         blocks and usages from it and yield ChatResponse objects.\n\n        Args:\n            start_datetime (`datetime`):\n                The start datetime of the response generation.\n            response (`AsyncStream`):\n                OpenAI AsyncStream object to parse.\n            structured_model (`Type[BaseModel] | None`, default `None`):\n                A Pydantic BaseModel class that defines the expected structure\n                for the model's output.\n\n        Returns:\n            `AsyncGenerator[ChatResponse, None]`:\n                An async generator that yields ChatResponse objects containing\n                the content blocks and usage information for each chunk in\n                the streaming response.\n\n        .. note::\n            If `structured_model` is not `None`, the expected structured output\n            will be stored in the metadata of the `ChatResponse`.\n        \"\"\"\n        usage, res = None, None\n        text = \"\"\n        thinking = \"\"\n        audio = \"\"\n        tool_calls = OrderedDict()\n        last_input_objs = {}  # Store last input_obj for each tool_call\n        metadata: dict | None = None\n        contents: List[\n            TextBlock | ToolUseBlock | ThinkingBlock | AudioBlock\n        ] = []\n        last_contents = None\n\n        async with response as stream:\n            async for item in stream:\n                if structured_model:\n                    if item.type != \"chunk\":\n                        continue\n                    chunk = item.chunk\n                else:\n                    chunk = item\n\n                if chunk.usage:\n                    usage = ChatUsage(\n                        input_tokens=chunk.usage.prompt_tokens,\n                        output_tokens=chunk.usage.completion_tokens,\n                        time=(datetime.now() - start_datetime).total_seconds(),\n                        metadata=chunk.usage,\n                    )\n\n                if not chunk.choices:\n                    if usage and contents:\n                        res = ChatResponse(\n                            content=contents,\n                            usage=usage,\n                            metadata=metadata,\n                        )\n                        yield res\n                    continue\n\n                choice = chunk.choices[0]\n\n                delta_reasoning = getattr(\n                    choice.delta,\n                    \"reasoning_content\",\n                    None,\n                )\n                if not isinstance(delta_reasoning, str):\n                    delta_reasoning = getattr(choice.delta, \"reasoning\", None)\n                if not isinstance(delta_reasoning, str):\n                    delta_reasoning = \"\"\n\n                thinking += delta_reasoning\n                text += getattr(choice.delta, \"content\", None) or \"\"\n\n                if (\n                    hasattr(choice.delta, \"audio\")\n                    and \"data\" in choice.delta.audio\n                ):\n                    audio += choice.delta.audio[\"data\"]\n                if (\n                    hasattr(choice.delta, \"audio\")\n                    and \"transcript\" in choice.delta.audio\n                ):\n                    text += choice.delta.audio[\"transcript\"]\n\n                for tool_call in (\n                    getattr(choice.delta, \"tool_calls\", None) or []\n                ):\n                    if tool_call.index in tool_calls:\n                        if tool_call.function.arguments is not None:\n                            tool_calls[tool_call.index][\n                                \"input\"\n                            ] += tool_call.function.arguments\n\n                    else:\n                        tool_calls[tool_call.index] = {\n                            \"type\": \"tool_use\",\n                            \"id\": tool_call.id,\n                            \"name\": tool_call.function.name,\n                            \"input\": tool_call.function.arguments or \"\",\n                        }\n\n                contents = []\n\n                if thinking:\n                    contents.append(\n                        ThinkingBlock(\n                            type=\"thinking\",\n                            thinking=thinking,\n                        ),\n                    )\n\n                if audio:\n                    media_type = self.generate_kwargs.get(\"audio\", {}).get(\n                        \"format\",\n                        \"wav\",\n                    )\n                    contents.append(\n                        AudioBlock(\n                            type=\"audio\",\n                            source=Base64Source(\n                                data=audio,\n                                media_type=f\"audio/{media_type}\",\n                                type=\"base64\",\n                            ),\n                        ),\n                    )\n\n                if text:\n                    contents.append(\n                        TextBlock(\n                            type=\"text\",\n                            text=text,\n                        ),\n                    )\n\n                    if structured_model:\n                        metadata = _json_loads_with_repair(text)\n\n                for tool_call in tool_calls.values():\n                    input_str = tool_call[\"input\"]\n                    tool_id = tool_call[\"id\"]\n\n                    # If parsing the tool input in streaming mode\n                    if self.stream_tool_parsing:\n                        repaired_input = _json_loads_with_repair(\n                            input_str or \"{}\",\n                        )\n                        # If the new repaired input is shorter than one in the\n                        # last chunk, use the last one to avoid regression\n                        last_input = last_input_objs.get(tool_id, {})\n                        if len(json.dumps(last_input)) > len(\n                            json.dumps(repaired_input),\n                        ):\n                            repaired_input = last_input\n                        last_input_objs[tool_id] = repaired_input\n\n                    else:\n                        # Otherwise, keep input as empty dict until the final\n                        # chunk\n                        repaired_input = {}\n\n                    contents.append(\n                        ToolUseBlock(\n                            type=tool_call[\"type\"],\n                            id=tool_id,\n                            name=tool_call[\"name\"],\n                            input=repaired_input,\n                            raw_input=input_str,\n                        ),\n                    )\n\n                if contents:\n                    res = ChatResponse(\n                        content=contents,\n                        usage=usage,\n                        metadata=metadata,\n                    )\n                    yield res\n                    last_contents = copy.deepcopy(contents)\n\n        # If stream_tool_parsing is False, yield last contents\n        if not self.stream_tool_parsing and tool_calls and last_contents:\n            metadata = None\n            # Update tool use blocks in last_contents inplace\n            for block in last_contents:\n                if block.get(\"type\") == \"tool_use\":\n                    block[\"input\"] = input_obj = _json_loads_with_repair(\n                        str(block.get(\"raw_input\") or \"{}\"),\n                    )\n\n                    if structured_model:\n                        metadata = input_obj\n\n            yield ChatResponse(\n                content=last_contents,\n                usage=usage,\n                metadata=metadata,\n            )\n\n    def _parse_openai_completion_response(\n        self,\n        start_datetime: datetime,\n        response: ChatCompletion,\n        structured_model: Type[BaseModel] | None = None,\n    ) -> ChatResponse:\n        \"\"\"Given an OpenAI chat completion response object, extract the content\n            blocks and usages from it.\n\n        Args:\n            start_datetime (`datetime`):\n                The start datetime of the response generation.\n            response (`ChatCompletion`):\n                OpenAI ChatCompletion object to parse.\n            structured_model (`Type[BaseModel] | None`, default `None`):\n                A Pydantic BaseModel class that defines the expected structure\n                for the model's output.\n\n        Returns:\n            ChatResponse (`ChatResponse`):\n                A ChatResponse object containing the content blocks and usage.\n\n        .. note::\n            If `structured_model` is not `None`, the expected structured output\n            will be stored in the metadata of the `ChatResponse`.\n        \"\"\"\n        content_blocks: List[\n            TextBlock | ToolUseBlock | ThinkingBlock | AudioBlock\n        ] = []\n        metadata: dict | None = None\n\n        if response.choices:\n            choice = response.choices[0]\n            reasoning = getattr(choice.message, \"reasoning_content\", None)\n            if not isinstance(reasoning, str):\n                reasoning = getattr(choice.message, \"reasoning\", None)\n            if not isinstance(reasoning, str):\n                reasoning = None\n\n            if reasoning is not None:\n                content_blocks.append(\n                    ThinkingBlock(\n                        type=\"thinking\",\n                        thinking=reasoning,\n                    ),\n                )\n\n            if choice.message.content:\n                content_blocks.append(\n                    TextBlock(\n                        type=\"text\",\n                        text=response.choices[0].message.content,\n                    ),\n                )\n            if choice.message.audio:\n                media_type = self.generate_kwargs.get(\"audio\", {}).get(\n                    \"format\",\n                    \"mp3\",\n                )\n                content_blocks.append(\n                    AudioBlock(\n                        type=\"audio\",\n                        source=Base64Source(\n                            data=choice.message.audio.data,\n                            media_type=f\"audio/{media_type}\",\n                            type=\"base64\",\n                        ),\n                    ),\n                )\n\n                if choice.message.audio.transcript:\n                    content_blocks.append(\n                        TextBlock(\n                            type=\"text\",\n                            text=choice.message.audio.transcript,\n                        ),\n                    )\n\n            for tool_call in choice.message.tool_calls or []:\n                content_blocks.append(\n                    ToolUseBlock(\n                        type=\"tool_use\",\n                        id=tool_call.id,\n                        name=tool_call.function.name,\n                        input=_json_loads_with_repair(\n                            tool_call.function.arguments,\n                        ),\n                    ),\n                )\n\n            if structured_model:\n                metadata = choice.message.parsed.model_dump()\n\n        usage = None\n        if response.usage:\n            usage = ChatUsage(\n                input_tokens=response.usage.prompt_tokens,\n                output_tokens=response.usage.completion_tokens,\n                time=(datetime.now() - start_datetime).total_seconds(),\n                metadata=response.usage,\n            )\n\n        parsed_response = ChatResponse(\n            content=content_blocks,\n            usage=usage,\n            metadata=metadata,\n        )\n\n        return parsed_response\n\n    def _format_tools_json_schemas(\n        self,\n        schemas: list[dict[str, Any]],\n    ) -> list[dict[str, Any]]:\n        \"\"\"Format the tools JSON schemas to the OpenAI format.\"\"\"\n        return schemas\n\n    def _format_tool_choice(\n        self,\n        tool_choice: Literal[\"auto\", \"none\", \"required\"] | str | None,\n    ) -> str | dict | None:\n        \"\"\"Format tool_choice parameter for API compatibility.\n\n        Args:\n            tool_choice (`Literal[\"auto\", \"none\", \"required\"] | str \\\n            | None`, default `None`):\n                Controls which (if any) tool is called by the model.\n                 Can be \"auto\", \"none\", \"required\", or specific tool name.\n                 For more details, please refer to\n                 https://platform.openai.com/docs/api-reference/responses/create#responses_create-tool_choice\n        Returns:\n            `dict | None`:\n                The formatted tool choice configuration dict, or None if\n                    tool_choice is None.\n        \"\"\"\n        if tool_choice is None:\n            return None\n\n        mode_mapping = {\n            \"auto\": \"auto\",\n            \"none\": \"none\",\n            \"required\": \"required\",\n        }\n        if tool_choice in mode_mapping:\n            return mode_mapping[tool_choice]\n        return {\"type\": \"function\", \"function\": {\"name\": tool_choice}}\n"
  },
  {
    "path": "src/agentscope/model/_trinity_model.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"A model class for RL Training with Trinity-RFT.\"\"\"\nfrom typing import (\n    Optional,\n    TYPE_CHECKING,\n)\nfrom typing_extensions import deprecated\nfrom ._openai_model import OpenAIChatModel\nfrom ..types import JSONSerializableObject\n\n\nif TYPE_CHECKING:\n    from openai import AsyncOpenAI\nelse:\n    AsyncOpenAI = \"openai.AsyncOpenAI\"\n\n\n@deprecated(\n    \"TrinityChatModel is deprecated. Please use OpenAIChatModel directly.\",\n)\nclass TrinityChatModel(OpenAIChatModel):\n    \"\"\"A model class for RL Training with Trinity-RFT.\"\"\"\n\n    def __init__(\n        self,\n        openai_async_client: AsyncOpenAI,\n        generate_kwargs: dict[str, JSONSerializableObject] | None = None,\n        enable_thinking: Optional[bool] = None,\n    ) -> None:\n        \"\"\"Initialize the Trinity model class.\n\n        Args:\n            openai_async_client (`AsyncOpenAI`):\n                The OpenAI async client instance provided by Trinity-RFT.\n            generate_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n            optional):\n                Additional keyword arguments to pass to the model's generate\n                method. Defaults to None.\n            enable_thinking (`bool`, optional):\n                Whether to enable the model's thinking capability. Only\n                applicable for Qwen3 series models. Defaults to None.\n        \"\"\"\n        model_name = getattr(openai_async_client, \"model_path\", None)\n        if model_name is None:\n            raise ValueError(\n                \"The provided openai_async_client does not have a \"\n                \"`model_path` attribute. Please ensure you are using \"\n                \"the instance provided by Trinity-RFT.\",\n            )\n        super().__init__(\n            model_name=model_name,\n            api_key=\"EMPTY\",\n            generate_kwargs=generate_kwargs,\n            stream=False,  # RL training does not support streaming\n        )\n        if enable_thinking is not None:\n            if \"chat_template_kwargs\" not in self.generate_kwargs:\n                self.generate_kwargs[\"chat_template_kwargs\"] = {}\n            assert isinstance(\n                self.generate_kwargs[\"chat_template_kwargs\"],\n                dict,\n            ), \"chat_template_kwargs must be a dictionary.\"\n            self.generate_kwargs[\"chat_template_kwargs\"][\n                \"enable_thinking\"\n            ] = enable_thinking\n        # change the client instance to the provided one\n        self.client = openai_async_client\n"
  },
  {
    "path": "src/agentscope/module/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The module in agentscope.\"\"\"\n\nfrom ._state_module import StateModule\n\n__all__ = [\n    \"StateModule\",\n]\n"
  },
  {
    "path": "src/agentscope/module/_state_module.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The state module in agentscope.\"\"\"\n\nimport json\nfrom collections import OrderedDict\nfrom dataclasses import dataclass\nfrom typing import Callable, Any, Optional\n\nfrom ..types import JSONSerializableObject\n\n\n@dataclass\nclass _JSONSerializeFunction:\n    to_json: Optional[Callable[[Any], Any]] = None\n    \"\"\"The function converting the original data to JSON data.\"\"\"\n    load_json: Optional[Callable[[Any], Any]] = None\n    \"\"\"The function converting the JSON data to original data.\"\"\"\n\n\nclass StateModule:\n    \"\"\"The state module class in agentscope to support nested state\n    serialization and deserialization.\"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"Initialize the state module.\"\"\"\n        self._module_dict = OrderedDict()\n        self._attribute_dict = OrderedDict()\n\n    def __setattr__(self, key: str, value: Any) -> None:\n        \"\"\"Set attributes and record state modules.\"\"\"\n        if isinstance(value, StateModule):\n            if not hasattr(self, \"_module_dict\"):\n                raise AttributeError(\n                    f\"Call the super().__init__() method within the \"\n                    f\"constructor of {self.__class__.__name__} before setting \"\n                    f\"any attributes.\",\n                )\n            self._module_dict[key] = value\n        super().__setattr__(key, value)\n\n    def __delattr__(self, key: str) -> None:\n        \"\"\"Delete attributes and remove from state modules.\"\"\"\n        if key in self._module_dict:\n            self._module_dict.pop(key)\n        if key in self._attribute_dict:\n            self._attribute_dict.pop(key)\n        super().__delattr__(key)\n\n    def state_dict(self) -> dict:\n        \"\"\"Get the state dictionary of the module, including the nested\n        state modules and registered attributes.\n\n        Returns:\n            `dict`:\n                A dictionary that keys are attribute names and values are\n                the state of the attribute.\n        \"\"\"\n        state = {}\n        for key in self._module_dict:\n            attr = getattr(self, key, None)\n            if isinstance(attr, StateModule):\n                state[key] = attr.state_dict()\n\n        for key in self._attribute_dict:\n            attr = getattr(self, key)\n            to_json_function = self._attribute_dict[key].to_json\n            if to_json_function is not None:\n                state[key] = to_json_function(attr)\n            else:\n                state[key] = attr\n\n        return state\n\n    def load_state_dict(self, state_dict: dict, strict: bool = True) -> None:\n        \"\"\"Load the state dictionary into the module.\n\n        Args:\n            state_dict (`dict`):\n                The state dictionary to load.\n            strict (`bool`, defaults to `True`):\n                If `True`, raises an error if any key in the module is not\n                found in the state_dict. If `False`, skips missing keys.\n        \"\"\"\n        for key in self._module_dict:\n            if key not in state_dict:\n                if strict:\n                    raise KeyError(\n                        f\"Key '{key}' not found in state_dict. Ensure that \"\n                        f\"the state_dict contains all required keys.\",\n                    )\n                continue\n            self._module_dict[key].load_state_dict(state_dict[key])\n\n        for key in self._attribute_dict:\n            if key not in state_dict:\n                if strict:\n                    raise KeyError(\n                        f\"Key '{key}' not found in state_dict. Ensure that \"\n                        f\"the state_dict contains all required keys.\",\n                    )\n                continue\n            from_json_func = self._attribute_dict[key].load_json\n            if from_json_func is not None:\n                setattr(self, key, from_json_func(state_dict[key]))\n            else:\n                setattr(self, key, state_dict[key])\n\n    def register_state(\n        self,\n        attr_name: str,\n        custom_to_json: Callable[[Any], JSONSerializableObject] | None = None,\n        custom_from_json: Callable[[JSONSerializableObject], Any]\n        | None = None,\n    ) -> None:\n        \"\"\"Register an attribute to be tracked as a state variable.\n\n        Args:\n            attr_name (`str`):\n                The name of the attribute to register.\n            custom_to_json (`Callable[[Any], JSONSerializableObject] | None`, \\\n            optional):\n                A custom function to convert the attribute to a\n                JSON-serializable format. If not provided, `json.dumps` will\n                be used.\n            custom_from_json (`Callable[[JSONSerializableObject], Any] | None`\\\n            , defaults to `None`):\n                A custom function to convert the JSON dictionary back to the\n                original attribute format.\n        \"\"\"\n        attr = getattr(self, attr_name)\n\n        if custom_to_json is None:\n            # Make sure the attribute is JSON serializable natively\n            try:\n                json.dumps(attr)\n            except Exception as e:\n                raise TypeError(\n                    f\"Attribute '{attr_name}' is not JSON serializable. \"\n                    \"Please provide a custom function to convert the \"\n                    \"attribute to a JSON-serializable format.\",\n                ) from e\n\n        if attr_name in self._module_dict:\n            raise ValueError(\n                f\"Attribute `{attr_name}` is already registered as a module. \",\n            )\n\n        self._attribute_dict[attr_name] = _JSONSerializeFunction(\n            to_json=custom_to_json,\n            load_json=custom_from_json,\n        )\n"
  },
  {
    "path": "src/agentscope/pipeline/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The pipeline module in AgentScope, that provides syntactic sugar for\ncomplex workflows and multi-agent conversations.\"\"\"\n\nfrom ._msghub import MsgHub\nfrom ._class import SequentialPipeline, FanoutPipeline\nfrom ._functional import (\n    sequential_pipeline,\n    fanout_pipeline,\n    stream_printing_messages,\n)\nfrom ._chat_room import ChatRoom\n\n__all__ = [\n    \"MsgHub\",\n    \"SequentialPipeline\",\n    \"sequential_pipeline\",\n    \"FanoutPipeline\",\n    \"fanout_pipeline\",\n    \"stream_printing_messages\",\n    \"ChatRoom\",\n]\n"
  },
  {
    "path": "src/agentscope/pipeline/_chat_room.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The Voice chat room\"\"\"\nimport asyncio\nfrom asyncio import Queue\n\nfrom ..agent import RealtimeAgent\nfrom ..realtime import ClientEvents, ServerEvents\n\n\nclass ChatRoom:\n    \"\"\"The chat room abstraction to broadcast messages among multiple realtime\n    agents, and handle the messages from the frontend.\n    \"\"\"\n\n    def __init__(self, agents: list[RealtimeAgent]) -> None:\n        \"\"\"Initialize the ChatRoom class.\n\n        Args:\n            agents (`list[RealtimeAgent]`):\n                The list of agents participating in the chat room.\n        \"\"\"\n        self.agents = agents\n\n        # The queue used to gather messages from all agents and push them to\n        # the frontend.\n        self._queue = Queue()\n\n        self._task = None\n\n    async def start(self, outgoing_queue: Queue) -> None:\n        \"\"\"Establish connections for all agents in the chat room.\n\n        Args:\n            outgoing_queue (`Queue`):\n                The queue to push messages to the frontend, which will be used\n                by all agents to push their messages.\n        \"\"\"\n\n        for agent in self.agents:\n            await agent.start(self._queue)\n\n        # Start the forwarding loop.\n        self._task = asyncio.create_task(self._forward_loop(outgoing_queue))\n\n    async def _forward_loop(self, outgoing_queue: Queue) -> None:\n        \"\"\"The loop to forward messages from all agents to the frontend and\n        the other agents.\n\n        Args:\n            outgoing_queue (`Queue`):\n                The queue to push messages to the frontend.\n        \"\"\"\n\n        while True:\n            # Obtain the message from the client frontend\n            event = await self._queue.get()\n\n            # Only push ServerEvents to the frontend, not ClientEvents\n            # to avoid echoing client messages back\n            if isinstance(event, ClientEvents.EventBase):\n                # Push the message to the frontend queue.\n                for agent in self.agents:\n                    await agent.handle_input(event)\n\n            elif isinstance(event, ServerEvents.EventBase):\n                # Broadcast the message to all agents except the sender.\n                # Use create_task instead of gather to avoid blocking\n\n                # Forward the agent/server events to the frontend\n                await outgoing_queue.put(event)\n\n                # Broadcast to other agents\n                sender_id = getattr(event, \"agent_id\", None)\n                if sender_id:\n                    for agent in self.agents:\n                        if agent.id != sender_id:\n                            await agent.handle_input(event)\n\n    async def stop(self) -> None:\n        \"\"\"Close connections for all agents in the chat room.\"\"\"\n\n        for agent in self.agents:\n            await agent.stop()\n\n        # Close the forwarding loop.\n        if not self._task.done():\n            self._task.cancel()\n\n    async def handle_input(self, event: ClientEvents.EventBase) -> None:\n        \"\"\"Handle input message from the frontend and distribute it to all\n        agents in the chat room.\n\n        Args:\n            event (`ClientEvents.EventBase`):\n                The event from the frontend.\n        \"\"\"\n        await self._queue.put(event)\n"
  },
  {
    "path": "src/agentscope/pipeline/_class.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Pipeline classes.\"\"\"\nfrom typing import Any\n\nfrom ._functional import sequential_pipeline, fanout_pipeline\nfrom ..agent import AgentBase\nfrom ..message import Msg\n\n\nclass SequentialPipeline:\n    \"\"\"An async sequential pipeline class, which executes a sequence of\n    agents sequentially. Compared with functional pipeline, this class\n    can be re-used.\"\"\"\n\n    def __init__(\n        self,\n        agents: list[AgentBase],\n    ) -> None:\n        \"\"\"Initialize a sequential pipeline class\n\n        Args:\n            agents (`list[AgentBase]`):\n                A list of agents.\n        \"\"\"\n        self.agents = agents\n\n    async def __call__(\n        self,\n        msg: Msg | list[Msg] | None = None,\n    ) -> Msg | list[Msg] | None:\n        \"\"\"Execute the sequential pipeline\n\n        Args:\n            msg (`Msg | list[Msg] | None`, defaults to `None`):\n                The initial input that will be passed to the first agent.\n        \"\"\"\n        return await sequential_pipeline(\n            agents=self.agents,\n            msg=msg,\n        )\n\n\nclass FanoutPipeline:\n    \"\"\"An async fanout pipeline class, which distributes the same input to\n    multiple agents. Compared with functional pipeline, this class can be\n    re-used and configured with default parameters.\"\"\"\n\n    def __init__(\n        self,\n        agents: list[AgentBase],\n        enable_gather: bool = True,\n    ) -> None:\n        \"\"\"Initialize a fanout pipeline class\n\n        Args:\n            agents (`list[AgentBase]`):\n                A list of agents to execute.\n            enable_gather (`bool`, defaults to `True`):\n                Whether to execute agents concurrently\n                using `asyncio.gather()`. If False, agents are executed\n                sequentially.\n        \"\"\"\n        self.agents = agents\n        self.enable_gather = enable_gather\n\n    async def __call__(\n        self,\n        msg: Msg | list[Msg] | None = None,\n        **kwargs: Any,\n    ) -> list[Msg]:\n        \"\"\"Execute the fanout pipeline\n\n        Args:\n            msg (`Msg | list[Msg] | None`, defaults to `None`):\n                The input message that will be distributed to all agents.\n            **kwargs (`Any`):\n                Additional keyword arguments passed to each agent during\n                execution.\n\n        Returns:\n            `list[Msg]`:\n                A list of output messages from all agents.\n        \"\"\"\n\n        return await fanout_pipeline(\n            agents=self.agents,\n            msg=msg,\n            enable_gather=self.enable_gather,\n            **kwargs,\n        )\n"
  },
  {
    "path": "src/agentscope/pipeline/_functional.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Functional counterpart for Pipeline\"\"\"\nimport asyncio\nfrom copy import deepcopy\nfrom typing import Any, AsyncGenerator, Tuple, Coroutine\nfrom ..agent import AgentBase\nfrom ..message import Msg, AudioBlock\n\n\nasync def sequential_pipeline(\n    agents: list[AgentBase],\n    msg: Msg | list[Msg] | None = None,\n) -> Msg | list[Msg] | None:\n    \"\"\"An async syntactic sugar pipeline that executes a sequence of agents\n    sequentially. The output of the previous agent will be passed as the\n    input to the next agent. The final output will be the output of the\n    last agent.\n\n    Example:\n        .. code-block:: python\n\n            agent1 = ReActAgent(...)\n            agent2 = ReActAgent(...)\n            agent3 = ReActAgent(...)\n\n            msg_input = Msg(\"user\", \"Hello\", \"user\")\n\n            msg_output = await sequential_pipeline(\n                [agent1, agent2, agent3],\n                msg_input\n            )\n\n    Args:\n        agents (`list[AgentBase]`):\n            A list of agents.\n        msg (`Msg | list[Msg] | None`, defaults to `None`):\n            The initial input that will be passed to the first agent.\n    Returns:\n        `Msg | list[Msg] | None`:\n            The output of the last agent in the sequence.\n    \"\"\"\n    for agent in agents:\n        msg = await agent(msg)\n    return msg\n\n\nasync def fanout_pipeline(\n    agents: list[AgentBase],\n    msg: Msg | list[Msg] | None = None,\n    enable_gather: bool = True,\n    **kwargs: Any,\n) -> list[Msg]:\n    \"\"\"A fanout pipeline that distributes the same input to multiple agents.\n    This pipeline sends the same message (or a deep copy of it) to all agents\n    and collects their responses. Agents can be executed either concurrently\n    using asyncio.gather() or sequentially depending on the enable_gather\n    parameter.\n\n    Example:\n        .. code-block:: python\n\n            agent1 = ReActAgent(...)\n            agent2 = ReActAgent(...)\n            agent3 = ReActAgent(...)\n\n            msg_input = Msg(\"user\", \"Hello\", \"user\")\n\n            # Concurrent execution (default)\n            results = await fanout_pipeline(\n                [agent1, agent2, agent3],\n                msg_input\n            )\n\n            # Sequential execution\n            results = await fanout_pipeline(\n                [agent1, agent2, agent3],\n                msg_input,\n                enable_gather=False\n            )\n\n    Args:\n        agents (`list[AgentBase]`):\n            A list of agents.\n        msg (`Msg | list[Msg] | None`, defaults to `None`):\n            The initial input that will be passed to all agents.\n        enable_gather (`bool`, defaults to `True`):\n            Whether to execute agents concurrently using `asyncio.gather()`.\n            If False, agents are executed sequentially.\n        **kwargs (`Any`):\n            Additional keyword arguments passed to each agent during execution.\n\n    Returns:\n        `list[Msg]`:\n            A list of response messages from each agent.\n    \"\"\"\n    if enable_gather:\n        tasks = [\n            asyncio.create_task(agent(deepcopy(msg), **kwargs))\n            for agent in agents\n        ]\n\n        return await asyncio.gather(*tasks)\n    else:\n        return [await agent(deepcopy(msg), **kwargs) for agent in agents]\n\n\nasync def stream_printing_messages(\n    agents: list[AgentBase],\n    coroutine_task: Coroutine,\n    queue: asyncio.Queue | None = None,\n    end_signal: str = \"[END]\",\n    yield_speech: bool = False,\n) -> AsyncGenerator[\n    Tuple[Msg, bool] | Tuple[Msg, bool, AudioBlock | list[AudioBlock] | None],\n    None,\n]:\n    \"\"\"This pipeline will gather the printing messages from agents when\n    execute the given coroutine task, and yield them one by one.\n    Only the messages that are printed by `await self.print(msg)` in the agent\n    will be forwarded to the message queue and yielded by this pipeline.\n\n    .. note:: The boolean in the yielded tuple indicates whether the message\n     is the last **chunk** for a streaming message, not the last message\n     returned by the agent. That means, there'll be multiple tuples with\n     `is_last_chunk=True` if the agent prints multiple messages.\n\n    .. note:: The messages with the same ``id`` is considered as the same\n     message, e.g., the chunks of a streaming message.\n\n    Args:\n        agents (`list[AgentBase]`):\n            A list of agents whose printing messages will be gathered and\n            yielded.\n        coroutine_task (`Coroutine`):\n            The coroutine task to be executed. This task should involve the\n            execution of the provided agents, so that their printing messages\n            can be captured and yielded.\n        queue (`asyncio.Queue | None`, optional):\n            Use this queue instead of creating a new one if provided.\n        end_signal (`str`, defaults to `\"[END]\"`):\n            A special signal to indicate the end of message streaming. When\n            this signal is received from the message queue, the generator will\n            stop yielding messages and exit the loop.\n        yield_speech (`bool`, defaults to `False`):\n            Whether to yield speech associated with the messages, if any.\n            If `True` and a speech is attached when calling `await\n            self.print()` in the agent, the yielded tuple will include the\n            speech as the third element. If `False`, only the message and\n            the boolean flag will be yielded.\n\n    Yields:\n        `Tuple[Msg, bool] | Tuple[Msg, bool, AudioBlock | list[AudioBlock] | \\\n        None]`:\n            A tuple containing the message, a boolean indicating whether\n            it's the last chunk in a streaming message, and optionally\n            the associated speech (if `yield_speech` is `True`).\n    \"\"\"\n\n    # Enable the message queue to get the intermediate messages\n    queue = queue or asyncio.Queue()\n    for agent in agents:\n        # Use one queue to gather messages from all agents\n        agent.set_msg_queue_enabled(True, queue)\n\n    # Execute the agent asynchronously\n    task = asyncio.create_task(coroutine_task)\n\n    if task.done():\n        await queue.put(end_signal)\n    else:\n        task.add_done_callback(lambda _: queue.put_nowait(end_signal))\n\n    # Receive the messages from the agent's message queue\n    while True:\n        # The message obj, and a boolean indicating whether it's the last chunk\n        # in a streaming message\n        printing_msg = await queue.get()\n\n        # Check if this is the end signal\n        if isinstance(printing_msg, str) and printing_msg == end_signal:\n            break\n\n        if yield_speech:\n            yield printing_msg\n        else:\n            msg, last, _ = printing_msg\n            yield msg, last\n\n    # Check exception after processing all messages\n    exception = task.exception()\n    if exception is not None:\n        raise exception from None\n"
  },
  {
    "path": "src/agentscope/pipeline/_msghub.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"MsgHub is designed to share messages among a group of agents.\"\"\"\n\nfrom collections.abc import Sequence\nfrom typing import Any\n\nimport shortuuid\n\nfrom .._logging import logger\nfrom ..agent import AgentBase\nfrom ..message import Msg\n\n\nclass MsgHub:\n    \"\"\"MsgHub class that controls the subscription of the participated agents.\n\n    Example:\n        In the following example, the reply message from `agent1`, `agent2`,\n        and `agent3` will be broadcast to all the other agents in the MsgHub.\n\n        .. code-block:: python\n\n            with MsgHub(participant=[agent1, agent2, agent3]):\n                agent1()\n                agent2()\n\n        Actually, it has the same effect as the following code, but much more\n        easy and elegant!\n\n        .. code-block:: python\n\n            x1 = agent1()\n            agent2.observe(x1)\n            agent3.observe(x1)\n\n            x2 = agent2()\n            agent1.observe(x2)\n            agent3.observe(x2)\n\n    \"\"\"\n\n    def __init__(\n        self,\n        participants: Sequence[AgentBase],\n        announcement: list[Msg] | Msg | None = None,\n        enable_auto_broadcast: bool = True,\n        name: str | None = None,\n    ) -> None:\n        \"\"\"Initialize a MsgHub context manager.\n\n        Args:\n            participants (`Sequence[AgentBase]`):\n                A sequence of agents that participate in the MsgHub.\n            announcement (`list[Msg] | Msg | None`):\n                The message that will be broadcast to all participants when\n                entering the MsgHub.\n            enable_auto_broadcast (`bool`, defaults to `True`):\n                Whether to enable automatic broadcasting of the replied\n                message from any participant to all other participants. If\n                disabled, the MsgHub will only serve as a manual message\n                broadcaster with the `announcement` argument and the\n                `broadcast()` method.\n            name (`str | None`):\n                The name of this MsgHub. If not provided, a random ID\n                will be generated.\n        \"\"\"\n\n        self.name = name or shortuuid.uuid()\n        self.participants = list(participants)\n        self.announcement = announcement\n        self.enable_auto_broadcast = enable_auto_broadcast\n\n    async def __aenter__(self) -> \"MsgHub\":\n        \"\"\"Will be called when entering the MsgHub.\"\"\"\n        self._reset_subscriber()\n\n        # broadcast the input message to all participants\n        if self.announcement is not None:\n            await self.broadcast(msg=self.announcement)\n\n        return self\n\n    async def __aexit__(self, *args: Any, **kwargs: Any) -> None:\n        \"\"\"Will be called when exiting the MsgHub.\"\"\"\n        if self.enable_auto_broadcast:\n            for agent in self.participants:\n                agent.remove_subscribers(self.name)\n\n    def _reset_subscriber(self) -> None:\n        \"\"\"Reset the subscriber for agent in `self.participant`\"\"\"\n        if self.enable_auto_broadcast:\n            for agent in self.participants:\n                agent.reset_subscribers(self.name, self.participants)\n\n    def add(\n        self,\n        new_participant: list[AgentBase] | AgentBase,\n    ) -> None:\n        \"\"\"Add new participant into this hub\"\"\"\n        if isinstance(new_participant, AgentBase):\n            new_participant = [new_participant]\n\n        for agent in new_participant:\n            if agent not in self.participants:\n                self.participants.append(agent)\n\n        self._reset_subscriber()\n\n    def delete(\n        self,\n        participant: list[AgentBase] | AgentBase,\n    ) -> None:\n        \"\"\"Delete agents from participant.\"\"\"\n        if isinstance(participant, AgentBase):\n            participant = [participant]\n\n        for agent in participant:\n            if agent in self.participants:\n                # remove agent from self.participant\n                self.participants.pop(self.participants.index(agent))\n            else:\n                logger.warning(\n                    \"Cannot find the agent with ID %s, skip its deletion.\",\n                    agent.id,\n                )\n\n        # Remove this agent from the subscriber of other agents\n        self._reset_subscriber()\n\n    async def broadcast(self, msg: list[Msg] | Msg) -> None:\n        \"\"\"Broadcast the message to all participants.\n\n        Args:\n            msg (`list[Msg] | Msg`):\n                Message(s) to be broadcast among all participants.\n        \"\"\"\n        for agent in self.participants:\n            await agent.observe(msg)\n\n    def set_auto_broadcast(self, enable: bool) -> None:\n        \"\"\"Enable automatic broadcasting of the replied message from any\n        participant to all other participants.\n\n        Args:\n            enable (`bool`):\n                Whether to enable automatic broadcasting. If disabled, the\n                MsgHub will only serve as a manual message broadcaster with\n                the `announcement` argument and the `broadcast()` method.\n        \"\"\"\n        if enable:\n            self.enable_auto_broadcast = True\n            self._reset_subscriber()\n        else:\n            self.enable_auto_broadcast = False\n            for agent in self.participants:\n                agent.remove_subscribers(self.name)\n"
  },
  {
    "path": "src/agentscope/plan/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The plan module in AgentScope.\"\"\"\nfrom ._plan_model import (\n    SubTask,\n    Plan,\n)\nfrom ._plan_notebook import (\n    DefaultPlanToHint,\n    PlanNotebook,\n)\nfrom ._storage_base import PlanStorageBase\nfrom ._in_memory_storage import InMemoryPlanStorage\n\n__all__ = [\n    \"SubTask\",\n    \"Plan\",\n    \"DefaultPlanToHint\",\n    \"PlanNotebook\",\n    \"PlanStorageBase\",\n    \"InMemoryPlanStorage\",\n]\n"
  },
  {
    "path": "src/agentscope/plan/_in_memory_storage.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The in-memory plan storage class.\"\"\"\nfrom collections import OrderedDict\n\nfrom ._plan_model import Plan\nfrom ._storage_base import PlanStorageBase\n\n\nclass InMemoryPlanStorage(PlanStorageBase):\n    \"\"\"In-memory plan storage.\"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"Initialize the in-memory plan storage.\"\"\"\n        super().__init__()\n        self.plans = OrderedDict()\n\n        # Support historical plan serialization and deserialization\n        self.register_state(\n            \"plans\",\n            lambda plans: {k: v.model_dump() for k, v in plans.items()},\n            lambda json_data: OrderedDict(\n                (k, Plan.model_validate(v)) for k, v in json_data.items()\n            ),\n        )\n\n    async def add_plan(self, plan: Plan, override: bool = True) -> None:\n        \"\"\"Add a plan to the storage.\n\n        Args:\n            plan (`Plan`):\n                The plan to be added.\n            override (`bool`, defaults to `True`):\n                Whether to override the existing plan with the same ID.\n        \"\"\"\n        if plan.id in self.plans and not override:\n            raise ValueError(\n                f\"Plan with id {plan.id} already exists.\",\n            )\n        self.plans[plan.id] = plan\n\n    async def delete_plan(self, plan_id: str) -> None:\n        \"\"\"Delete a plan from the storage.\n\n        Args:\n            plan_id (`str`):\n                The ID of the plan to be deleted.\n        \"\"\"\n        self.plans.pop(plan_id, None)\n\n    async def get_plans(self) -> list[Plan]:\n        \"\"\"Get all plans from the storage.\n\n        Returns:\n            `list[Plan]`:\n                A list of all plans in the storage.\n        \"\"\"\n        return list(self.plans.values())\n\n    async def get_plan(self, plan_id: str) -> Plan | None:\n        \"\"\"Get a plan by its ID.\n\n        Args:\n            plan_id (`str`):\n                The ID of the plan to be retrieved.\n\n        Returns:\n            `Plan | None`:\n                The plan with the specified ID, or None if not found.\n        \"\"\"\n        return self.plans.get(plan_id, None)\n"
  },
  {
    "path": "src/agentscope/plan/_plan_model.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The models used in the plan module.\"\"\"\nfrom typing import Literal\n\nimport shortuuid\nfrom pydantic import BaseModel, Field\n\nfrom .._utils._common import _get_timestamp\n\n\nclass SubTask(BaseModel):\n    \"\"\"The subtask model used in the plan module.\"\"\"\n\n    name: str = Field(\n        description=(\n            \"The subtask name, should be concise, descriptive and not\"\n            \"exceed 10 words.\"\n        ),\n    )\n    description: str = Field(\n        description=(\n            \"The subtask description, including the constraints, target and \"\n            \"outcome to be achieved. The description should be clear, \"\n            \"specific and concise, and all the constraints, target and \"\n            \"outcome should be specific and measurable.\"\n        ),\n    )\n    expected_outcome: str = Field(\n        description=(\n            \"The expected outcome of the subtask, which should be specific, \"\n            \"concrete and measurable.\"\n        ),\n    )\n    outcome: str | None = Field(\n        description=\"The actual outcome of the subtask.\",\n        default=None,\n    )\n    state: Literal[\"todo\", \"in_progress\", \"done\", \"abandoned\"] = Field(\n        description=\"The state of the subtask.\",\n        default=\"todo\",\n    )\n    created_at: str = Field(\n        description=\"The time the subtask was created.\",\n        default_factory=_get_timestamp,\n    )\n    # Result related fields\n    finished_at: str | None = Field(\n        description=\"The time the subtask was finished.\",\n        default=None,\n    )\n\n    def finish(self, outcome: str) -> None:\n        \"\"\"Finish the subtask with the actual outcome.\"\"\"\n        self.state = \"done\"\n        self.outcome = outcome\n        self.finished_at = _get_timestamp()\n\n    def to_oneline_markdown(self) -> str:\n        \"\"\"Convert the subtask to MarkDown format.\"\"\"\n        status_map = {\n            \"todo\": \"- []\",\n            \"in_progress\": \"- [][WIP]\",\n            \"done\": \"- [x]\",\n            \"abandoned\": \"- [][Abandoned]\",\n        }\n        return f\"{status_map[self.state]} {self.name}\"\n\n    def to_markdown(self, detailed: bool = False) -> str:\n        \"\"\"Convert the subtask to MarkDown format.\n\n        Args:\n            detailed (`bool`, defaults to `False`):\n                Whether to include detailed information about the subtask.\n        \"\"\"\n        status_map = {\n            \"todo\": \"- [ ] \",\n            \"in_progress\": \"- [ ] [WIP]\",\n            \"done\": \"- [x] \",\n            \"abandoned\": \"- [ ] [Abandoned]\",\n        }\n\n        if detailed:\n            markdown_strs = [\n                f\"{status_map[self.state]}{self.name}\",\n                f\"\\t- Created At: {self.created_at}\",\n                f\"\\t- Description: {self.description}\",\n                f\"\\t- Expected Outcome: {self.expected_outcome}\",\n                f\"\\t- State: {self.state}\",\n            ]\n\n            if self.state == \"done\":\n                markdown_strs.extend(\n                    [\n                        f\"\\t- Finished At: {self.finished_at}\",\n                        f\"\\t- Actual Outcome: {self.outcome}\",\n                    ],\n                )\n\n            return \"\\n\".join(markdown_strs)\n\n        return f\"{status_map[self.state]}{self.name}\"\n\n\nclass Plan(BaseModel):\n    \"\"\"The plan model used in the plan module, contains a list of subtasks.\"\"\"\n\n    id: str = Field(default_factory=shortuuid.uuid)\n    name: str = Field(\n        description=(\n            \"The plan name, should be concise, descriptive and not exceed 10 \"\n            \"words.\"\n        ),\n    )\n    description: str = Field(\n        description=(\n            \"The plan description, including the constraints, target and \"\n            \"outcome to be achieved. The description should be clear, \"\n            \"specific and concise, and all the constraints, target and \"\n            \"outcome should be specific and measurable.\"\n        ),\n    )\n    expected_outcome: str = Field(\n        description=(\n            \"The expected outcome of the plan, which should be specific, \"\n            \"concrete and measurable.\"\n        ),\n    )\n    subtasks: list[SubTask] = Field(\n        description=(\"A list of subtasks that make up the plan.\"),\n    )\n    created_at: str = Field(\n        description=\"The time the plan was created.\",\n        default_factory=_get_timestamp,\n    )\n    state: Literal[\"todo\", \"in_progress\", \"done\", \"abandoned\"] = Field(\n        description=\"The state of the plan.\",\n        default=\"todo\",\n    )\n    finished_at: str | None = Field(\n        description=\"The time the plan was finished.\",\n        default=None,\n    )\n    outcome: str | None = Field(\n        description=\"The actual outcome of the plan.\",\n        default=None,\n    )\n\n    def refresh_plan_state(self) -> str:\n        \"\"\"Refresh the plan state based on the states of its subtasks. This\n        function only switches the plan state between \"todo\" and \"in_progress\".\n\n        # TODO: Handle the plan state much more formally.\n        \"\"\"\n        if self.state in [\"done\", \"abandoned\"]:\n            return \"\"\n\n        any_in_progress = any(_.state == \"in_progress\" for _ in self.subtasks)\n\n        if any_in_progress and self.state == \"todo\":\n            self.state = \"in_progress\"\n            return \"The plan state has been updated to 'in_progress'.\"\n\n        elif not any_in_progress and self.state == \"in_progress\":\n            self.state = \"todo\"\n            return \"The plan state has been updated to 'todo'.\"\n\n        return \"\"\n\n    def finish(\n        self,\n        state: Literal[\"done\", \"abandoned\"],\n        outcome: str,\n    ) -> None:\n        \"\"\"Finish the plan.\"\"\"\n        self.state = state\n        self.outcome = outcome\n        self.finished_at = _get_timestamp()\n\n    def to_markdown(self, detailed: bool = False) -> str:\n        \"\"\"Convert the plan to MarkDown format.\"\"\"\n        subtasks_markdown = \"\\n\".join(\n            [\n                subtask.to_markdown(\n                    detailed=detailed,\n                )\n                for subtask in self.subtasks\n            ],\n        )\n\n        return \"\\n\".join(\n            [\n                f\"# {self.name}\",\n                f\"**Description**: {self.description}\",\n                f\"**Expected Outcome**: {self.expected_outcome}\",\n                f\"**State**: {self.state}\",\n                f\"**Created At**: {self.created_at}\",\n                \"## Subtasks\",\n                subtasks_markdown,\n            ],\n        )\n"
  },
  {
    "path": "src/agentscope/plan/_plan_notebook.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The plan notebook class, used to manage the plan, providing hints and\ntool functions to the agent.\"\"\"\nfrom collections import OrderedDict\nfrom typing import Callable, Literal, Coroutine, Any, Awaitable\n\nfrom ._in_memory_storage import InMemoryPlanStorage\nfrom ._plan_model import SubTask, Plan\nfrom ._storage_base import PlanStorageBase\nfrom .._utils._common import _execute_async_or_sync_func\nfrom ..message import TextBlock, Msg\nfrom ..module import StateModule\nfrom ..tool import ToolResponse\n\n\nclass DefaultPlanToHint:\n    \"\"\"The default function to generate the hint message based on the current\n    plan to guide the agent on next steps.\"\"\"\n\n    hint_prefix: str = \"<system-hint>\"\n    hint_suffix: str = \"</system-hint>\"\n\n    no_plan: str = (\n        \"If the user's query is complex (e.g. programming a website, game or \"\n        \"app), or requires a long chain of steps to complete (e.g. conduct \"\n        \"research on a certain topic from different sources), you NEED to \"\n        \"create a plan first by calling 'create_plan'. Otherwise, you can \"\n        \"directly execute the user's query without planning.\"\n    )\n\n    at_the_beginning: str = (\n        \"The current plan:\\n\"\n        \"```\\n\"\n        \"{plan}\\n\"\n        \"```\\n\"\n        \"Your options include:\\n\"\n        \"- Mark the first subtask as 'in_progress' by calling \"\n        \"'update_subtask_state' with subtask_idx=0 and state='in_progress', \"\n        \"and start executing it.\\n\"\n        \"- If the first subtask is not executable, analyze why and what you \"\n        \"can do to advance the plan, e.g. ask user for more information, \"\n        \"revise the plan by calling 'revise_current_plan'.\\n\"\n        \"- If the user asks you to do something unrelated to the plan, \"\n        \"prioritize the completion of user's query first, and then return \"\n        \"to the plan afterward.\\n\"\n        \"- If the user no longer wants to perform the current plan, confirm \"\n        \"with the user and call the 'finish_plan' function.\\n\"\n    )\n\n    when_a_subtask_in_progress: str = (\n        \"The current plan:\\n\"\n        \"```\\n\"\n        \"{plan}\\n\"\n        \"```\\n\"\n        \"Now the subtask at index {subtask_idx}, named '{subtask_name}', is \"\n        \"'in_progress'. Its details are as follows:\\n\"\n        \"```\\n\"\n        \"{subtask}\\n\"\n        \"```\\n\"\n        \"Your options include:\\n\"\n        \"- Go on execute the subtask and get the outcome.\\n\"\n        \"- Call 'finish_subtask' with the specific outcome if the subtask is \"\n        \"finished.\\n\"\n        \"- Ask the user for more information if you need.\\n\"\n        \"- Revise the plan by calling 'revise_current_plan' if necessary.\\n\"\n        \"- If the user asks you to do something unrelated to the plan, \"\n        \"prioritize the completion of user's query first, and then return to \"\n        \"the plan afterward.\"\n    )\n\n    when_no_subtask_in_progress: str = (\n        \"The current plan:\\n\"\n        \"```\\n\"\n        \"{plan}\\n\"\n        \"```\\n\"\n        \"The first {index} subtasks are done, and there is no subtask \"\n        \"'in_progress'. Now Your options include:\\n\"\n        \"- Mark the next subtask as 'in_progress' by calling \"\n        \"'update_subtask_state', and start executing it.\\n\"\n        \"- Ask the user for more information if you need.\\n\"\n        \"- Revise the plan by calling 'revise_current_plan' if necessary.\\n\"\n        \"- If the user asks you to do something unrelated to the plan, \"\n        \"prioritize the completion of user's query first, and then return to \"\n        \"the plan afterward.\"\n    )\n\n    at_the_end: str = (\n        \"The current plan:\\n\"\n        \"```\\n\"\n        \"{plan}\\n\"\n        \"```\\n\"\n        \"All the subtasks are done. Now your options are:\\n\"\n        \"- Finish the plan by calling 'finish_plan' with the specific \"\n        \"outcome, and summarize the whole process and outcome to the user.\\n\"\n        \"- Revise the plan by calling 'revise_current_plan' if necessary.\\n\"\n        \"- If the user asks you to do something unrelated to the plan, \"\n        \"prioritize the completion of user's query first, and then return to \"\n        \"the plan afterward.\"\n    )\n\n    def __call__(self, plan: Plan | None) -> str | None:\n        \"\"\"Generate the hint message based on the input plan to guide the\n        agent on next steps.\n\n        Args:\n            plan (`Plan | None`):\n                The current plan, used to generate the hint message.\n\n        Returns:\n            `str | None`:\n                The generated hint message, or None if the plan is None or\n                there is no relevant hint.\n        \"\"\"\n        if plan is None:\n            hint = self.no_plan\n\n        else:\n            # Count the number of subtasks in each state\n            n_todo, n_in_progress, n_done, n_abandoned = 0, 0, 0, 0\n            in_progress_subtask_idx = None\n            for idx, subtask in enumerate(plan.subtasks):\n                if subtask.state == \"todo\":\n                    n_todo += 1\n\n                elif subtask.state == \"in_progress\":\n                    n_in_progress += 1\n                    in_progress_subtask_idx = idx\n\n                elif subtask.state == \"done\":\n                    n_done += 1\n\n                elif subtask.state == \"abandoned\":\n                    n_abandoned += 1\n\n            hint = None\n            if n_in_progress == 0 and n_done == 0:\n                # All subtasks are todo\n                hint = self.at_the_beginning.format(\n                    plan=plan.to_markdown(),\n                )\n\n            elif n_in_progress > 0 and in_progress_subtask_idx is not None:\n                # One subtask is in_progress\n                hint = self.when_a_subtask_in_progress.format(\n                    plan=plan.to_markdown(),\n                    subtask_idx=in_progress_subtask_idx,\n                    subtask_name=plan.subtasks[in_progress_subtask_idx].name,\n                    subtask=plan.subtasks[in_progress_subtask_idx].to_markdown(\n                        detailed=True,\n                    ),\n                )\n\n            elif n_done + n_abandoned == len(plan.subtasks):\n                # All subtasks are done or abandoned\n                hint = self.at_the_end.format(\n                    plan=plan.to_markdown(),\n                )\n\n            elif n_in_progress == 0 and n_done > 0:\n                # No subtask is in_progress, and some subtasks are done\n                hint = self.when_no_subtask_in_progress.format(\n                    plan=plan.to_markdown(),\n                    index=n_done,\n                )\n\n        if hint:\n            return f\"{self.hint_prefix}{hint}{self.hint_suffix}\"\n\n        return hint\n\n\nclass PlanNotebook(StateModule):\n    \"\"\"The plan notebook to manage the plan, providing hints and plan related\n    tool functions to the agent.\"\"\"\n\n    _plan_change_hooks: dict[\n        str,\n        Callable[[\"PlanNotebook\", Plan], None]\n        | Callable[[\"PlanNotebook\", Plan], Awaitable[None]],\n    ]\n    \"\"\"The hooks that will be triggered when the plan is changed. For example,\n    used to display the plan on the frontend.\"\"\"\n\n    description: str = (\n        \"The plan-related tools. Activate this tool when you need to execute \"\n        \"complex task, e.g. building a website or a game. Once activated, \"\n        \"you'll enter the plan mode, where you will be guided to complete \"\n        \"the given query by creating and following a plan, and hint message \"\n        \"wrapped by <system-hint></system-hint> will guide you to complete \"\n        \"the task. If you think the user no longer wants to perform the \"\n        \"current task, you need to confirm with the user and call the \"\n        \"'finish_plan' function.\"\n    )\n\n    def __init__(\n        self,\n        max_subtasks: int | None = None,\n        plan_to_hint: Callable[[Plan | None], str | None] | None = None,\n        storage: PlanStorageBase | None = None,\n    ) -> None:\n        \"\"\"Initialize the plan notebook.\n\n        Args:\n            max_subtasks (`int | None`, optional):\n                The maximum number of subtasks in a plan.\n            plan_to_hint (`Callable[[Plan | None], str | None] | None`, \\\n             optional):\n                The function to generate the hint message based on the\n                current plan. If not provided, a default `DefaultPlanToHint`\n                object will be used.\n            storage (`PlanStorageBase | None`, optional):\n                The plan storage. If not provided, an in-memory storage will\n                be used.\n        \"\"\"\n        super().__init__()\n\n        self.max_tasks = max_subtasks\n        self.plan_to_hint = plan_to_hint or DefaultPlanToHint()\n        self.storage = storage or InMemoryPlanStorage()\n\n        self.current_plan: Plan | None = None\n\n        self._plan_change_hooks = OrderedDict()\n\n        # Register the current_plan state for state management\n        self.register_state(\n            \"current_plan\",\n            custom_to_json=lambda _: _.model_dump() if _ else None,\n            custom_from_json=lambda _: Plan.model_validate(_) if _ else None,\n        )\n\n    async def create_plan(\n        self,\n        name: str,\n        description: str,\n        expected_outcome: str,\n        subtasks: list[SubTask],\n    ) -> ToolResponse:\n        \"\"\"Create a plan by given name and sub-tasks.\n\n        Args:\n            name (`str`):\n                The plan name, should be concise, descriptive and not exceed\n                10 words.\n            description (`str`):\n                The plan description, including the constraints, target and\n                outcome to be achieved. The description should be clear,\n                specific and concise, and all the constraints, target and\n                outcome should be specific and measurable.\n            expected_outcome (`str`):\n                The expected outcome of the plan, which should be specific,\n                concrete and measurable.\n            subtasks (`list[SubTask]`):\n                A list of sequential sub-tasks that make up the plan.\n\n        Returns:\n            `ToolResponse`:\n                The response of the tool call.\n        \"\"\"\n        plan = Plan(\n            name=name,\n            description=description,\n            expected_outcome=expected_outcome,\n            subtasks=subtasks,\n        )\n\n        if self.current_plan is None:\n            res = ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Plan '{name}' created successfully.\",\n                    ),\n                ],\n            )\n\n        else:\n            res = ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=(\n                            \"The current plan named \"\n                            f\"'{self.current_plan.name}' is replaced by the \"\n                            f\"newly created plan named '{name}'.\"\n                        ),\n                    ),\n                ],\n            )\n\n        self.current_plan = plan\n        await self._trigger_plan_change_hooks()\n        return res\n\n    def _validate_current_plan(self) -> None:\n        \"\"\"Validate the current plan.\"\"\"\n        if self.current_plan is None:\n            raise ValueError(\n                \"The current plan is None, you need to create a plan by \"\n                \"calling create_plan() first.\",\n            )\n\n    async def revise_current_plan(\n        self,\n        subtask_idx: int,\n        action: Literal[\"add\", \"revise\", \"delete\"],\n        subtask: SubTask | None = None,\n    ) -> ToolResponse:\n        \"\"\"Revise the current plan by adding, revising or deleting a sub-task.\n\n        Args:\n            subtask_idx (`int`):\n                The index of the sub-task to be revised, starting from 0.\n            action (`Literal[\"add\", \"revise\", \"delete\"]`):\n                The action to be performed on the sub-task. If \"add\", the\n                sub-task will be inserted before the given index. If \"revise\",\n                the sub-task at the given index will be revised. If \"delete\",\n                the sub-task at the given index will be deleted.\n            subtask (`SubTask | None`, optional):\n                The sub-task to be added or revised. Required if action is\n                \"add\" or \"revise\".\n\n        Raises:\n            `ValueError`:\n                If the current plan is `None`, `ValueError` will be raised.\n\n        Returns:\n            `ToolResponse`:\n                The response of the tool call.\n        \"\"\"\n        # Validate the arguments first\n        response: list[str] = []\n        if isinstance(subtask_idx, str):\n            try:\n                subtask_idx = int(subtask_idx)\n            except ValueError:\n                return ToolResponse(\n                    content=[\n                        TextBlock(\n                            type=\"text\",\n                            text=f\"Error: Invalid type for argument \"\n                            f\"'subtask_idx'. Expected 'int', but got \"\n                            f\"'{type(subtask_idx)}'.\",\n                        ),\n                    ],\n                )\n\n        if action not in [\"add\", \"revise\", \"delete\"]:\n            response.append(\n                f\"Invalid action '{action}'. Must be one of 'add', 'revise', \"\n                f\"'delete'.\",\n            )\n\n        if action in [\"add\", \"revise\"] and subtask is None:\n            response.append(\n                f\"The subtask must be provided when action is '{action}', \"\n                \"but got None.\",\n            )\n\n        self._validate_current_plan()\n\n        if action != \"add\" and subtask_idx >= len(\n            self.current_plan.subtasks,\n        ):\n            response.append(\n                f\"Invalid subtask_idx '{subtask_idx}' for action \"\n                f\"'{action}'. Must be between 0 \"\n                f\"and {len(self.current_plan.subtasks) - 1}.\",\n            )\n\n        if action == \"add\" and not (\n            0 <= subtask_idx <= len(self.current_plan.subtasks)\n        ):\n            max_idx = len(self.current_plan.subtasks)\n            response.append(\n                f\"Invalid subtask_idx '{subtask_idx}' for action 'add'. \"\n                f\"Must be between 0 and {max_idx}.\",\n            )\n\n        if response:\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=\"Error: \" + response[0],\n                    ),\n                ],\n            )\n\n        if subtask is not None:\n            # Convert to SubTask model if it's a dict\n            subtask = SubTask.model_validate(subtask)\n\n        if action == \"delete\":\n            subtask = self.current_plan.subtasks.pop(subtask_idx)\n            await self._trigger_plan_change_hooks()\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Subtask (named '{subtask.name}') at index \"\n                        f\"{subtask_idx} is deleted successfully.\",\n                    ),\n                ],\n            )\n\n        if action == \"add\" and subtask:\n            self.current_plan.subtasks.insert(subtask_idx, subtask)\n            await self._trigger_plan_change_hooks()\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"New subtask is added successfully at index \"\n                        f\"{subtask_idx}.\",\n                    ),\n                ],\n            )\n\n        # revise\n        self.current_plan.subtasks[subtask_idx] = subtask\n        await self._trigger_plan_change_hooks()\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=f\"Subtask at index {subtask_idx} is revised \"\n                    f\"successfully.\",\n                ),\n            ],\n        )\n\n    async def update_subtask_state(\n        self,\n        subtask_idx: int,\n        state: Literal[\"todo\", \"in_progress\", \"abandoned\"],\n    ) -> ToolResponse:\n        \"\"\"Update the state of a subtask by given index and state. Note if you\n        want to mark a subtask as done, you SHOULD call `finish_subtask`\n        instead with the specific outcome.\n\n        Args:\n            subtask_idx (`int`):\n                The index of the subtask to be updated, starting from 0.\n            state (`Literal[\"todo\", \"in_progress\", \"abandoned\"]`):\n                The new state of the subtask. If you want to mark a subtask\n                as done, you SHOULD call `finish_subtask` instead with the\n                specific outcome.\n        \"\"\"\n        self._validate_current_plan()\n\n        if isinstance(subtask_idx, str):\n            try:\n                subtask_idx = int(subtask_idx)\n            except ValueError:\n                pass\n\n        if not isinstance(subtask_idx, int):\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Invalid type for argument 'subtask_idx'. \"\n                        f\"Expected 'int', but got '{type(subtask_idx)}'.\",\n                    ),\n                ],\n            )\n\n        if not 0 <= subtask_idx < len(self.current_plan.subtasks):\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Invalid subtask_idx '{subtask_idx}'. Must \"\n                        f\"be between 0 and \"\n                        f\"{len(self.current_plan.subtasks) - 1}.\",\n                    ),\n                ],\n            )\n\n        if state not in [\"todo\", \"in_progress\", \"abandoned\"]:\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Invalid state '{state}'. Must be one of \"\n                        \"'todo', 'in_progress', 'abandoned'.\",\n                    ),\n                ],\n            )\n\n        # Only one subtask can be in_progress at a time\n        if state == \"in_progress\":\n            # Check only one subtask is in_progress\n            for idx, subtask in enumerate(self.current_plan.subtasks):\n                # Check all previous subtasks are done or deprecated\n                if idx < subtask_idx and subtask.state not in [\n                    \"done\",\n                    \"abandoned\",\n                ]:\n                    return ToolResponse(\n                        content=[\n                            TextBlock(\n                                type=\"text\",\n                                text=(\n                                    f\"Subtask (at index {idx}) named \"\n                                    f\"'{subtask.name}' is not done yet. You \"\n                                    \"should finish the previous subtasks \"\n                                    \"first.\"\n                                ),\n                            ),\n                        ],\n                    )\n\n                # Check no other subtask is in_progress\n                if subtask.state == \"in_progress\":\n                    return ToolResponse(\n                        content=[\n                            TextBlock(\n                                type=\"text\",\n                                text=(\n                                    f\"Subtask (at index {idx}) named \"\n                                    f\"'{subtask.name}' is already \"\n                                    \"'in_progress'. You should finish it \"\n                                    \"first before starting another subtask.\"\n                                ),\n                            ),\n                        ],\n                    )\n\n        self.current_plan.subtasks[subtask_idx].state = state\n\n        # Update the plan state to in_progress if not already\n        suffix = self.current_plan.refresh_plan_state()\n\n        await self._trigger_plan_change_hooks()\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=f\"Subtask at index {subtask_idx}, named \"\n                    f\"'{self.current_plan.subtasks[subtask_idx].name}' \"\n                    f\"is marked as '{state}' successfully. \" + suffix,\n                ),\n            ],\n        )\n\n    async def finish_subtask(\n        self,\n        subtask_idx: int,\n        subtask_outcome: str,\n    ) -> ToolResponse:\n        \"\"\"Label the subtask as done by given index and outcome.\n\n        Args:\n            subtask_idx (`int`):\n                The index of the sub-task to be marked as done, starting\n                from 0.\n            subtask_outcome (`str`):\n                The specific outcome of the sub-task, should exactly match the\n                expected outcome in the sub-task description. SHOULDN't be\n                what you did or general description, e.g. \"I have searched\n                xxx\", \"I have written the code for xxx\", etc. It SHOULD be\n                the specific data, information, or path to the file, e.g.\n                \"There are 5 articles about xxx, they are\\n- xxx\\n- xxx\\n...\"\n        \"\"\"\n        self._validate_current_plan()\n\n        if isinstance(subtask_idx, str):\n            try:\n                subtask_idx = int(subtask_idx)\n            except ValueError:\n                pass\n\n        if not isinstance(subtask_idx, int):\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Invalid type for argument 'subtask_idx'. \"\n                        f\"Expected 'int', but got '{type(subtask_idx)}'.\",\n                    ),\n                ],\n            )\n\n        if not 0 <= subtask_idx < len(self.current_plan.subtasks):\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Invalid subtask_idx '{subtask_idx}'. Must \"\n                        f\"be between 0 and \"\n                        f\"{len(self.current_plan.subtasks) - 1}.\",\n                    ),\n                ],\n            )\n\n        for idx, subtask in enumerate(\n            self.current_plan.subtasks[0:subtask_idx],\n        ):\n            if subtask.state not in [\"done\", \"abandoned\"]:\n                return ToolResponse(\n                    content=[\n                        TextBlock(\n                            type=\"text\",\n                            text=(\n                                \"Cannot finish subtask at index \"\n                                f\"{subtask_idx} because the previous \"\n                                f\"subtask (at index {idx}) named \"\n                                f\"'{subtask.name}' is not done yet. You \"\n                                \"should finish the previous subtasks first.\"\n                            ),\n                        ),\n                    ],\n                )\n\n        # Label the subtask as done\n        self.current_plan.subtasks[subtask_idx].finish(subtask_outcome)\n        # Auto activate the next subtask if exists\n        if subtask_idx + 1 < len(self.current_plan.subtasks):\n            self.current_plan.subtasks[subtask_idx + 1].state = \"in_progress\"\n            next_subtask = self.current_plan.subtasks[subtask_idx + 1]\n            await self._trigger_plan_change_hooks()\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=(\n                            f\"Subtask (at index {subtask_idx}) named \"\n                            f\"'{self.current_plan.subtasks[subtask_idx].name}'\"\n                            \" is marked as done successfully. The next \"\n                            f\"subtask named '{next_subtask.name}' is \"\n                            f\"activated.\"\n                        ),\n                    ),\n                ],\n            )\n\n        await self._trigger_plan_change_hooks()\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=(\n                        f\"Subtask (at index {subtask_idx}) named \"\n                        f\"'{self.current_plan.subtasks[subtask_idx].name}'\"\n                        \" is marked as done successfully. \"\n                    ),\n                ),\n            ],\n        )\n\n    async def view_subtasks(self, subtask_idx: list[int]) -> ToolResponse:\n        \"\"\"View the details of the sub-tasks by given indexes.\n\n        Args:\n            subtask_idx (`list[int]`):\n                The indexes of the sub-tasks to be viewed, starting from 0.\n        \"\"\"\n        self._validate_current_plan()\n\n        gathered_strs = []\n        invalid_subtask_idx = []\n        for idx in subtask_idx:\n            if not 0 <= idx < len(self.current_plan.subtasks):\n                invalid_subtask_idx.append(idx)\n                continue\n\n            subtask_markdown = self.current_plan.subtasks[idx].to_markdown(\n                detailed=True,\n            )\n            gathered_strs.append(\n                f\"Subtask at index {idx}:\\n\"\n                \"```\\n\"\n                f\"{subtask_markdown}\\n\"\n                \"```\\n\",\n            )\n\n        if invalid_subtask_idx:\n            gathered_strs.append(\n                f\"Invalid subtask_idx '{invalid_subtask_idx}'. Must be \"\n                f\"between 0 and {len(self.current_plan.subtasks) - 1}.\",\n            )\n\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=\"\\n\".join(gathered_strs),\n                ),\n            ],\n        )\n\n    async def finish_plan(\n        self,\n        state: Literal[\"done\", \"abandoned\"],\n        outcome: str,\n    ) -> ToolResponse:\n        \"\"\"Finish the current plan by given outcome, or abandon it with the\n        given reason if the user no longer wants to perform it. Note that you\n        SHOULD confirm with the user before abandoning the plan.\n\n        Args:\n            state (`Literal[\"done\", \"abandoned\"]`):\n                The state to finish the plan. If \"done\", the plan will be\n                marked as done with the given outcome. If \"abandoned\", the\n                plan will be abandoned with the given reason.\n            outcome (`str`):\n                The specific outcome of the plan if state is \"done\", or the\n                reason for abandoning the plan if state is \"abandoned\".\n        \"\"\"\n        if self.current_plan is None:\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=\"There is no plan to finish.\",\n                    ),\n                ],\n            )\n\n        self.current_plan.finish(state, outcome)\n\n        # Store the finished plan into history\n        await self.storage.add_plan(self.current_plan)\n\n        self.current_plan = None\n        await self._trigger_plan_change_hooks()\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=f\"The current plan is finished successfully as \"\n                    f\"'{state}'.\",\n                ),\n            ],\n        )\n\n    async def view_historical_plans(self) -> ToolResponse:\n        \"\"\"View the historical plans.\"\"\"\n        historical_plans = await self.storage.get_plans()\n\n        plans_str = [\n            f\"\"\"Plan named '{_.name}':\n- ID: {_.id}\n- Created at: {_.created_at}\n- Description: {_.description}\n- State: {_.state}\n\"\"\"\n            for _ in historical_plans\n        ]\n\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=\"\\n\".join(plans_str),\n                ),\n            ],\n        )\n\n    async def recover_historical_plan(self, plan_id: str) -> ToolResponse:\n        \"\"\"Recover a historical plan by given plan ID, the plan ID can be\n        obtained by calling `view_historical_plans`. Note the recover\n        operation will override the current plan if exists.\n\n        Args:\n            plan_id (`str`):\n                The ID of the historical plan to be recovered.\n        \"\"\"\n        historical_plan = await self.storage.get_plan(plan_id)\n        if historical_plan is None:\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Cannot find the plan with ID '{plan_id}'.\",\n                    ),\n                ],\n            )\n\n        # Store the current plan into history if exists\n        if self.current_plan:\n            if self.current_plan.state != \"done\":\n                self.current_plan.finish(\n                    \"abandoned\",\n                    f\"The plan execution is interrupted by a new plan \"\n                    f\"with ID '{historical_plan.id}'.\",\n                )\n            await self.storage.add_plan(self.current_plan)\n            res = ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=(\n                            \"The current plan named \"\n                            f\"'{self.current_plan.name}' is replaced by the \"\n                            f\"historical plan named '{historical_plan.name}' \"\n                            f\"with ID '{historical_plan.id}'.\"\n                        ),\n                    ),\n                ],\n            )\n        else:\n            res = ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=(\n                            f\"Historical plan named '{historical_plan.name}' \"\n                            f\"with ID '{historical_plan.id}' is recovered \"\n                            \"successfully.\"\n                        ),\n                    ),\n                ],\n            )\n        self.current_plan = historical_plan\n        await self._trigger_plan_change_hooks()\n        return res\n\n    def list_tools(\n        self,\n    ) -> list[Callable[..., Coroutine[Any, Any, ToolResponse]]]:\n        \"\"\"List all tool functions provided to agent\n\n        Returns:\n            `list[Callable[..., ToolResponse]]`:\n                A list of all tool functions provided by the plan notebook to\n                the agent.\n        \"\"\"\n        return [\n            # subtask related tools\n            self.view_subtasks,\n            self.update_subtask_state,\n            self.finish_subtask,\n            # plan related tools\n            self.create_plan,\n            self.revise_current_plan,\n            self.finish_plan,\n            # historical plan related tools\n            self.view_historical_plans,\n            self.recover_historical_plan,\n        ]\n\n    async def get_current_hint(self) -> Msg | None:\n        \"\"\"Get the hint message based on the current plan and subtasks states.\n        This function will call the `plan_to_hint` function to generate the\n        hint message.\n\n        Returns:\n            `Msg | None`:\n                The hint message wrapped by <system-hint></system-hint>, or\n                None if there is no relevant hint.\n        \"\"\"\n        hint_content = self.plan_to_hint(self.current_plan)\n        if hint_content:\n            msg = Msg(\n                \"user\",\n                hint_content,\n                \"user\",\n            )\n            return msg\n\n        return None\n\n    def register_plan_change_hook(\n        self,\n        hook_name: str,\n        hook: Callable[[\"PlanNotebook\", Plan], None]\n        | Callable[[\"PlanNotebook\", Plan], Awaitable[None]],\n    ) -> None:\n        \"\"\"Register a plan hook that will be triggered when the plan is\n        changed.\n\n        Args:\n            hook_name (`str`):\n                The name of the hook, should be unique.\n            hook (`Callable[[\"PlanNotebook\", Plan], None] | \\\n            Callable[[\"PlanNotebook\", Plan], Awaitable[None]]):\n                The hook function, which takes the current plan as input and\n                returns nothing.\n        \"\"\"\n        self._plan_change_hooks[hook_name] = hook\n\n    def remove_plan_change_hook(self, hook_name: str) -> None:\n        \"\"\"Remove a plan change hook by given name.\n\n        Args:\n            hook_name (`str`):\n                The name of the hook to be removed.\n        \"\"\"\n        if hook_name in self._plan_change_hooks:\n            self._plan_change_hooks.pop(hook_name)\n        else:\n            raise ValueError(f\"Hook '{hook_name}' not found.\")\n\n    async def _trigger_plan_change_hooks(self) -> None:\n        \"\"\"Trigger all the plan change hooks.\"\"\"\n        for hook in self._plan_change_hooks.values():\n            await _execute_async_or_sync_func(\n                hook,\n                self,\n                self.current_plan,\n            )\n"
  },
  {
    "path": "src/agentscope/plan/_storage_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The base class for plan storage.\"\"\"\nfrom abc import abstractmethod\n\nfrom agentscope.module import StateModule\nfrom agentscope.plan._plan_model import Plan\n\n\nclass PlanStorageBase(StateModule):\n    \"\"\"The base class for plan storage.\"\"\"\n\n    @abstractmethod\n    async def add_plan(self, plan: Plan) -> None:\n        \"\"\"Add a plan to the storage.\"\"\"\n\n    @abstractmethod\n    async def delete_plan(self, plan_id: str) -> None:\n        \"\"\"Delete a plan from the storage.\"\"\"\n\n    @abstractmethod\n    async def get_plans(self) -> list[Plan]:\n        \"\"\"Get all plans from the storage.\"\"\"\n\n    @abstractmethod\n    async def get_plan(self, plan_id: str) -> Plan | None:\n        \"\"\"Get a plan by its ID.\"\"\"\n"
  },
  {
    "path": "src/agentscope/py.typed",
    "content": ""
  },
  {
    "path": "src/agentscope/rag/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The retrieval-augmented generation (RAG) module in AgentScope.\"\"\"\n\nfrom ._document import (\n    DocMetadata,\n    Document,\n)\nfrom ._reader import (\n    ReaderBase,\n    TextReader,\n    PDFReader,\n    ImageReader,\n    WordReader,\n    ExcelReader,\n    PowerPointReader,\n)\nfrom ._store import (\n    VDBStoreBase,\n    QdrantStore,\n    MilvusLiteStore,\n    OceanBaseStore,\n    MongoDBStore,\n    AlibabaCloudMySQLStore,\n)\nfrom ._knowledge_base import KnowledgeBase\nfrom ._simple_knowledge import SimpleKnowledge\n\n\n__all__ = [\n    \"ReaderBase\",\n    \"TextReader\",\n    \"PDFReader\",\n    \"ImageReader\",\n    \"WordReader\",\n    \"ExcelReader\",\n    \"PowerPointReader\",\n    \"DocMetadata\",\n    \"Document\",\n    \"VDBStoreBase\",\n    \"QdrantStore\",\n    \"MilvusLiteStore\",\n    \"OceanBaseStore\",\n    \"MongoDBStore\",\n    \"AlibabaCloudMySQLStore\",\n    \"KnowledgeBase\",\n    \"SimpleKnowledge\",\n]\n"
  },
  {
    "path": "src/agentscope/rag/_document.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The document data structure used in RAG as the data chunk and\nretrieval result.\"\"\"\nfrom dataclasses import dataclass, field\n\nimport shortuuid\nfrom dashscope.api_entities.dashscope_response import DictMixin\n\nfrom ..message import (\n    TextBlock,\n    ImageBlock,\n    VideoBlock,\n)\nfrom ..types import Embedding\n\n\n@dataclass\nclass DocMetadata(DictMixin):\n    \"\"\"The metadata of the document.\"\"\"\n\n    content: TextBlock | ImageBlock | VideoBlock\n    \"\"\"The data content, e.g., text, image, video.\"\"\"\n\n    doc_id: str\n    \"\"\"The document ID.\"\"\"\n\n    chunk_id: int\n    \"\"\"The chunk ID.\"\"\"\n\n    total_chunks: int\n    \"\"\"The total number of chunks.\"\"\"\n\n\n@dataclass\nclass Document:\n    \"\"\"The data chunk.\"\"\"\n\n    metadata: DocMetadata\n    \"\"\"The metadata of the data chunk.\"\"\"\n\n    id: str = field(default_factory=shortuuid.uuid)\n    \"\"\"The unique ID of the data chunk.\"\"\"\n\n    # The fields that will be filled when the document is added to or\n    # retrieved from the knowledge base.\n\n    embedding: Embedding | None = field(default_factory=lambda: None)\n    \"\"\"The embedding of the data chunk.\"\"\"\n\n    score: float | None = None\n    \"\"\"The relevance score of the data chunk.\"\"\"\n"
  },
  {
    "path": "src/agentscope/rag/_knowledge_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The knowledge base abstraction for retrieval-augmented generation (RAG).\"\"\"\nfrom abc import abstractmethod\nfrom typing import Any\n\nfrom ._reader import Document\nfrom ..embedding import EmbeddingModelBase\nfrom ._store import VDBStoreBase\nfrom ..message import TextBlock\nfrom ..tool import ToolResponse\n\n\nclass KnowledgeBase:\n    \"\"\"The knowledge base abstraction for retrieval-augmented generation\n    (RAG).\n\n    The ``retrieve`` and ``add_documents`` methods need to be implemented\n    in the subclasses. We also provide a quick method ``retrieve_knowledge``\n    that enables the agent to retrieve knowledge easily.\n    \"\"\"\n\n    embedding_store: VDBStoreBase\n    \"\"\"The embedding store for the knowledge base.\"\"\"\n\n    embedding_model: EmbeddingModelBase\n    \"\"\"The embedding model for the knowledge base.\"\"\"\n\n    def __init__(\n        self,\n        embedding_store: VDBStoreBase,\n        embedding_model: EmbeddingModelBase,\n    ) -> None:\n        \"\"\"Initialize the knowledge base.\"\"\"\n        self.embedding_store = embedding_store\n        self.embedding_model = embedding_model\n\n    @abstractmethod\n    async def retrieve(\n        self,\n        query: str,\n        limit: int = 5,\n        score_threshold: float | None = None,\n        **kwargs: Any,\n    ) -> list[Document]:\n        \"\"\"Retrieve relevant documents by the given query.\n\n        Args:\n            query (`str`):\n                The query string to retrieve relevant documents.\n            limit (`int`, defaults to 5):\n                The number of relevant documents to retrieve.\n            score_threshold (`float | None`, defaults to `None`):\n                The score threshold to filter the retrieved documents. If\n                provided, only documents with a score higher than the\n                threshold will be returned.\n            **kwargs (`Any`):\n                Other keyword arguments for the vector database search API.\n        \"\"\"\n\n    @abstractmethod\n    async def add_documents(\n        self,\n        documents: list[Document],\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Add documents to the knowledge base, which will embed the documents\n        and store them in the embedding store.\n\n        Args:\n            documents (`list[Document]`):\n                A list of documents to add.\n        \"\"\"\n\n    # A quick method that enable the agent to retrieve knowledge\n    # Developers can wrap the `retrieve` method by themselves to support\n    # more flexible usage\n    async def retrieve_knowledge(\n        self,\n        query: str,\n        limit: int = 5,\n        score_threshold: float | None = None,\n        **kwargs: Any,\n    ) -> ToolResponse:\n        \"\"\"Retrieve relevant documents from the knowledge base. Note the\n        `query` parameter is directly related to the retrieval quality, and\n        for the same question, you can try many different queries to get the\n        best results. Adjust the `limit` and `score_threshold` parameters\n        to get more or fewer results.\n\n        Args:\n            query (`str`):\n                The query string, which should be specific and concise. For\n                example, you should provide the specific name instead of\n                \"you\", \"my\", \"he\", \"she\", etc.\n            limit (`int`, defaults to 3):\n                The number of relevant documents to retrieve.\n            score_threshold (`float`, defaults to 0.8):\n                A threshold in [0, 1] and only the relevance score above this\n                threshold will be returned. Reduce this value to get more\n                results.\n        \"\"\"\n\n        docs = await self.retrieve(\n            query=query,\n            limit=limit,\n            score_threshold=score_threshold,\n            **kwargs,\n        )\n\n        if len(docs):\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Score: {_.score}, \"\n                        f\"Content: {_.metadata.content['text']}\",\n                    )\n                    for _ in docs\n                ],\n            )\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=\"No relevant documents found. TRY to reduce the \"\n                    \"`score_threshold` parameter to get \"\n                    \"more results.\",\n                ),\n            ],\n        )\n"
  },
  {
    "path": "src/agentscope/rag/_reader/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The reader abstraction for retrieval-augmented generation (RAG).\"\"\"\n\nfrom ._reader_base import ReaderBase, Document\nfrom ._text_reader import TextReader\nfrom ._pdf_reader import PDFReader\nfrom ._word_reader import WordReader\nfrom ._image_reader import ImageReader\nfrom ._excel_reader import ExcelReader\nfrom ._ppt_reader import PowerPointReader\n\n__all__ = [\n    \"Document\",\n    \"ReaderBase\",\n    \"TextReader\",\n    \"PDFReader\",\n    \"WordReader\",\n    \"ImageReader\",\n    \"ExcelReader\",\n    \"PowerPointReader\",\n]\n"
  },
  {
    "path": "src/agentscope/rag/_reader/_excel_reader.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=protected-access\n\"\"\"The Excel reader to read and chunk Excel files.\"\"\"\nimport base64\nimport hashlib\nimport json\nfrom typing import Any, Literal\n\nfrom ._reader_base import ReaderBase\nfrom ._text_reader import TextReader\nfrom ._utils import _get_media_type_from_data\nfrom .._document import Document, DocMetadata\nfrom ...message import ImageBlock, Base64Source, TextBlock\nfrom ..._logging import logger\n\n\ndef _get_excel_column_name(col_index: int) -> str:\n    \"\"\"Convert a 0-based column index to Excel column name (A, B, ..., Z, AA,\n    AB, ...).\n\n    Args:\n        col_index (`int`):\n            The 0-based column index.\n\n    Returns:\n        `str`:\n            The Excel column name (e.g., 'A' for 0, 'B' for 1, 'AA' for 26).\n    \"\"\"\n    result = \"\"\n    col_index += 1  # Convert to 1-based\n    while col_index > 0:\n        col_index -= 1\n        result = chr(ord(\"A\") + col_index % 26) + result\n        col_index //= 26\n    return result\n\n\ndef _extract_table_data(df: Any) -> list[list[str]]:\n    \"\"\"Extract table data from a DataFrame, handling NaN values.\n\n    Args:\n        df (`Any`):\n            The pandas DataFrame object.\n\n    Returns:\n        `list[list[str]]`:\n            Table data represented as a 2D list, where each inner list\n            represents a row, and each string in the row represents a cell.\n    \"\"\"\n    import pandas as pd\n\n    table_data = []\n    for _, row in df.iterrows():\n        row_data = []\n        for cell_val in row:\n            # Convert NaN to empty string, preserve line breaks\n            if pd.isna(cell_val):\n                cell_text = \"\"\n            else:\n                cell_text = str(cell_val).strip()\n                # Normalize line breaks\n                cell_text = cell_text.replace(\"\\r\\n\", \"\\n\").replace(\"\\r\", \"\\n\")\n            row_data.append(cell_text)\n        table_data.append(row_data)\n\n    return table_data\n\n\ndef _extract_images_from_worksheet(\n    worksheet: Any,\n) -> list[tuple[int, ImageBlock]]:\n    \"\"\"Extract images from a worksheet with their row positions.\n\n    Args:\n        worksheet (`Any`):\n            The openpyxl worksheet object.\n\n    Returns:\n        `list[tuple[int, ImageBlock]]`:\n            A list of tuples containing (row_index, ImageBlock), where\n            row_index is 0-based. Empty if no images found.\n    \"\"\"\n    images = []\n\n    if not (hasattr(worksheet, \"_images\") and worksheet._images):\n        return images\n\n    for img in worksheet._images:\n        try:\n            # Get image row position (0-based)\n            row_index = 0\n            if hasattr(img, \"anchor\") and hasattr(img.anchor, \"_from\"):\n                row_index = img.anchor._from.row\n\n            # Get image data\n            img_data = img._data()\n\n            # Determine media type\n            media_type = _get_media_type_from_data(img_data)\n\n            # Convert to base64\n            base64_data = base64.b64encode(img_data).decode(\"utf-8\")\n\n            image_block = ImageBlock(\n                type=\"image\",\n                source=Base64Source(\n                    type=\"base64\",\n                    media_type=media_type,\n                    data=base64_data,\n                ),\n            )\n\n            images.append((row_index, image_block))\n        except Exception as e:\n            logger.warning(\"Failed to extract image from worksheet: %s\", e)\n\n    return images\n\n\nclass ExcelReader(ReaderBase):\n    \"\"\"The Excel reader that supports reading text, image, and table\n    content from Excel files (.xlsx, .xls files), and chunking the text\n    content into smaller pieces.\n\n    .. note:: The table content can be extracted in Markdown or JSON format.\n\n        **Markdown format example** (``include_cell_coordinates=False``):\n\n        .. code-block:: text\n\n            | Name  | Age | City     |\n            |-------|-----|----------|\n            | Alice | 25  | New York |\n            | Bob   | 30  | London   |\n\n        **Markdown format example** (``include_cell_coordinates=True``):\n\n        .. code-block:: text\n\n            | [A1] Name  | [B1] Age | [C1] City     |\n            |------------|----------|---------------|\n            | [A2] Alice | [B2] 25  | [C2] New York |\n            | [A3] Bob   | [B3] 30  | [C3] London   |\n\n        **JSON format example** (``include_cell_coordinates=False``):\n\n        .. code-block:: json\n\n            [\"Name\", \"Age\", \"City\"]\n            [\"Alice\", \"25\", \"New York\"]\n            [\"Bob\", \"30\", \"London\"]\n\n        **JSON format example** (``include_cell_coordinates=True``):\n\n        .. code-block:: json\n\n            {\"A1\": \"Name\", \"B1\": \"Age\", \"C1\": \"City\"}\n            {\"A2\": \"Alice\", \"B2\": \"25\", \"C2\": \"New York\"}\n            {\"A3\": \"Bob\", \"B3\": \"30\", \"C3\": \"London\"}\n    \"\"\"\n\n    def __init__(\n        self,\n        chunk_size: int = 512,\n        split_by: Literal[\"char\", \"sentence\", \"paragraph\"] = \"sentence\",\n        include_sheet_names: bool = True,\n        include_cell_coordinates: bool = False,\n        include_image: bool = False,\n        separate_sheet: bool = False,\n        separate_table: bool = False,\n        table_format: Literal[\"markdown\", \"json\"] = \"markdown\",\n    ) -> None:\n        \"\"\"Initialize the Excel reader.\n\n        Args:\n            chunk_size (`int`, default to 512):\n                The size of each chunk, in number of characters.\n            split_by (`Literal[\"char\", \"sentence\", \"paragraph\"]`, default to \\\n            \"sentence\"):\n                The unit to split the text, can be \"char\", \"sentence\", or\n                \"paragraph\". The \"sentence\" option is implemented using the\n                \"nltk\" library, which only supports English text.\n            include_sheet_names (`bool`, default to True):\n                Whether to include sheet names in the extracted text.\n            include_cell_coordinates (`bool`, default to False):\n                Whether to include cell coordinates (e.g., A1, B2) in the\n                extracted text.\n            include_image (`bool`, default to False):\n                Whether to include image content in the document. If True,\n                images will be extracted and included as base64-encoded images.\n            separate_sheet (`bool`, default to False):\n                Whether to treat each sheet as a separate document. If True,\n                each sheet will be extracted as a separate Document object\n                instead of being merged together.\n            separate_table (`bool`, default to False):\n                If True, tables will be treated as a new chunk to avoid\n                truncation. But note when the table exceeds the chunk size,\n                it will still be truncated.\n            table_format (`Literal[\"markdown\", \"json\"]`, \\\n            default to \"markdown\"):\n                The format to extract table content. Note if the table cell\n                contains `\\n`, the Markdown format may not render correctly.\n                In that case, you can use the `json` format, which extracts\n                the table as a JSON string of a `list[list[str]]` object.\n        \"\"\"\n        self._validate_init_params(chunk_size, split_by)\n\n        if table_format not in [\"markdown\", \"json\"]:\n            raise ValueError(\n                \"The table_format must be one of 'markdown' or 'json', \"\n                f\"got {table_format}\",\n            )\n\n        self.chunk_size = chunk_size\n        self.split_by = split_by\n        self.include_sheet_names = include_sheet_names\n        self.include_cell_coordinates = include_cell_coordinates\n        self.include_image = include_image\n        self.separate_sheet = separate_sheet\n        self.separate_table = separate_table\n        self.table_format = table_format\n\n        # Use TextReader to do the chunking\n        self._text_reader = TextReader(self.chunk_size, self.split_by)\n\n    def _validate_init_params(self, chunk_size: int, split_by: str) -> None:\n        \"\"\"Validate initialization parameters.\n\n        Args:\n            chunk_size (`int`):\n                The chunk size to validate.\n            split_by (`str`):\n                The split mode to validate.\n        \"\"\"\n        if chunk_size <= 0:\n            raise ValueError(\n                f\"The chunk_size must be positive, got {chunk_size}\",\n            )\n\n        if split_by not in [\"char\", \"sentence\", \"paragraph\"]:\n            raise ValueError(\n                \"The split_by must be one of 'char', 'sentence' or \"\n                f\"'paragraph', got {split_by}\",\n            )\n\n    async def __call__(\n        self,\n        excel_path: str,\n    ) -> list[Document]:\n        \"\"\"Read an Excel file, split it into chunks, and return a list of\n        Document objects. The text, image, and table content will be returned\n        in the same order as they appear in the Excel file.\n\n        Args:\n            excel_path (`str`):\n                The input Excel file path (.xlsx or .xls file).\n\n        Returns:\n            `list[Document]`:\n                A list of Document objects, where the metadata contains the\n                chunked text, doc id and chunk id.\n        \"\"\"\n        # Generate document ID\n        doc_id = self.get_doc_id(excel_path)\n\n        # Load Excel file and workbook\n        excel_file = None\n        workbook = None\n\n        try:\n            import pandas as pd\n        except ImportError as e:\n            raise ImportError(\n                \"Please install pandas to use the Excel reader. \"\n                \"You can install it by `pip install pandas`.\",\n            ) from e\n\n        try:\n            excel_file = pd.ExcelFile(excel_path)\n\n            # Load workbook if images are needed\n            if self.include_image:\n                try:\n                    from openpyxl import load_workbook\n\n                    workbook = load_workbook(excel_path)\n                except ImportError:\n                    logger.warning(\n                        \"openpyxl not available, image extraction disabled\",\n                    )\n                    workbook = None\n\n            # Process sheets\n            if self.separate_sheet:\n                return await self._process_sheets_separately(\n                    excel_file,\n                    doc_id,\n                    workbook,\n                )\n            else:\n                return await self._process_sheets_merged(\n                    excel_file,\n                    doc_id,\n                    workbook,\n                )\n\n        except (\n            pd.errors.EmptyDataError,\n            pd.errors.ParserError,\n            FileNotFoundError,\n            PermissionError,\n        ) as e:\n            raise ValueError(\n                f\"Failed to read Excel file {excel_path}: {e}\",\n            ) from e\n        finally:\n            # Ensure all resources are closed\n            if workbook is not None:\n                workbook.close()\n            if excel_file is not None:\n                excel_file.close()\n\n    async def _process_sheets_merged(\n        self,\n        excel_file: Any,\n        doc_id: str,\n        workbook: Any = None,\n    ) -> list[Document]:\n        \"\"\"Process all sheets as a merged document, maintaining order of\n        text, table, and image content.\n\n        Args:\n            excel_file (`Any`):\n                The pandas ExcelFile object.\n            doc_id (`str`):\n                The document ID.\n            workbook (`Any`, optional):\n                The openpyxl workbook if available.\n\n        Returns:\n            `list[Document]`:\n                A list of Document objects from all sheets merged together,\n                maintaining content order.\n        \"\"\"\n        # Get all blocks from all sheets in order\n        all_blocks = []\n        for sheet_name in excel_file.sheet_names:\n            sheet_blocks = self._get_sheet_blocks(\n                excel_file,\n                sheet_name,\n                workbook,\n            )\n            all_blocks.extend(sheet_blocks)\n\n        # Convert blocks to documents\n        return await self._blocks_to_documents(all_blocks, doc_id)\n\n    async def _process_sheets_separately(\n        self,\n        excel_file: Any,\n        doc_id: str,\n        workbook: Any = None,\n    ) -> list[Document]:\n        \"\"\"Process each sheet as separate documents.\n\n        Args:\n            excel_file (`Any`):\n                The pandas ExcelFile object.\n            doc_id (`str`):\n                The document ID.\n            workbook (`Any`, optional):\n                The openpyxl workbook if available.\n\n        Returns:\n            `list[Document]`:\n                A list of Document objects with each sheet processed\n                separately.\n        \"\"\"\n        all_docs = []\n\n        for sheet_name in excel_file.sheet_names:\n            sheet_blocks = self._get_sheet_blocks(\n                excel_file,\n                sheet_name,\n                workbook,\n            )\n            sheet_docs = await self._blocks_to_documents(sheet_blocks, doc_id)\n            all_docs.extend(sheet_docs)\n\n        return all_docs\n\n    def _get_sheet_blocks(\n        self,\n        excel_file: Any,\n        sheet_name: str,\n        workbook: Any = None,\n    ) -> list[TextBlock | ImageBlock]:\n        \"\"\"Extract all data blocks from a sheet in order (text, table, image).\n\n        Args:\n            excel_file (`Any`):\n                The pandas ExcelFile object.\n            sheet_name (`str`):\n                The name of the sheet.\n            workbook (`Any`, optional):\n                The openpyxl workbook if available.\n\n        Returns:\n            `list[TextBlock | ImageBlock]`:\n                A list of data blocks extracted from the sheet, maintaining\n                the order they appear in the sheet based on row positions.\n        \"\"\"\n        blocks: list[TextBlock | ImageBlock] = []\n        positioned_blocks: list[tuple[int, TextBlock | ImageBlock, str]] = []\n\n        # Add sheet header\n        sheet_header = (\n            f\"Sheet: {sheet_name}\" if self.include_sheet_names else None\n        )\n\n        try:\n            df = excel_file.parse(sheet_name=sheet_name)\n\n            if df.empty:\n                return blocks\n\n            # Extract images with their row positions if enabled\n            images_with_positions: list[tuple[int, ImageBlock]] = []\n            if self.include_image and workbook is not None:\n                try:\n                    worksheet = workbook[sheet_name]\n                    images_with_positions = _extract_images_from_worksheet(\n                        worksheet,\n                    )\n                except Exception as e:\n                    logger.warning(\n                        \"Failed to extract images from sheet '%s': %s\",\n                        sheet_name,\n                        e,\n                    )\n\n            # Extract table data\n            table_data = _extract_table_data(df)\n\n            if self.table_format == \"markdown\":\n                table_text = self._table_to_markdown(table_data, sheet_header)\n            else:\n                table_text = self._table_to_json(table_data, sheet_header)\n\n            # Calculate table row position for sorting\n            # Row 0 is the header row in pandas (if header exists)\n            # Table data spans from row 0 to row len(df)\n            # In Excel, this is typically row 1 to row (len(df) + 1) in\n            # 1-based indexing\n            # In 0-based indexing used by openpyxl: row 0 to row len(df)\n            table_start_row = 0\n\n            # Create table block\n            table_block = TextBlock(\n                type=\"text\",\n                text=table_text,\n            )\n\n            # Add table block with its position for sorting\n            positioned_blocks.append((table_start_row, table_block, \"table\"))\n\n            # Add image blocks with their positions\n            for row_index, image_block in images_with_positions:\n                positioned_blocks.append((row_index, image_block, \"image\"))\n\n            # Sort blocks by row position\n            positioned_blocks.sort(key=lambda x: x[0])\n\n            # Extract blocks in sorted order and merge consecutive blocks\n            # if needed\n            last_type = None\n            for row_index, block, block_type in positioned_blocks:\n                if block_type == \"table\":\n                    # Handle table block merging based on separate_table\n                    # Logic matches WordReader: merge if not separate_table and\n                    # last_type is \"text\" or \"table\"\n                    if not self.separate_table and last_type in [\n                        \"text\",\n                        \"table\",\n                    ]:\n                        blocks[-1][\"text\"] += \"\\n\" + block[\"text\"]\n                    else:\n                        blocks.append(block)\n                    last_type = \"table\"\n                elif block_type == \"image\":\n                    blocks.append(block)\n                    last_type = \"image\"\n\n        except Exception as e:\n            logger.warning(\"Failed to process sheet '%s': %s\", sheet_name, e)\n\n        return blocks\n\n    async def _blocks_to_documents(\n        self,\n        blocks: list[TextBlock | ImageBlock],\n        doc_id: str,\n    ) -> list[Document]:\n        \"\"\"Convert data blocks to Document objects.\n\n        Args:\n            blocks (`list[TextBlock | ImageBlock]`):\n                A list of data blocks.\n            doc_id (`str`):\n                The document ID.\n\n        Returns:\n            `list[Document]`:\n                A list of Document objects.\n        \"\"\"\n        documents = []\n\n        for block in blocks:\n            if block[\"type\"] == \"text\":\n                # Process text blocks through TextReader for chunking\n                text_docs = await self._text_reader(block[\"text\"])\n                for doc in text_docs:\n                    # Update doc_id but keep other metadata\n                    doc.metadata.doc_id = doc_id\n                    doc.id = doc_id\n                    documents.append(doc)\n            elif block[\"type\"] == \"image\":\n                # Images are independent documents\n                documents.append(\n                    Document(\n                        metadata=DocMetadata(\n                            content=block,\n                            doc_id=doc_id,\n                            chunk_id=0,  # Will be set later\n                            total_chunks=1,\n                        ),\n                    ),\n                )\n\n        # Set chunk ids and total chunks\n        total_chunks = len(documents)\n        for idx, doc in enumerate(documents):\n            doc.metadata.chunk_id = idx\n            doc.metadata.total_chunks = total_chunks\n\n        return documents\n\n    def _table_to_markdown(\n        self,\n        table_data: list[list[str]],\n        sheet_header: str | None = None,\n    ) -> str:\n        \"\"\"Convert table data to Markdown format.\n\n        Args:\n            table_data (`list[list[str]]`):\n                Table data represented as a 2D list.\n            sheet_header (`str | None`, optional):\n                Optional sheet header to prepend.\n\n        Returns:\n            `str`:\n                Table in Markdown format.\n        \"\"\"\n        if not table_data:\n            return sheet_header or \"\"\n\n        md_table = \"\"\n\n        # Add sheet header if provided\n        if sheet_header:\n            md_table += sheet_header + \"\\n\"\n\n        # If no rows, return header only\n        if not table_data or not table_data[0]:\n            return md_table.strip() or \"\"\n\n        num_cols = len(table_data[0])\n\n        # Escape pipe characters in cells to avoid breaking Markdown table\n        # structure\n        def escape_pipes(cell_text: str) -> str:\n            \"\"\"Escape pipe characters in cell content.\"\"\"\n            return cell_text.replace(\"|\", \"\\\\|\")\n\n        def format_cell(cell: str, row_idx: int, col_idx: int) -> str:\n            \"\"\"Format cell content with optional coordinates.\"\"\"\n            escaped = escape_pipes(cell)\n            if self.include_cell_coordinates:\n                coord = f\"{_get_excel_column_name(col_idx)}{row_idx + 1}\"\n                return f\"[{coord}] {escaped}\"\n            return escaped\n\n        # Header row (first row)\n        escaped_header = [\n            format_cell(cell, 0, col_idx)\n            for col_idx, cell in enumerate(table_data[0])\n        ]\n        header_row = \"| \" + \" | \".join(escaped_header) + \" |\\n\"\n        md_table += header_row\n\n        # Separator row\n        separator_row = \"| \" + \" | \".join([\"---\"] * num_cols) + \" |\\n\"\n        md_table += separator_row\n\n        # Data rows\n        for row_idx, row in enumerate(table_data[1:], start=1):\n            # Ensure row has same number of columns as header\n            while len(row) < num_cols:\n                row.append(\"\")\n            # Format each cell with optional coordinates\n            formatted_row = [\n                format_cell(cell, row_idx, col_idx)\n                for col_idx, cell in enumerate(row[:num_cols])\n            ]\n            data_row = \"| \" + \" | \".join(formatted_row) + \" |\\n\"\n            md_table += data_row\n\n        return md_table\n\n    def _table_to_json(\n        self,\n        table_data: list[list[str]],\n        sheet_header: str | None = None,\n    ) -> str:\n        \"\"\"Convert table data to JSON string.\n\n        Args:\n            table_data (`list[list[str]]`):\n                Table data represented as a 2D list.\n            sheet_header (`str | None`, optional):\n                Optional sheet header to prepend.\n\n        Returns:\n            `str`:\n                Table in JSON string format.\n        \"\"\"\n        json_strs = []\n\n        # Add sheet header if provided\n        if sheet_header:\n            json_strs.append(sheet_header)\n\n        # Add system info marker\n        json_strs.append(\n            \"<system-info>A table loaded as a JSON array:</system-info>\",\n        )\n\n        for row_idx, row in enumerate(table_data):\n            if self.include_cell_coordinates:\n                # Include cell coordinates in the format {\"A1\": \"value\", ...}\n                row_dict = {\n                    f\"{_get_excel_column_name(col_idx)}{row_idx + 1}\": cell\n                    for col_idx, cell in enumerate(row)\n                }\n                json_strs.append(json.dumps(row_dict, ensure_ascii=False))\n            else:\n                json_strs.append(json.dumps(row, ensure_ascii=False))\n\n        return \"\\n\".join(json_strs)\n\n    def get_doc_id(self, excel_path: str) -> str:\n        \"\"\"Generate unique document ID from file path.\n\n        Args:\n            excel_path (`str`):\n                The path to the Excel file.\n\n        Returns:\n            `str`:\n                The document ID (SHA256 hash of the file path).\n        \"\"\"\n        return hashlib.sha256(excel_path.encode(\"utf-8\")).hexdigest()\n"
  },
  {
    "path": "src/agentscope/rag/_reader/_image_reader.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The Image reader modules\"\"\"\nimport hashlib\n\nfrom .. import DocMetadata\nfrom ...message import ImageBlock, URLSource\nfrom .._reader import ReaderBase, Document\n\n\nclass ImageReader(ReaderBase):\n    \"\"\"A simple image reader that wraps the image into a Document object.\n\n    This class is only a simple implementation to support multimodal RAG.\n    \"\"\"\n\n    async def __call__(self, image_url: str | list[str]) -> list[Document]:\n        \"\"\"Read an image and return the wrapped Document object.\n\n        Args:\n            image_url (`str | list[str]`):\n                The image URL(s) or path(s).\n\n        Returns:\n            `list[Document]`:\n                A list of Document objects containing the image data.\n        \"\"\"\n        # Read the image data and wrap it into a Document object.\n        if isinstance(image_url, str):\n            image_url = [image_url]\n\n        image_blocks: list[ImageBlock] = [\n            ImageBlock(\n                type=\"image\",\n                source=URLSource(\n                    type=\"url\",\n                    url=_,\n                ),\n            )\n            for _ in image_url\n        ]\n\n        doc_idx = [self.get_doc_id(_) for _ in image_url]\n\n        return [\n            Document(\n                metadata=DocMetadata(\n                    content=image_block,\n                    doc_id=doc_id,\n                    chunk_id=0,\n                    total_chunks=1,\n                ),\n            )\n            for doc_id, image_block in zip(doc_idx, image_blocks)\n        ]\n\n    def get_doc_id(self, image_path: str) -> str:\n        \"\"\"Generate a document ID based on the image path.\n\n        Args:\n            image_path (`str`):\n                The image path or URL.\n\n        Returns:\n            `str`:\n                The generated document ID.\n        \"\"\"\n        return hashlib.md5(image_path.encode(\"utf-8\")).hexdigest()\n"
  },
  {
    "path": "src/agentscope/rag/_reader/_pdf_reader.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The PDF reader to read and chunk PDF files.\"\"\"\nimport hashlib\nfrom typing import Literal\n\nfrom ._reader_base import ReaderBase\nfrom ._text_reader import TextReader\nfrom .._document import Document\n\n\nclass PDFReader(ReaderBase):\n    \"\"\"The PDF reader that splits text into chunks by a fixed chunk size.\"\"\"\n\n    def __init__(\n        self,\n        chunk_size: int = 512,\n        split_by: Literal[\"char\", \"sentence\", \"paragraph\"] = \"sentence\",\n    ) -> None:\n        \"\"\"Initialize the text reader.\n\n        Args:\n            chunk_size (`int`, default to 512):\n                The size of each chunk, in number of characters.\n            split_by (`Literal[\"char\", \"sentence\", \"paragraph\"]`, default to \\\n            \"sentence\"):\n                The unit to split the text, can be \"char\", \"sentence\", or\n                \"paragraph\". The \"sentence\" option is implemented using the\n                \"nltk\" library, which only supports English text.\n        \"\"\"\n        if chunk_size <= 0:\n            raise ValueError(\n                f\"The chunk_size must be positive, got {chunk_size}\",\n            )\n\n        if split_by not in [\"char\", \"sentence\", \"paragraph\"]:\n            raise ValueError(\n                \"The split_by must be one of 'char', 'sentence' or \"\n                f\"'paragraph', got {split_by}\",\n            )\n\n        self.chunk_size = chunk_size\n        self.split_by = split_by\n\n        # To avoid code duplication, we use TextReader to do the chunking.\n        self._text_reader = TextReader(\n            self.chunk_size,\n            self.split_by,\n        )\n\n    async def __call__(\n        self,\n        pdf_path: str,\n    ) -> list[Document]:\n        \"\"\"Read a PDF file, split it into chunks, and return a list of\n        Document objects.\n\n        Args:\n            pdf_path (`str`):\n                The input PDF file path.\n        \"\"\"\n        try:\n            from pypdf import PdfReader\n        except ImportError as e:\n            raise ImportError(\n                \"Please install pypdf to use the PDF reader. \"\n                \"You can install it by `pip install pypdf`.\",\n            ) from e\n\n        reader = PdfReader(pdf_path)\n\n        gather_texts = []\n        for page in reader.pages:\n            gather_texts.append(page.extract_text())\n\n        doc_id = hashlib.sha256(pdf_path.encode(\"utf-8\")).hexdigest()\n\n        docs = await self._text_reader(\"\\n\\n\".join(gather_texts))\n        for doc in docs:\n            doc.id = doc_id\n\n        return docs\n\n    def get_doc_id(self, pdf_path: str) -> str:\n        \"\"\"Get the document ID. This function can be used to check if the\n        doc_id already exists in the knowledge base.\"\"\"\n        return hashlib.sha256(pdf_path.encode(\"utf-8\")).hexdigest()\n"
  },
  {
    "path": "src/agentscope/rag/_reader/_ppt_reader.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The PowerPoint reader to read and chunk PowerPoint presentations.\"\"\"\nimport base64\nimport hashlib\nfrom typing import Any, Literal\n\nfrom ._reader_base import ReaderBase\nfrom ._text_reader import TextReader\nfrom ._utils import (\n    _get_media_type_from_data,\n    _table_to_json,\n    _table_to_markdown,\n)\nfrom .._document import Document, DocMetadata\nfrom ...message import ImageBlock, Base64Source, TextBlock\nfrom ..._logging import logger\n\n\ndef _extract_table_data(table: Any) -> list[list[str]]:\n    \"\"\"Extract table data from a PowerPoint table.\n\n    Args:\n        table (`Any`):\n            The table object from python-pptx.\n\n    Returns:\n        `list[list[str]]`:\n            Table data represented as a 2D list, where each inner list\n            represents a row, and each string in the row represents a cell.\n    \"\"\"\n    table_data = []\n    for row in table.rows:\n        row_data = []\n        for cell in row.cells:\n            # Extract text from cell, preserving line breaks within cells\n            cell_text = cell.text.strip()\n            # Replace line breaks with \\n to preserve structure\n            cell_text = cell_text.replace(\"\\r\\n\", \"\\n\").replace(\"\\r\", \"\\n\")\n            row_data.append(cell_text)\n        table_data.append(row_data)\n    return table_data\n\n\ndef _extract_images_from_shape(shape: Any) -> list[ImageBlock]:\n    \"\"\"Extract images from a shape (if it contains images).\n\n    Args:\n        shape (`Any`):\n            The shape object from python-pptx.\n\n    Returns:\n        `list[ImageBlock]`:\n            A list of ImageBlock objects, empty if no images found.\n    \"\"\"\n    images = []\n\n    # Check if shape is a picture\n    try:\n        from pptx.enum.shapes import MSO_SHAPE_TYPE\n\n        picture_type = MSO_SHAPE_TYPE.PICTURE\n    except ImportError:\n        picture_type = 13  # MSO_SHAPE_TYPE.PICTURE fallback\n\n    if shape.shape_type == picture_type:\n        try:\n            # Get image data\n            image_data = shape.image.blob\n\n            # Determine media type\n            media_type = _get_media_type_from_data(image_data)\n\n            # Convert to base64\n            base64_data = base64.b64encode(image_data).decode(\"utf-8\")\n\n            images.append(\n                ImageBlock(\n                    type=\"image\",\n                    source=Base64Source(\n                        type=\"base64\",\n                        media_type=media_type,\n                        data=base64_data,\n                    ),\n                ),\n            )\n        except Exception as e:\n            logger.warning(\"Failed to extract image from shape: %s\", e)\n\n    return images\n\n\nclass PowerPointReader(ReaderBase):\n    \"\"\"The PowerPoint reader that supports reading text, image, and table\n    content from PowerPoint presentations (.pptx files), and chunking the text\n    content into smaller pieces.\n\n    .. note:: The table content can be extracted in Markdown or JSON format.\n    \"\"\"\n\n    def __init__(\n        self,\n        chunk_size: int = 512,\n        split_by: Literal[\"char\", \"sentence\", \"paragraph\"] = \"sentence\",\n        include_image: bool = True,\n        separate_slide: bool = False,\n        separate_table: bool = False,\n        table_format: Literal[\"markdown\", \"json\"] = \"markdown\",\n        slide_prefix: str | None = \"<slide index={index}>\",\n        slide_suffix: str | None = \"</slide>\",\n    ) -> None:\n        \"\"\"Initialize the PowerPoint reader.\n\n        Args:\n            chunk_size (`int`, default to 512):\n                The size of each chunk, in number of characters.\n            split_by (`Literal[\"char\", \"sentence\", \"paragraph\"]`, default to \\\n            \"sentence\"):\n                The unit to split the text, can be \"char\", \"sentence\", or\n                \"paragraph\". The \"sentence\" option is implemented using the\n                \"nltk\" library, which only supports English text.\n            include_image (`bool`, default to True):\n                Whether to include image content in the document. If True,\n                images will be extracted and included as base64-encoded images.\n            separate_slide (`bool`, default to False):\n                Whether to treat each slide as a separate document. If True,\n                each slide will be extracted as a separate Document object\n                instead of being merged together.\n            separate_table (`bool`, default to False):\n                If True, tables will be treated as a new chunk to avoid\n                truncation. But note when the table exceeds the chunk size,\n                it will still be truncated.\n            table_format (`Literal[\"markdown\", \"json\"]`, \\\n             default to \"markdown\"):\n                The format to extract table content. Note if the table cell\n                contains `\\n`, the Markdown format may not render correctly.\n                In that case, you can use the `json` format, which extracts\n                the table as a JSON string of a `list[list[str]]` object.\n            slide_prefix (`str`, default to `<slide index={index}>`):\n                Optional prefix to add before each slide's content. Supports\n                `{index}` placeholder for 1-based slide number. For example,\n                `\"<slide index={index}>\"` will produce `\"<slide index=1>\"` for\n                the first slide. If None, no prefix is added.\n            slide_suffix (`str`, default to `</slide>`):\n                Optional suffix to add after each slide's content. For example,\n                `\"</slide>\"`. If None, no suffix is added.\n        \"\"\"\n        self._validate_init_params(chunk_size, split_by)\n\n        if table_format not in [\"markdown\", \"json\"]:\n            raise ValueError(\n                \"The table_format must be one of 'markdown' or 'json', \"\n                f\"got {table_format}\",\n            )\n\n        self.chunk_size = chunk_size\n        self.split_by = split_by\n        self.include_image = include_image\n        self.separate_slide = separate_slide\n        self.separate_table = separate_table\n        self.table_format = table_format\n        self.slide_prefix = slide_prefix\n        self.slide_suffix = slide_suffix\n\n        # Use TextReader to do the chunking\n        self._text_reader = TextReader(self.chunk_size, self.split_by)\n\n    def _validate_init_params(self, chunk_size: int, split_by: str) -> None:\n        \"\"\"Validate initialization parameters.\n\n        Args:\n            chunk_size (`int`):\n                The chunk size to validate.\n            split_by (`str`):\n                The split mode to validate.\n        \"\"\"\n        if chunk_size <= 0:\n            raise ValueError(\n                f\"The chunk_size must be positive, got {chunk_size}\",\n            )\n\n        if split_by not in [\"char\", \"sentence\", \"paragraph\"]:\n            raise ValueError(\n                \"The split_by must be one of 'char', 'sentence' or \"\n                f\"'paragraph', got {split_by}\",\n            )\n\n    async def __call__(\n        self,\n        ppt_path: str,\n    ) -> list[Document]:\n        \"\"\"Read a PowerPoint file, split it into chunks, and return a list of\n        Document objects. The text, image, and table content will be returned\n        in the same order as they appear in the PowerPoint presentation.\n\n        Args:\n            ppt_path (`str`):\n                The input PowerPoint file path (.pptx file).\n\n        Returns:\n            `list[Document]`:\n                A list of Document objects, where the metadata contains the\n                chunked text, doc id and chunk id.\n        \"\"\"\n        # Generate document ID\n        doc_id = self.get_doc_id(ppt_path)\n\n        # Load PowerPoint presentation\n        try:\n            from pptx import Presentation\n\n            prs = Presentation(ppt_path)\n        except ImportError as e:\n            raise ImportError(\n                \"Please install python-pptx to use the PowerPoint reader. \"\n                \"You can install it by `pip install python-pptx`.\",\n            ) from e\n\n        # Process slides\n        if self.separate_slide:\n            return await self._process_slides_separately(prs, doc_id)\n        else:\n            return await self._process_slides_merged(prs, doc_id)\n\n    async def _process_slides_merged(\n        self,\n        prs: Any,\n        doc_id: str,\n    ) -> list[Document]:\n        \"\"\"Process all slides as a merged document, maintaining order of\n        text, table, and image content.\n\n        Args:\n            prs (`Any`):\n                The python-pptx Presentation object.\n            doc_id (`str`):\n                The document ID.\n\n        Returns:\n            `list[Document]`:\n                A list of Document objects from all slides merged together,\n                maintaining content order.\n        \"\"\"\n        # Get all blocks from all slides in order\n        all_blocks = []\n        for slide_idx, slide in enumerate(prs.slides):\n            slide_blocks = self._get_slide_blocks(slide, slide_idx)\n            all_blocks.extend(slide_blocks)\n\n        # Convert blocks to documents\n        return await self._blocks_to_documents(all_blocks, doc_id)\n\n    async def _process_slides_separately(\n        self,\n        prs: Any,\n        doc_id: str,\n    ) -> list[Document]:\n        \"\"\"Process each slide as separate documents.\n\n        Args:\n            prs (`Any`):\n                The python-pptx Presentation object.\n            doc_id (`str`):\n                The document ID.\n\n        Returns:\n            `list[Document]`:\n                A list of Document objects with each slide processed\n                separately.\n        \"\"\"\n        all_docs = []\n\n        for slide_idx, slide in enumerate(prs.slides):\n            slide_blocks = self._get_slide_blocks(slide, slide_idx)\n            slide_docs = await self._blocks_to_documents(slide_blocks, doc_id)\n            all_docs.extend(slide_docs)\n\n        return all_docs\n\n    def _get_slide_blocks(\n        self,\n        slide: Any,\n        slide_idx: int,\n    ) -> list[TextBlock | ImageBlock]:\n        \"\"\"Extract all data blocks from a slide in order (text, table, image).\n\n        Args:\n            slide (`Any`):\n                The slide object from python-pptx.\n            slide_idx (`int`):\n                The index of the slide.\n\n        Returns:\n            `list[TextBlock | ImageBlock]`:\n                A list of data blocks extracted from the slide, maintaining\n                the order they appear in the slide.\n        \"\"\"\n        blocks: list[TextBlock | ImageBlock] = []\n        last_type = None\n\n        # Generate slide header from prefix if provided\n        slide_header = self._get_slide_header(slide_idx)\n\n        for shape in slide.shapes:\n            last_type = self._process_shape(\n                shape,\n                slide_idx,\n                blocks,\n                last_type,\n                slide_header,\n            )\n\n        # Add slide suffix to the last text block if provided\n        self._add_slide_suffix(blocks, slide_header)\n\n        return blocks\n\n    def _get_slide_header(self, slide_idx: int) -> str:\n        \"\"\"Generate slide header from prefix if provided.\n\n        Args:\n            slide_idx (`int`):\n                The index of the slide.\n\n        Returns:\n            `str`:\n                The slide header string, or empty string if no prefix.\n        \"\"\"\n        if self.slide_prefix is not None:\n            return self.slide_prefix.format(index=slide_idx + 1)\n        return \"\"\n\n    def _process_shape(\n        self,\n        shape: Any,\n        slide_idx: int,\n        blocks: list[TextBlock | ImageBlock],\n        last_type: str | None,\n        slide_header: str,\n    ) -> str | None:\n        \"\"\"Process a single shape and add its content to blocks.\n\n        Args:\n            shape (`Any`):\n                The shape object from python-pptx.\n            slide_idx (`int`):\n                The index of the slide.\n            blocks (`list[TextBlock | ImageBlock]`):\n                The list of blocks to add to.\n            last_type (`str | None`):\n                The type of the last block.\n            slide_header (`str`):\n                The slide header to prepend if this is the first block.\n\n        Returns:\n            `str | None`:\n                The updated last_type.\n        \"\"\"\n        shape_type, extracted_data = self._extract_shape_content(\n            shape,\n            slide_idx,\n        )\n\n        if not extracted_data:\n            return last_type\n\n        if shape_type == \"image\" and isinstance(extracted_data, list):\n            blocks.extend(extracted_data)\n            return \"image\"\n\n        if shape_type == \"table\" and isinstance(extracted_data, str):\n            return self._add_table_block(\n                blocks,\n                extracted_data,\n                last_type,\n                slide_header,\n            )\n\n        if shape_type == \"text\" and isinstance(extracted_data, str):\n            return self._add_text_block(\n                blocks,\n                extracted_data,\n                last_type,\n                slide_header,\n            )\n\n        return last_type\n\n    def _add_slide_suffix(\n        self,\n        blocks: list[TextBlock | ImageBlock],\n        slide_header: str,\n    ) -> None:\n        \"\"\"Add slide suffix to the last text block if provided.\n\n        Note: suffix can only be appended to text blocks since ImageBlock\n        doesn't have a text field.\n\n        Args:\n            blocks (`list[TextBlock | ImageBlock]`):\n                The list of blocks to modify.\n            slide_header (`str`):\n                The slide header to use if creating a new text block.\n        \"\"\"\n        if self.slide_suffix is None or not blocks:\n            return\n\n        # Find the last text block and append suffix\n        for i in range(len(blocks) - 1, -1, -1):\n            if blocks[i].get(\"type\") == \"text\":\n                blocks[i][\"text\"] += \"\\n\" + self.slide_suffix\n                return\n\n        # No text block found (slide contains only images),\n        # create a new text block for the suffix\n        suffix_text = (\n            slide_header + \"\\n\" + self.slide_suffix\n            if slide_header\n            else self.slide_suffix\n        )\n        blocks.append(TextBlock(type=\"text\", text=suffix_text))\n\n    def _extract_shape_content(\n        self,\n        shape: Any,\n        slide_idx: int,\n    ) -> tuple[str | None, list[ImageBlock] | str | None]:\n        \"\"\"Extract content from a shape (image, table, or text).\n\n        Args:\n            shape (`Any`):\n                The shape object from python-pptx.\n            slide_idx (`int`):\n                The index of the slide (for error logging).\n\n        Returns:\n            `tuple[str | None, list[ImageBlock] | str | None]`:\n                A tuple of (content_type, content_data).\n                content_type can be \"image\", \"table\", \"text\", or None.\n        \"\"\"\n        # Check for images first\n        if self.include_image:\n            shape_images = _extract_images_from_shape(shape)\n            if shape_images:\n                return (\"image\", shape_images)\n\n        # Check for tables\n        if hasattr(shape, \"has_table\") and shape.has_table:\n            try:\n                table_data = _extract_table_data(shape.table)\n                if self.table_format == \"markdown\":\n                    return (\"table\", _table_to_markdown(table_data))\n                return (\"table\", _table_to_json(table_data))\n            except Exception as e:\n                logger.warning(\n                    \"Failed to extract table from slide %d: %s\",\n                    slide_idx + 1,\n                    e,\n                )\n                return (None, None)\n\n        # Extract text from text frames\n        if hasattr(shape, \"has_text_frame\") and shape.has_text_frame:\n            try:\n                text_frame = shape.text_frame\n                text_parts = [\n                    para.text.strip()\n                    for para in text_frame.paragraphs\n                    if para.text.strip()\n                ]\n                if text_parts:\n                    return (\"text\", \"\\n\".join(text_parts))\n            except Exception as e:\n                logger.warning(\n                    \"Failed to extract text from shape in slide %d: %s\",\n                    slide_idx + 1,\n                    e,\n                )\n\n        return (None, None)\n\n    def _add_table_block(\n        self,\n        blocks: list[TextBlock | ImageBlock],\n        table_text: str,\n        last_type: str | None,\n        slide_header: str,\n    ) -> str:\n        \"\"\"Add a table block to the blocks list.\n\n        Args:\n            blocks (`list[TextBlock | ImageBlock]`):\n                The list of blocks to add to.\n            table_text (`str`):\n                The formatted table text.\n            last_type (`str | None`):\n                The type of the last block.\n            slide_header (`str`):\n                The slide header to prepend if this is the first block.\n\n        Returns:\n            `str`:\n                The updated last_type (\"table\").\n        \"\"\"\n        should_merge = (\n            not self.separate_table\n            and last_type in [\"text\", \"table\"]\n            and blocks\n        )\n\n        if should_merge:\n            blocks[-1][\"text\"] += \"\\n\" + table_text\n        else:\n            if last_type is None and slide_header:\n                table_text = slide_header + \"\\n\" + table_text\n            blocks.append(\n                TextBlock(\n                    type=\"text\",\n                    text=table_text,\n                ),\n            )\n\n        return \"table\"\n\n    def _add_text_block(\n        self,\n        blocks: list[TextBlock | ImageBlock],\n        text: str,\n        last_type: str | None,\n        slide_header: str,\n    ) -> str:\n        \"\"\"Add a text block to the blocks list.\n\n        Args:\n            blocks (`list[TextBlock | ImageBlock]`):\n                The list of blocks to add to.\n            text (`str`):\n                The text content.\n            last_type (`str | None`):\n                The type of the last block.\n            slide_header (`str`):\n                The slide header to prepend if this is the first block.\n\n        Returns:\n            `str`:\n                The updated last_type (\"text\").\n        \"\"\"\n        should_merge = (\n            last_type == \"text\"\n            or (last_type == \"table\" and not self.separate_table)\n        ) and blocks\n\n        if should_merge:\n            blocks[-1][\"text\"] += \"\\n\" + text\n        else:\n            if last_type is None and slide_header:\n                text = slide_header + \"\\n\" + text\n            blocks.append(\n                TextBlock(\n                    type=\"text\",\n                    text=text,\n                ),\n            )\n\n        return \"text\"\n\n    async def _blocks_to_documents(\n        self,\n        blocks: list[TextBlock | ImageBlock],\n        doc_id: str,\n    ) -> list[Document]:\n        \"\"\"Convert data blocks to Document objects.\n\n        Args:\n            blocks (`list[TextBlock | ImageBlock]`):\n                A list of data blocks.\n            doc_id (`str`):\n                The document ID.\n\n        Returns:\n            `list[Document]`:\n                A list of Document objects.\n        \"\"\"\n        documents = []\n\n        for block in blocks:\n            if block[\"type\"] == \"text\":\n                # Process text blocks through TextReader for chunking\n                for _ in await self._text_reader(block[\"text\"]):\n                    documents.append(\n                        Document(\n                            metadata=DocMetadata(\n                                content=_.metadata.content,\n                                doc_id=doc_id,\n                                # The chunk_id and total_chunks will be reset\n                                chunk_id=0,\n                                total_chunks=0,\n                            ),\n                        ),\n                    )\n            elif block[\"type\"] == \"image\":\n                # Images are independent documents\n                documents.append(\n                    Document(\n                        metadata=DocMetadata(\n                            content=block,\n                            doc_id=doc_id,\n                            chunk_id=0,  # Will be set later\n                            total_chunks=1,\n                        ),\n                    ),\n                )\n\n        # Set chunk ids and total chunks\n        total_chunks = len(documents)\n        for idx, doc in enumerate(documents):\n            doc.metadata.chunk_id = idx\n            doc.metadata.total_chunks = total_chunks\n\n        return documents\n\n    def get_doc_id(self, ppt_path: str) -> str:\n        \"\"\"Generate unique document ID from file path.\n\n        Args:\n            ppt_path (`str`):\n                The path to the PowerPoint file.\n\n        Returns:\n            `str`:\n                The document ID (SHA256 hash of the file path).\n        \"\"\"\n        return hashlib.sha256(ppt_path.encode(\"utf-8\")).hexdigest()\n"
  },
  {
    "path": "src/agentscope/rag/_reader/_reader_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The reader base class for retrieval-augmented generation (RAG).\"\"\"\nfrom abc import abstractmethod\nfrom typing import Any\n\nfrom .._document import Document\n\n\nclass ReaderBase:\n    \"\"\"The reader base class, which is responsible for reading the original\n    data, splitting it into chunks, and converting each chunk into a `Document`\n    object.\"\"\"\n\n    @abstractmethod\n    async def __call__(self, *args: Any, **kwargs: Any) -> list[Document]:\n        \"\"\"The async call function that takes the input files and returns the\n        vector records\"\"\"\n\n    @abstractmethod\n    def get_doc_id(self, *args: Any, **kwargs: Any) -> str:\n        \"\"\"Get a unique document ID for the input data. This method is to\n        expose the document ID generation logic to the developers\n\n        Returns:\n            `str`:\n                A unique document ID for the input data.\n        \"\"\"\n"
  },
  {
    "path": "src/agentscope/rag/_reader/_text_reader.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The text reader that reads text into vector records.\"\"\"\nimport hashlib\nimport os\nfrom typing import Literal\n\nfrom ._reader_base import ReaderBase, Document\nfrom .._document import DocMetadata\nfrom ..._logging import logger\nfrom ...message import TextBlock\n\n\nclass TextReader(ReaderBase):\n    \"\"\"The text reader that splits text into chunks by a fixed chunk size\n    and chunk overlap.\"\"\"\n\n    def __init__(\n        self,\n        chunk_size: int = 512,\n        split_by: Literal[\"char\", \"sentence\", \"paragraph\"] = \"sentence\",\n    ) -> None:\n        \"\"\"Initialize the text reader.\n\n        Args:\n            chunk_size (`int`, default to 512):\n                The size of each chunk, in number of characters.\n            split_by (`Literal[\"char\", \"paragraph\"]`, default to \\\n            \"sentence\"):\n                The unit to split the text, can be \"char\", \"sentence\", or\n                \"paragraph\". Note that \"sentence\" is implemented by \"nltk\"\n                library, which only supports English text.\n        \"\"\"\n        if chunk_size <= 0:\n            raise ValueError(\n                f\"The chunk_size must be positive, got {chunk_size}\",\n            )\n\n        if split_by not in [\"char\", \"sentence\", \"paragraph\"]:\n            raise ValueError(\n                \"The split_by must be one of 'char', 'sentence' or \"\n                f\"'paragraph', got {split_by}\",\n            )\n\n        self.chunk_size = chunk_size\n        self.split_by = split_by\n\n    async def __call__(\n        self,\n        text: str,\n    ) -> list[Document]:\n        \"\"\"Read a text string, split it into chunks, and return a list of\n        Document objects.\n\n        Args:\n            text (`str`):\n                The input text string, or a path to the local text file.\n\n        Returns:\n            `list[Document]`:\n                A list of Document objects, where the metadata contains the\n                chunked text, doc id and chunk id.\n        \"\"\"\n        if os.path.exists(text) and os.path.isfile(text):\n            logger.info(\"Reading text from local file: %s\", text)\n            with open(text, \"r\", encoding=\"utf-8\") as file:\n                text = file.read()\n\n        logger.info(\n            \"Reading text with chunk_size=%d, split_by=%s\",\n            self.chunk_size,\n            self.split_by,\n        )\n        splits = []\n        if self.split_by == \"char\":\n            # Split by character\n            for i in range(0, len(text), self.chunk_size):\n                start = max(0, i)\n                end = min(i + self.chunk_size, len(text))\n                splits.append(text[start:end])\n\n        elif self.split_by == \"sentence\":\n            try:\n                import nltk\n\n                nltk.download(\"punkt\", quiet=True)\n                nltk.download(\"punkt_tab\", quiet=True)\n            except ImportError as e:\n                raise ImportError(\n                    \"nltk is not installed. Please install it with \"\n                    \"`pip install nltk`.\",\n                ) from e\n\n            sentences = nltk.sent_tokenize(text)\n\n            # Handle the chunk_size for sentences\n            processed_sentences = []\n            for _ in sentences:\n                if len(_) <= self.chunk_size:\n                    processed_sentences.append(_)\n                else:\n                    # If the sentence itself exceeds chunk size, we need to\n                    # truncate it\n                    chunks = [\n                        _[j : j + self.chunk_size]\n                        for j in range(0, len(_), self.chunk_size)\n                    ]\n                    processed_sentences.extend(chunks)\n\n            splits.extend(processed_sentences)\n\n        elif self.split_by == \"paragraph\":\n            paragraphs = [_ for _ in text.split(\"\\n\") if len(_)]\n            for para in paragraphs:\n                if len(para) <= self.chunk_size:\n                    splits.append(para)\n\n                else:\n                    # If the paragraph itself exceeds chunk size, we need to\n                    # truncate it\n                    chunks = [\n                        para[k : k + self.chunk_size]\n                        for k in range(0, len(para), self.chunk_size)\n                    ]\n                    splits.extend(chunks)\n\n        logger.info(\n            \"Finished splitting the text into %d chunks.\",\n            len(splits),\n        )\n\n        doc_id = self.get_doc_id(text)\n\n        return [\n            Document(\n                id=doc_id,\n                metadata=DocMetadata(\n                    content=TextBlock(type=\"text\", text=_),\n                    doc_id=doc_id,\n                    chunk_id=idx,\n                    total_chunks=len(splits),\n                ),\n            )\n            for idx, _ in enumerate(splits)\n        ]\n\n    def get_doc_id(self, text: str) -> str:\n        \"\"\"Get the document ID. This function can be used to check if the\n        doc_id already exists in the knowledge base.\"\"\"\n        return hashlib.sha256(text.encode(\"utf-8\")).hexdigest()\n"
  },
  {
    "path": "src/agentscope/rag/_reader/_utils.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Utility functions for RAG readers.\"\"\"\nimport json\n\n\ndef _get_media_type_from_data(data: bytes) -> str:\n    \"\"\"Determine media type from image data.\n\n    Args:\n        data (`bytes`):\n            The raw image data.\n\n    Returns:\n        `str`:\n            The MIME type of the image (e.g., \"image/png\", \"image/jpeg\").\n    \"\"\"\n    # Image signature mapping\n    signatures = {\n        b\"\\x89PNG\\r\\n\\x1a\\n\": \"image/png\",\n        b\"\\xff\\xd8\": \"image/jpeg\",\n        b\"GIF87a\": \"image/gif\",\n        b\"GIF89a\": \"image/gif\",\n        b\"BM\": \"image/bmp\",\n    }\n\n    # Check signatures\n    for signature, media_type in signatures.items():\n        if data.startswith(signature):\n            return media_type\n\n    # Check WebP (RIFF at start + WEBP at offset 8)\n    if len(data) > 12 and data[:4] == b\"RIFF\" and data[8:12] == b\"WEBP\":\n        return \"image/webp\"\n\n    # Default to JPEG\n    return \"image/jpeg\"\n\n\ndef _table_to_json(table_data: list[list[str]]) -> str:\n    \"\"\"Convert table data to JSON string.\n\n    Args:\n        table_data (`list[list[str]]`):\n            Table data represented as a 2D list.\n\n    Returns:\n        `str`:\n            A JSON string representing the table as a 2D array,\n            prefixed with a system-info tag.\n    \"\"\"\n    json_str = json.dumps(table_data, ensure_ascii=False)\n    return (\n        \"<system-info>A table loaded as a JSON array:</system-info>\\n\"\n        + json_str\n    )\n\n\ndef _table_to_markdown(table_data: list[list[str]]) -> str:\n    \"\"\"Convert table data to Markdown format.\n\n    Args:\n        table_data (`list[list[str]]`):\n            Table data represented as a 2D list.\n\n    Returns:\n        `str`:\n            Table in Markdown format.\n    \"\"\"\n    if not table_data:\n        return \"\"\n\n    num_cols = len(table_data[0]) if table_data else 0\n    if num_cols == 0:\n        return \"\"\n\n    md_table = \"\"\n\n    # Header row\n    header_row = \"| \" + \" | \".join(table_data[0]) + \" |\\n\"\n    md_table += header_row\n\n    # Separator row\n    separator_row = \"| \" + \" | \".join([\"---\"] * num_cols) + \" |\\n\"\n    md_table += separator_row\n\n    # Data rows\n    for row in table_data[1:]:\n        # Ensure row has same number of columns as header\n        while len(row) < num_cols:\n            row.append(\"\")\n        data_row = \"| \" + \" | \".join(row[:num_cols]) + \" |\\n\"\n        md_table += data_row\n\n    return md_table\n"
  },
  {
    "path": "src/agentscope/rag/_reader/_word_reader.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=W0212\n\"\"\"The Word reader to read and chunk Word documents.\"\"\"\nimport base64\nimport hashlib\nfrom typing import Literal, TYPE_CHECKING\n\n\nfrom ._reader_base import ReaderBase\nfrom ._text_reader import TextReader\nfrom ._utils import _table_to_json, _table_to_markdown\nfrom .._document import Document, DocMetadata\nfrom ..._logging import logger\nfrom ...message import ImageBlock, Base64Source, TextBlock\n\nif TYPE_CHECKING:\n    from docx.table import Table as DocxTable\n    from docx.text.paragraph import Paragraph as DocxParagraph\nelse:\n    DocxTable = \"docx.table.Table\"\n    DocxParagraph = \"docx.text.paragraph.Paragraph\"\n\n# VML (Vector Markup Language) namespace URI.\n# Not registered in python-docx's default nsmap, so we define it here\n# instead of relying on qn(\"v:...\") which may fail in some versions.\n_VML_NS = \"{urn:schemas-microsoft-com:vml}\"\n\n\ndef _extract_text_from_paragraph(para: DocxParagraph) -> str:\n    \"\"\"Extract text from a paragraph, including text in text boxes and shapes.\n\n    Args:\n        para (`Paragraph`):\n            The paragraph object from which to extract text.\n\n\n    Returns:\n        `str`:\n            Extracted text\n    \"\"\"\n    text = \"\"\n\n    # Method 1: Extract all w:t elements directly from XML\n    #  (handles revisions, hyperlinks, etc.)\n    from docx.oxml.ns import qn\n\n    for t_elem in para._element.findall(\".//\" + qn(\"w:t\")):\n        if t_elem.text:\n            text += t_elem.text\n\n    # Method 2: If no text found, try standard text property\n    if not text:\n        text = para.text.strip()\n\n    # Method 3: If still no text, try to extract from text boxes and shapes\n    if not text:\n        # Check for text boxes (txbxContent)\n        txbx_contents = para._element.findall(\".//\" + qn(\"w:txbxContent\"))\n        for txbx in txbx_contents:\n            # Extract all text from paragraphs within the text box\n            for p_elem in txbx.findall(\".//\" + qn(\"w:p\")):\n                for t_elem in p_elem.findall(\".//\" + qn(\"w:t\")):\n                    if t_elem.text:\n                        text += t_elem.text\n\n        # Check for VML text boxes\n        vml_textboxes = para._element.findall(\".//\" + _VML_NS + \"textbox\")\n        for vml_tb in vml_textboxes:\n            for p_elem in vml_tb.findall(\".//\" + qn(\"w:p\")):\n                for t_elem in p_elem.findall(\".//\" + qn(\"w:t\")):\n                    if t_elem.text:\n                        text += t_elem.text\n\n    return text.strip()\n\n\ndef _extract_table_data(table: DocxTable) -> list[list[str]]:\n    \"\"\"Extract table data, handling merged cells and preserving line breaks\n    within cells.\n\n    Args:\n        table (`Table`):\n            The table object from which to extract data.\n\n    Returns:\n        `list[list[str]]`:\n            Table data represented as a 2D list.\n    \"\"\"\n\n    from docx.oxml.ns import qn\n\n    table_data = []\n    # Extract table cell elements directly from XML\n    for tr in table._element.findall(qn(\"w:tr\")):\n        row_data = []\n\n        tcs = tr.findall(qn(\"w:tc\"))\n        for tc in tcs:\n            # Extract paragraphs within the table cell (preserve line breaks)\n            paragraphs = []\n            for p_elem in tc.findall(qn(\"w:p\")):\n                # Obtain all text elements within the paragraph\n                texts = []\n                for t_elem in p_elem.findall(\".//\" + qn(\"w:t\")):\n                    if t_elem.text:\n                        texts.append(t_elem.text)\n\n                para_text = \"\".join(texts)\n                if para_text:\n                    # Only add non-empty paragraphs\n                    paragraphs.append(para_text)\n\n            # Use \\n to join multiple paragraphs\n            cell_text = \"\\n\".join(paragraphs)\n            row_data.append(cell_text)\n\n        table_data.append(row_data)\n\n    return table_data\n\n\ndef _extract_image_data(para: DocxParagraph) -> list[ImageBlock]:\n    \"\"\"Extract image data from a paragraph.\n\n    Args:\n        para (`Paragraph`):\n            The paragraph object from which to extract images.\n\n    Returns:\n        `list[ImageBlock]`:\n            A list of image blocks with base64-encoded image data\n    \"\"\"\n    images = []\n\n    from docx.oxml.ns import qn\n\n    # Method 1: Find all drawing elements (modern Word format)\n    drawings = para._element.findall(\".//\" + qn(\"w:drawing\"))\n\n    for drawing in drawings:\n        # Try to find blip elements (embedded images)\n        blips = drawing.findall(\".//\" + qn(\"a:blip\"))\n\n        for blip in blips:\n            # Get the relationship ID\n            embed = blip.get(qn(\"r:embed\"))\n\n            if embed:\n                try:\n                    # Get the image part from the document\n                    image_part = para.part.related_parts[embed]\n                    # Get the image binary data\n                    image_data = image_part.blob\n                    # Encode to base64\n                    image_base64 = base64.b64encode(image_data).decode(\"utf-8\")\n\n                    # Get image format from content type\n                    content_type = image_part.content_type\n\n                    images.append(\n                        ImageBlock(\n                            type=\"image\",\n                            source=Base64Source(\n                                type=\"base64\",\n                                data=image_base64,\n                                media_type=content_type,\n                            ),\n                        ),\n                    )\n                except Exception as e:\n                    logger.error(\n                        \"Failed to extract image: %s\",\n                        e,\n                    )\n\n    # Method 2: Check for pict elements (older Word format)\n    picts = para._element.findall(\".//\" + qn(\"w:pict\"))\n\n    for pict in picts:\n        imagedatas = pict.findall(\".//\" + _VML_NS + \"imagedata\")\n\n        for imagedata in imagedatas:\n            rel_id = imagedata.get(qn(\"r:id\"))\n\n            if rel_id:\n                try:\n                    image_part = para.part.related_parts[rel_id]\n                    image_data = image_part.blob\n                    image_base64 = base64.b64encode(image_data).decode(\"utf-8\")\n\n                    images.append(\n                        ImageBlock(\n                            type=\"image\",\n                            source=Base64Source(\n                                type=\"base64\",\n                                data=image_base64,\n                                media_type=image_part.content_type,\n                            ),\n                        ),\n                    )\n                except Exception as e:\n                    logger.error(\n                        \"Failed to extract image from pict: %s\",\n                        e,\n                    )\n    return images\n\n\nclass WordReader(ReaderBase):\n    \"\"\"The reader that supports reading text, image, and table content from\n    Word documents (.docx files), and chunking the text content into smaller\n    pieces.\n\n    .. note:: The table content can be extracted in Markdown or JSON format.\n\n    \"\"\"\n\n    def __init__(\n        self,\n        chunk_size: int = 512,\n        split_by: Literal[\"char\", \"sentence\", \"paragraph\"] = \"sentence\",\n        include_image: bool = True,\n        separate_table: bool = False,\n        table_format: Literal[\"markdown\", \"json\"] = \"markdown\",\n    ) -> None:\n        \"\"\"Initialize the Word reader.\n\n        Args:\n            chunk_size (`int`, default to 512):\n                The size of each chunk, in number of characters.\n            split_by (`Literal[\"char\", \"sentence\", \"paragraph\"]`, default to \\\n            \"sentence\"):\n                The unit to split the text, can be \"char\", \"sentence\", or\n                \"paragraph\". The \"sentence\" option is implemented using the\n                \"nltk\" library, which only supports English text.\n            include_image (`bool`, default to False):\n                Whether to include image content in the returned document. If\n                activated, the embedding model you use must support image\n                input, e.g. `DashScopeMultiModalEmbedding`.\n            separate_table (`bool`, default to False):\n                If True, tables will be treated as a new chunk to avoid\n                truncation. But note when the table exceeds the chunk size,\n                it will still be truncated.\n            table_format (`Literal[\"markdown\", \"json\"]`, \\\n            default to \"markdown\"):\n                The format to extract table content. Note if the table cell\n                contains `\\n`, the Markdown format may not render correctly.\n                In that case, you can use the `json` format, which extracts\n                the table as a JSON string of a `list[list[str]]` object.\n        \"\"\"\n        if chunk_size <= 0:\n            raise ValueError(\n                f\"The chunk_size must be positive, got {chunk_size}\",\n            )\n\n        if split_by not in [\"char\", \"sentence\", \"paragraph\"]:\n            raise ValueError(\n                \"The split_by must be one of 'char', 'sentence' or \"\n                f\"'paragraph', got {split_by}\",\n            )\n\n        if table_format not in [\"markdown\", \"json\"]:\n            raise ValueError(\n                \"The table_format must be one of 'markdown' or 'json', \"\n                f\"got {table_format}\",\n            )\n\n        self.chunk_size = chunk_size\n        self.split_by = split_by\n        self.include_image = include_image\n        self.separate_table = separate_table\n        self.table_format = table_format\n\n        # To avoid code duplication, we use TextReader to do the chunking.\n        self._text_reader = TextReader(\n            self.chunk_size,\n            self.split_by,\n        )\n\n    async def __call__(\n        self,\n        word_path: str,\n    ) -> list[Document]:\n        \"\"\"Read a Word document, split it into chunks, and return a list of\n        Document objects. The text, image, and table content will be returned\n        in the same order as they appear in the Word document.\n\n        Args:\n            word_path (`str`):\n                The input Word document file path (.docx file).\n\n        Returns:\n            `list[Document]`:\n                A list of Document objects, where the metadata contains the\n                chunked text, doc id and chunk id.\n        \"\"\"\n\n        blocks = self._get_data_blocks(word_path)\n\n        doc_id = self.get_doc_id(word_path)\n        documents = []\n        for block in blocks:\n            if block[\"type\"] == \"text\":\n                for _ in await self._text_reader(block[\"text\"]):\n                    documents.append(\n                        Document(\n                            metadata=DocMetadata(\n                                content=_.metadata.content,\n                                doc_id=doc_id,\n                                # The chunk_id and total_chunks will be reset\n                                chunk_id=0,\n                                total_chunks=0,\n                            ),\n                        ),\n                    )\n\n            elif block[\"type\"] == \"image\":\n                documents.append(\n                    Document(\n                        metadata=DocMetadata(\n                            content=block,\n                            doc_id=doc_id,\n                            chunk_id=0,\n                            total_chunks=1,\n                        ),\n                    ),\n                )\n\n        # Set chunk ids and total chunks\n        total_chunks = len(documents)\n        for idx, doc in enumerate(documents):\n            doc.metadata.chunk_id = idx\n            doc.metadata.total_chunks = total_chunks\n\n        return documents\n\n    def _get_data_blocks(self, word_path: str) -> list[TextBlock | ImageBlock]:\n        \"\"\"This function will return a list of dicts, each dict has a\n        'type' field indicating 'text', 'table', or 'image', and a\n        corresponding field containing the actual data.\n\n        Args:\n            word_path (`str`):\n                The input Word document file path (.docx file).\n\n        Returns:\n            `list[TextBlock | ImageBlock]`:\n                A list of data blocks extracted from the Word document.\n        \"\"\"\n        # Read the Word document\n        try:\n            from docx import Document as DocxDocument\n            from docx.oxml import CT_P, CT_Tbl\n            from docx.text.paragraph import Paragraph\n            from docx.table import Table\n            from docx.oxml.ns import qn\n\n        except ImportError as e:\n            raise ImportError(\n                \"Please install python-docx to use the Word reader. \"\n                \"You can install it by `pip install python-docx`.\",\n            ) from e\n\n        doc = DocxDocument(word_path)\n\n        # If the last block is a table\n        last_type = None\n\n        blocks: list[TextBlock | ImageBlock] = []\n        for element in doc.element.body:\n            if isinstance(element, CT_P):\n                para = Paragraph(element, doc)\n\n                # Extract the text\n                text = _extract_text_from_paragraph(para)\n\n                if self.include_image:\n                    # Check if the paragraph contains images\n                    has_drawing = bool(\n                        para._element.findall(\".//\" + qn(\"w:drawing\")),\n                    )\n                    has_pict = bool(\n                        para._element.findall(\".//\" + qn(\"w:pict\")),\n                    )\n\n                    if has_drawing or has_pict:\n                        # Extract the image\n                        blocks.extend(_extract_image_data(para))\n                        last_type = \"image\"\n\n                # For current text block:\n                # |   separate_table   |  True  | False  |\n                # |--------------------|--------|--------|\n                # | last_type == text  | append | append |\n                # | last_type == image |  new   |  new   |\n                # | last_type == table |  new   | append |\n                # | last_type == None  |  new   |  new   |\n                if (\n                    last_type == \"text\"\n                    or last_type == \"table\"\n                    and not self.separate_table\n                ):\n                    blocks[-1][\"text\"] += \"\\n\" + text\n                else:\n                    blocks.append(\n                        TextBlock(\n                            type=\"text\",\n                            text=text,\n                        ),\n                    )\n\n                # Update last type\n                last_type = \"text\"\n\n            elif isinstance(element, CT_Tbl):\n                # Extract the table data\n                table_data = _extract_table_data(Table(element, doc))\n\n                if self.table_format == \"markdown\":\n                    text = _table_to_markdown(table_data)\n                else:\n                    text = _table_to_json(table_data)\n\n                # For current table block:\n                # |   separate_table   |  True  | False  |\n                # |--------------------|--------|--------|\n                # | last_type == text  |  new   | append |\n                # | last_type == image |  new   |  new   |\n                # | last_type == table |  new   | append |\n                # | last_type == None  |  new   |  new   |\n                if not self.separate_table and last_type in [\"text\", \"table\"]:\n                    blocks[-1][\"text\"] += \"\\n\" + text\n                else:\n                    blocks.append(\n                        TextBlock(\n                            type=\"text\",\n                            text=text,\n                        ),\n                    )\n\n                last_type = \"table\"\n\n        return blocks\n\n    def get_doc_id(self, word_path: str) -> str:\n        \"\"\"Generate a document ID based on the Word file path.\n\n        Args:\n            word_path (`str`):\n                The Word file path.\n\n        Returns:\n            `str`:\n                The generated document ID.\n        \"\"\"\n        return hashlib.md5(word_path.encode(\"utf-8\")).hexdigest()\n"
  },
  {
    "path": "src/agentscope/rag/_simple_knowledge.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"A general implementation of the knowledge class in AgentScope RAG module.\"\"\"\nfrom typing import Any\n\nfrom ._reader import Document\nfrom ..message import TextBlock\nfrom ._knowledge_base import KnowledgeBase\n\n\nclass SimpleKnowledge(KnowledgeBase):\n    \"\"\"A simple knowledge base implementation.\"\"\"\n\n    async def retrieve(\n        self,\n        query: str,\n        limit: int = 5,\n        score_threshold: float | None = None,\n        **kwargs: Any,\n    ) -> list[Document]:\n        \"\"\"Retrieve relevant documents by the given queries.\n\n        Args:\n            query (`str`):\n                The query string to retrieve relevant documents.\n            limit (`int`, defaults to 5):\n                The number of relevant documents to retrieve.\n            score_threshold: float | None = None,\n                The threshold of the score to filter the results.\n            **kwargs (`Any`):\n                Other keyword arguments for the vector database search API.\n\n        Returns:\n            `list[Document]`:\n                A list of relevant documents.\n\n        TODO: handle the case when the query is too long.\n        \"\"\"\n        res_embedding = await self.embedding_model(\n            [\n                TextBlock(\n                    type=\"text\",\n                    text=query,\n                ),\n            ],\n        )\n        res = await self.embedding_store.search(\n            res_embedding.embeddings[0],\n            limit=limit,\n            score_threshold=score_threshold,\n            **kwargs,\n        )\n        return res\n\n    async def add_documents(\n        self,\n        documents: list[Document],\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Add documents to the knowledge\n\n        Args:\n            documents (`list[Document]`):\n                The list of documents to add.\n        \"\"\"\n        # Prepare the content to be embedded\n        for doc in documents:\n            if (\n                doc.metadata.content[\"type\"]\n                not in self.embedding_model.supported_modalities\n            ):\n                raise ValueError(\n                    f\"The embedding model {self.embedding_model.model_name} \"\n                    f\"does not support {doc.metadata.content['type']} data.\",\n                )\n\n        # Get the embeddings\n        res_embeddings = await self.embedding_model(\n            [_.metadata.content for _ in documents],\n        )\n\n        for doc, embedding in zip(documents, res_embeddings.embeddings):\n            doc.embedding = embedding\n\n        await self.embedding_store.add(documents)\n"
  },
  {
    "path": "src/agentscope/rag/_store/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The vector database store abstraction in AgentScope RAG module.\"\"\"\n\nfrom ._store_base import (\n    VDBStoreBase,\n)\nfrom ._qdrant_store import QdrantStore\nfrom ._milvuslite_store import MilvusLiteStore\nfrom ._oceanbase_store import OceanBaseStore\nfrom ._mongodb_store import MongoDBStore\nfrom ._alibabacloud_mysql_store import AlibabaCloudMySQLStore\n\n__all__ = [\n    \"VDBStoreBase\",\n    \"QdrantStore\",\n    \"MilvusLiteStore\",\n    \"OceanBaseStore\",\n    \"MongoDBStore\",\n    \"AlibabaCloudMySQLStore\",\n]\n"
  },
  {
    "path": "src/agentscope/rag/_store/_alibabacloud_mysql_store.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The AlibabaCloud MySQL vector store implementation.\"\"\"\nimport json\nfrom typing import Any, Literal, TYPE_CHECKING\n\nfrom .._reader import Document\nfrom ._store_base import VDBStoreBase\nfrom .._document import DocMetadata\n\nfrom ..._utils._common import _map_text_to_uuid\nfrom ...types import Embedding\n\nif TYPE_CHECKING:\n    from mysql.connector import MySQLConnection\nelse:\n    MySQLConnection = \"mysql.connector.MySQLConnection\"\n\n\nclass AlibabaCloudMySQLStore(VDBStoreBase):\n    \"\"\"The AlibabaCloud MySQL vector store implementation, supporting vector\n    search operations using MySQL's native vector functions.\n\n    .. note:: AlibabaCloud MySQL vector search requires MySQL 8.0+.\n    This implementation uses MySQL's native vector functions\n    (VEC_DISTANCE_COSINE, VEC_DISTANCE_EUCLIDEAN, VEC_FROMTEXT) for\n    efficient vector similarity search with ORDER BY in SQL.\n    Only COSINE and EUCLIDEAN distance metrics are supported.\n\n    .. note:: Requires mysql-connector-python package. Install with:\n    `pip install mysql-connector-python`\n\n    .. note:: For AlibabaCloud MySQL instances, ensure vector search plugin\n    is enabled. Contact AlibabaCloud support if needed.\n\n    \"\"\"\n\n    def __init__(\n        self,\n        host: str,\n        port: int,\n        user: str,\n        password: str,\n        database: str,\n        table_name: str,\n        dimensions: int,\n        distance: Literal[\"COSINE\", \"EUCLIDEAN\"] = \"COSINE\",\n        hnsw_m: int = 16,\n        connection_kwargs: dict[str, Any] | None = None,\n    ) -> None:\n        \"\"\"Initialize the AlibabaCloud MySQL vector store.\n\n        Args:\n            host (`str`):\n                The hostname of the AlibabaCloud MySQL server.\n                Example: \"rm-xxxxx.mysql.rds.aliyuncs.com\"\n            port (`int`):\n                The port number of the MySQL server (typically 3306).\n            user (`str`):\n                The username for authentication.\n            password (`str`):\n                The password for authentication.\n            database (`str`):\n                The database name to use.\n            table_name (`str`):\n                The name of the table to store the embeddings.\n            dimensions (`int`):\n                The dimension of the embeddings.\n            distance (`Literal[\"COSINE\", \"EUCLIDEAN\"]`, default to \"COSINE\"):\n                The distance metric to use for similarity search. Can be\n                one of \"COSINE\" (cosine similarity) or \"EUCLIDEAN\"\n                (Euclidean distance). Defaults to \"COSINE\".\n            hnsw_m (`int`, default to 16):\n                The M parameter for HNSW vector index, which controls\n                the number of bi-directional links created for each node\n                during construction. Higher values create denser graphs\n                with better recall but use more memory. Typical values\n                range from 4 to 64. Defaults to 16.\n            connection_kwargs (`dict[str, Any] | None`, optional):\n                Other keyword arguments for the MySQL connector.\n                Example: {\"ssl_ca\": \"/path/to/ca.pem\", \"charset\": \"utf8mb4\"}\n        \"\"\"\n\n        try:\n            import mysql.connector\n        except ImportError as e:\n            raise ImportError(\n                \"MySQL connector is not installed. Please install it with \"\n                \"`pip install mysql-connector-python`.\",\n            ) from e\n\n        connection_kwargs = connection_kwargs or {}\n\n        # Initialize connection parameters\n        self.connection_params = {\n            \"host\": host,\n            \"port\": port,\n            \"user\": user,\n            \"password\": password,\n            \"database\": database,\n            **connection_kwargs,\n        }\n\n        self.table_name = table_name\n        self.dimensions = dimensions\n        self.distance = distance\n        self.hnsw_m = hnsw_m\n\n        # Initialize connection\n        self._conn = mysql.connector.connect(**self.connection_params)\n        self._cursor = self._conn.cursor(dictionary=True)\n\n    def _get_distance_function(self) -> str:\n        \"\"\"Get the MySQL native vector distance function name based on the\n        distance metric.\n\n        Returns:\n            `str`:\n                The SQL vector distance function name.\n        \"\"\"\n        if self.distance == \"COSINE\":\n            return \"VEC_DISTANCE_COSINE\"\n        elif self.distance == \"EUCLIDEAN\":\n            return \"VEC_DISTANCE_EUCLIDEAN\"\n        else:\n            raise ValueError(\n                f\"Unsupported distance metric: {self.distance}. \"\n                f\"AlibabaCloud MySQL only supports 'COSINE' and 'EUCLIDEAN'.\",\n            )\n\n    def _format_vector_for_sql(self, vector: list[float]) -> str:\n        \"\"\"Format a vector as a string for MySQL VEC_FROMTEXT function.\n        Args:\n            vector (`list[float]`):\n                The vector to format.\n        Returns:\n            `str`:\n                The formatted vector string like \"[1,2,3,4]\".\n        \"\"\"\n        return \"[\" + \",\".join(map(str, vector)) + \"]\"\n\n    async def _validate_table(self) -> None:\n        \"\"\"Validate the table exists, if not, create it.\n        Creates a table with VECTOR type columns and automatically\n        creates a vector index based on the specified distance metric\n        using HNSW algorithm.\n        \"\"\"\n        # Get distance metric in lowercase for SQL\n        distance_metric = self.distance.lower()\n\n        # Create table with VECTOR INDEX in a single statement\n        # VECTOR(dimensions) type is available in AlibabaCloud MySQL 8.0+\n        # VECTOR INDEX uses HNSW algorithm with M parameter for\n        # graph connectivity\n        # IF NOT EXISTS prevents errors if table already exists\n        create_table_sql = f\"\"\"\n        CREATE TABLE IF NOT EXISTS {self.table_name} (\n            id VARCHAR(255) PRIMARY KEY,\n            embedding VECTOR({self.dimensions}) NOT NULL,\n            doc_id VARCHAR(255) NOT NULL,\n            chunk_id INT NOT NULL,\n            content TEXT NOT NULL,\n            total_chunks INT NOT NULL,\n            INDEX idx_doc_id (doc_id),\n            INDEX idx_chunk_id (chunk_id),\n            VECTOR INDEX (embedding) M={self.hnsw_m} DISTANCE={distance_metric}\n        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci\n        \"\"\"\n        self._cursor.execute(create_table_sql)\n        self._conn.commit()\n\n    async def add(self, documents: list[Document], **kwargs: Any) -> None:\n        \"\"\"Add embeddings to the AlibabaCloud MySQL vector store.\n\n        Args:\n            documents (`list[Document]`):\n                A list of embedding records to be recorded in the MySQL store.\n            **kwargs (`Any`):\n                Additional arguments for the insert operation.\n        \"\"\"\n        await self._validate_table()\n\n        # Prepare data for insertion\n        for doc in documents:\n            # Generate a unique ID\n            unique_string = json.dumps(\n                {\n                    \"doc_id\": doc.metadata.doc_id,\n                    \"chunk_id\": doc.metadata.chunk_id,\n                    \"content\": doc.metadata.content,\n                },\n                ensure_ascii=False,\n            )\n            unique_id = _map_text_to_uuid(unique_string)\n\n            # Format vector for MySQL VEC_FROMTEXT\n            if doc.embedding is None:\n                raise ValueError(\n                    f\"Document embedding cannot be None for doc_id: \"\n                    f\"{doc.metadata.doc_id}\",\n                )\n            vector_text = self._format_vector_for_sql(doc.embedding)\n\n            # Insert data using VEC_FROMTEXT to convert text to vector\n            insert_sql = f\"\"\"\n            INSERT INTO {self.table_name}\n            (id, embedding, doc_id, chunk_id, content, total_chunks)\n            VALUES (%s, VEC_FROMTEXT(%s), %s, %s, %s, %s)\n            ON DUPLICATE KEY UPDATE\n                embedding = VEC_FROMTEXT(%s),\n                doc_id = VALUES(doc_id),\n                chunk_id = VALUES(chunk_id),\n                content = VALUES(content),\n                total_chunks = VALUES(total_chunks)\n            \"\"\"\n\n            # Serialize content to JSON if it's not a string\n            content_str = (\n                doc.metadata.content\n                if isinstance(doc.metadata.content, str)\n                else json.dumps(doc.metadata.content, ensure_ascii=False)\n            )\n\n            self._cursor.execute(\n                insert_sql,\n                (\n                    unique_id,\n                    vector_text,\n                    doc.metadata.doc_id,\n                    doc.metadata.chunk_id,\n                    content_str,\n                    doc.metadata.total_chunks,\n                    vector_text,  # For ON DUPLICATE KEY UPDATE\n                ),\n            )\n\n        self._conn.commit()\n\n    async def search(\n        self,\n        query_embedding: Embedding,\n        limit: int,\n        score_threshold: float | None = None,\n        **kwargs: Any,\n    ) -> list[Document]:\n        \"\"\"Search relevant documents from the AlibabaCloud MySQL vector store.\n\n        Args:\n            query_embedding (`Embedding`):\n                The embedding of the query text.\n            limit (`int`):\n                The number of relevant documents to retrieve.\n            score_threshold (`float | None`, optional):\n                The minimum similarity score threshold to filter the\n                results. Score is calculated as 1 - distance, where\n                higher scores indicate higher similarity. Only documents\n                with score >= score_threshold will be returned.\n            **kwargs (`Any`):\n                Additional arguments for the search operation.\n                - filter (`str`): WHERE clause to filter the search results.\n        \"\"\"\n\n        # Format query vector for MySQL VEC_FROMTEXT\n        query_vector_text = self._format_vector_for_sql(query_embedding)\n\n        # Get the distance function\n        distance_func = self._get_distance_function()\n\n        # Build WHERE clause\n        where_conditions = []\n        if \"filter\" in kwargs and kwargs[\"filter\"]:\n            where_conditions.append(kwargs[\"filter\"])\n\n        # Add score threshold condition if specified\n        # Score is calculated as 1 - distance, so higher scores\n        # indicate higher similarity\n        # To filter by score_threshold, we need:\n        # 1.0 - distance >= score_threshold\n        if score_threshold is not None:\n            where_conditions.append(\n                f\"{distance_func}(embedding, VEC_FROMTEXT(%s)) <= %s\",\n            )\n\n        where_clause = \"\"\n        if where_conditions:\n            where_clause = \"WHERE \" + \" AND \".join(where_conditions)\n\n        # Build and execute the search query using MySQL native vector\n        # functions with ORDER BY for efficient sorting\n        search_sql = f\"\"\"\n        SELECT\n            id,\n            doc_id,\n            chunk_id,\n            content,\n            total_chunks,\n            {distance_func}(embedding, VEC_FROMTEXT(%s)) as distance\n        FROM {self.table_name}\n        {where_clause}\n        ORDER BY distance ASC\n        LIMIT %s\n        \"\"\"\n\n        # Prepare parameters\n        params: list[str | float | int] = [query_vector_text]\n        if score_threshold is not None:\n            # Convert score threshold to distance threshold:\n            # distance <= 1.0 - score\n            params.extend([query_vector_text, 1.0 - score_threshold])\n        params.append(limit)\n\n        self._cursor.execute(search_sql, params)\n        results = self._cursor.fetchall()\n\n        # Process results\n        collected_res = []\n        for row in results:\n            # Deserialize content from JSON if it's a JSON string\n            content = row[\"content\"]\n            try:\n                content = json.loads(content)\n            except (json.JSONDecodeError, TypeError):\n                # If it's not valid JSON, keep it as is (plain string)\n                pass\n\n            doc_metadata = DocMetadata(\n                content=content,\n                doc_id=row[\"doc_id\"],\n                chunk_id=row[\"chunk_id\"],\n                total_chunks=row[\"total_chunks\"],\n            )\n\n            # Create Document\n            # Convert distance to score: score = 1 - distance\n            # Higher scores indicate higher similarity\n            collected_res.append(\n                Document(\n                    embedding=None,  # Vector not returned for efficiency\n                    score=1.0 - row[\"distance\"],\n                    metadata=doc_metadata,\n                ),\n            )\n\n        return collected_res\n\n    async def delete(\n        self,\n        ids: list[str] | None = None,\n        filter: str | None = None,  # pylint: disable=redefined-builtin\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Delete documents from the AlibabaCloud MySQL vector store.\n\n        Args:\n            ids (`list[str] | None`, optional):\n                List of entity IDs to delete.\n            filter (`str | None`, optional):\n                WHERE clause expression to filter documents to delete.\n            **kwargs (`Any`):\n                Additional arguments for the delete operation.\n        \"\"\"\n        if ids is None and filter is None:\n            raise ValueError(\n                \"Either ids or filter must be provided for deletion.\",\n            )\n\n        if ids is not None:\n            # Delete by IDs\n            placeholders = \",\".join([\"%s\"] * len(ids))\n            delete_sql = (\n                f\"DELETE FROM {self.table_name} WHERE id IN ({placeholders})\"\n            )\n            self._cursor.execute(delete_sql, ids)\n        elif filter is not None:\n            # Delete by filter\n            delete_sql = f\"DELETE FROM {self.table_name} WHERE {filter}\"\n            self._cursor.execute(delete_sql)\n\n        self._conn.commit()\n\n    def get_client(self) -> MySQLConnection:\n        \"\"\"Get the underlying MySQL connection, so that developers can access\n        the full functionality of AlibabaCloud MySQL.\n\n        Returns:\n            `MySQLConnection`:\n                The underlying MySQL connection.\n        \"\"\"\n        return self._conn\n\n    def close(self) -> None:\n        \"\"\"Close the database connection.\"\"\"\n        if self._cursor:\n            self._cursor.close()\n        if self._conn:\n            self._conn.close()\n\n    def __del__(self) -> None:\n        \"\"\"Destructor to ensure connection is closed.\"\"\"\n        try:\n            self.close()\n        except Exception:  # pylint: disable=broad-except\n            pass\n"
  },
  {
    "path": "src/agentscope/rag/_store/_milvuslite_store.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The Milvus Lite vector store implementation.\"\"\"\nimport json\nfrom typing import Any, Literal, TYPE_CHECKING\n\nfrom .._reader import Document\nfrom ._store_base import VDBStoreBase\nfrom .._document import DocMetadata\n\nfrom ..._utils._common import _map_text_to_uuid\nfrom ...types import Embedding\n\nif TYPE_CHECKING:\n    from pymilvus import MilvusClient\nelse:\n    MilvusClient = \"pymilvus.MilvusClient\"\n\n\nclass MilvusLiteStore(VDBStoreBase):\n    \"\"\"The Milvus Lite vector store implementation, supporting both local and\n    remote Milvus instances.\n\n    .. note:: In Milvus Lite, we use the scalar fields to store the metadata,\n    including the document ID, chunk ID, and original content. The new\n    MilvusClient API is used for simplified operations.\n\n    .. note:: Milvus Lite is not supported on Windows OS for now (2025-10-21).\n\n    \"\"\"\n\n    def __init__(\n        self,\n        uri: str,\n        collection_name: str,\n        dimensions: int,\n        distance: Literal[\"COSINE\", \"L2\", \"IP\"] = \"COSINE\",\n        token: str = \"\",\n        client_kwargs: dict[str, Any] | None = None,\n        collection_kwargs: dict[str, Any] | None = None,\n    ) -> None:\n        \"\"\"Initialize the Milvus Lite vector store.\n\n        Args:\n            uri (`str`):\n                The URI of the Milvus instance. For Milvus Lite, use a local\n                file path like \"./milvus_demo.db\". For remote Milvus server,\n                use URI like \"http://localhost:19530\".\n            collection_name (`str`):\n                The name of the collection to store the embeddings.\n            dimensions (`int`):\n                The dimension of the embeddings.\n            distance (`Literal[\"COSINE\", \"L2\", \"IP\"]`, default to \"COSINE\"):\n                The distance metric to use for the collection. Can be one of\n                \"COSINE\", \"L2\", or \"IP\". Defaults to \"COSINE\".\n            token (`str`, defaults to \"\"):\n                The token for authentication when connecting to remote Milvus.\n                Format: \"username:password\". Not needed for Milvus Lite.\n            client_kwargs (`dict[str, Any] | None`, optional):\n                Other keyword arguments for the Milvus client.\n            collection_kwargs (`dict[str, Any] | None`, optional):\n                Other keyword arguments for creating the collection.\n        \"\"\"\n\n        try:\n            from pymilvus import MilvusClient\n        except ImportError as e:\n            raise ImportError(\n                \"Milvus client is not installed. Please install it with \"\n                \"`pip install pymilvus[milvus_lite]`.\",\n            ) from e\n\n        client_kwargs = client_kwargs or {}\n\n        # Initialize MilvusClient with uri and optional token\n        init_params = {\"uri\": uri, **client_kwargs}\n        if token:\n            init_params[\"token\"] = token\n\n        self._client = MilvusClient(**init_params)\n\n        self.collection_name = collection_name\n        self.dimensions = dimensions\n        self.distance = distance\n        self.collection_kwargs = collection_kwargs or {}\n\n    async def _validate_collection(self) -> None:\n        \"\"\"Validate the collection exists, if not, create it.\"\"\"\n        if not self._client.has_collection(self.collection_name):\n            # Create collection with the new MilvusClient API\n            # By default, it creates an auto-incrementing integer ID field\n            kwargs = {\n                \"collection_name\": self.collection_name,\n                \"dimension\": self.dimensions,\n                \"metric_type\": self.distance,\n                **self.collection_kwargs,\n            }\n\n            self._client.create_collection(**kwargs)\n\n    async def add(self, documents: list[Document], **kwargs: Any) -> None:\n        \"\"\"Add embeddings to the Milvus vector store.\n\n        Args:\n            documents (`list[Document]`):\n                A list of embedding records to be recorded in the Milvus store.\n            **kwargs (`Any`):\n                Additional arguments for the insert operation.\n        \"\"\"\n        await self._validate_collection()\n\n        # Prepare data for insertion using the new MilvusClient API\n        data = []\n        for doc in documents:\n            # Generate a unique integer ID based on hash\n            unique_string = json.dumps(\n                {\n                    \"doc_id\": doc.metadata.doc_id,\n                    \"chunk_id\": doc.metadata.chunk_id,\n                    \"content\": doc.metadata.content,\n                },\n                ensure_ascii=False,\n            )\n\n            id_type = self.collection_kwargs.get(\"id_type\", \"int\")\n            if id_type == \"string\":\n                unique_id = _map_text_to_uuid(unique_string)[:6]\n            else:\n                unique_id = abs(hash(unique_string)) % (10**10)\n\n            # Prepare data entry with vector and metadata\n            entry = {\n                # Fixed fields for Milvus\n                \"id\": unique_id,\n                \"vector\": doc.embedding,\n                # fields that will be returned in the \"entity\" field during\n                #  search\n                \"doc_id\": doc.metadata.doc_id,\n                \"chunk_id\": doc.metadata.chunk_id,\n                \"content\": doc.metadata.content,\n                \"total_chunks\": doc.metadata.total_chunks,\n            }\n            data.append(entry)\n\n        # Insert data using MilvusClient\n        self._client.insert(\n            collection_name=self.collection_name,\n            data=data,\n        )\n\n    async def search(\n        self,\n        query_embedding: Embedding,\n        limit: int,\n        score_threshold: float | None = None,\n        **kwargs: Any,\n    ) -> list[Document]:\n        \"\"\"Search relevant documents from the Milvus vector store.\n\n        Args:\n            query_embedding (`Embedding`):\n                The embedding of the query text.\n            limit (`int`):\n                The number of relevant documents to retrieve.\n            score_threshold (`float | None`, optional):\n                The threshold of the score to filter the results.\n            **kwargs (`Any`):\n                Additional arguments for the Milvus client search API.\n                - filter (`str`): Expression to filter the search results.\n                - output_fields (`list[str]`): Fields to include in results.\n        \"\"\"\n\n        # Get output fields if specified\n        if \"output_fields\" not in kwargs:\n            kwargs[\"output_fields\"] = [\n                \"doc_id\",\n                \"chunk_id\",\n                \"content\",\n                \"total_chunks\",\n            ]\n\n        # Execute search using MilvusClient\n        results = self._client.search(\n            collection_name=self.collection_name,\n            data=[query_embedding],\n            limit=limit,\n            **kwargs,\n        )\n\n        # Process results\n        collected_res = []\n        for hits in results:\n            for hit in hits:\n                # Check score threshold\n                if (\n                    score_threshold is not None\n                    and hit[\"distance\"] < score_threshold\n                ):\n                    continue\n\n                # Get metadata from entity\n                entity = hit[\"entity\"]\n\n                doc_metadata = DocMetadata(\n                    content=entity.get(\"content\", \"\"),\n                    doc_id=entity.get(\"doc_id\", \"\"),\n                    chunk_id=entity.get(\"chunk_id\", 0),\n                    total_chunks=entity.get(\"total_chunks\", 0),\n                )\n\n                # Create Document\n                collected_res.append(\n                    Document(\n                        embedding=None,  # Vector not returned by default\n                        score=hit[\"distance\"],\n                        metadata=doc_metadata,\n                    ),\n                )\n\n        return collected_res\n\n    async def delete(\n        self,\n        ids: list[str] | None = None,\n        filter: str | None = None,  # pylint: disable=redefined-builtin\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Delete documents from the Milvus vector store.\n\n        Args:\n            ids (`list[str] | None`, optional):\n                List of entity IDs to delete.\n            filter (`str | None`, optional):\n                Expression to filter documents to delete.\n            **kwargs (`Any`):\n                Additional arguments for the delete operation.\n        \"\"\"\n        if ids is None and filter is None:\n            raise ValueError(\n                \"Either ids or filter_expr must be provided for deletion.\",\n            )\n\n        # Delete data using MilvusClient\n        self._client.delete(\n            collection_name=self.collection_name,\n            ids=ids,\n            filter=filter,\n        )\n\n    def get_client(self) -> MilvusClient:\n        \"\"\"Get the underlying Milvus client, so that developers can access\n        the full functionality of Milvus.\n\n        Returns:\n            `MilvusClient`:\n                The underlying Milvus client.\n        \"\"\"\n        return self._client\n"
  },
  {
    "path": "src/agentscope/rag/_store/_mongodb_store.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The MongoDB vector store implementation using MongoDB Vector Search.\n\nThis implementation provides a vector database store using MongoDB's vector\n search capabilities. It requires MongoDB with vector search support and\n automatically creates vector search indexes.\n\"\"\"\nimport asyncio\nimport time\n\nfrom typing import Any, Literal, TYPE_CHECKING\n\nfrom .._reader import Document\nfrom ._store_base import VDBStoreBase\nfrom .._document import DocMetadata\nfrom ...types import Embedding\n\nif TYPE_CHECKING:\n    from pymongo import AsyncMongoClient\nelse:\n    AsyncMongoClient = \"pymongo.AsyncMongoClient\"\n\n\nclass MongoDBStore(VDBStoreBase):\n    \"\"\"MongoDB vector store using MongoDB Vector Search.\n\n    This class provides a vector database store implementation using MongoDB's\n    vector search capabilities. It requires MongoDB with vector search support\n    and creates vector search indexes automatically.\n\n    .. note:: Ensure your MongoDB instance supports Vector Search\n    functionality.\n\n    .. note:: The store automatically creates database, collection, and vector\n    search index on first operation. No manual initialization is required.\n    \"\"\"\n\n    def __init__(\n        self,\n        host: str,\n        db_name: str,\n        collection_name: str,\n        dimensions: int,\n        index_name: str = \"vector_index\",\n        distance: Literal[\"cosine\", \"euclidean\", \"dotProduct\"] = \"cosine\",\n        filter_fields: list[str] | None = None,\n        client_kwargs: dict[str, Any] | None = None,\n        db_kwargs: dict[str, Any] | None = None,\n        collection_kwargs: dict[str, Any] | None = None,\n    ) -> None:\n        \"\"\"Initialize the MongoDB vector store.\n\n        Args:\n            host (`str`):\n                MongoDB connection host, e.g., \"mongodb://localhost:27017\" or\n                \"mongodb+srv://cluster.mongodb.net/\".\n            db_name (`str`):\n                Database name to store vector documents.\n            collection_name (`str`):\n                Collection name to store vector documents.\n            dimensions (`int`):\n                Embedding dimensions for the vector search index.\n            index_name (`str`, defaults to \"vector_index\"):\n                Vector search index name.\n            distance (`Literal[\"cosine\", \"euclidean\", \"dotProduct\"]`, \\\n            defaults to \"cosine\"):\n                Distance metric for vector similarity. Can be one of \"cosine\",\n                \"euclidean\", or \"dotProduct\".\n            filter_fields (`list[str] | None`, optional):\n                List of field paths to index for filtering in $vectorSearch.\n                For example: [\"payload.doc_id\", \"payload.chunk_id\"].\n                These fields can then be used in the `filter` parameter of\n                the `search` method. MongoDB $vectorSearch filter supports:\n                $gt, $gte, $lt, $lte, $eq, $ne, $in, $nin, $exists, $not.\n            client_kwargs (`dict[str, Any] | None`, optional):\n                Additional kwargs for MongoDB client.\n            db_kwargs (`dict[str, Any] | None`, optional):\n                Additional kwargs for database.\n            collection_kwargs (`dict[str, Any] | None`, optional):\n                Additional kwargs for collection.\n\n        Raises:\n            ImportError: If pymongo is not installed.\n        \"\"\"\n        try:\n            from pymongo import AsyncMongoClient\n        except ImportError as e:\n            raise ImportError(\n                \"Please install the latest pymongo package to use \"\n                \"AsyncMongoClient: `pip install pymongo`\",\n            ) from e\n\n        self._client: AsyncMongoClient = AsyncMongoClient(\n            host,\n            **(client_kwargs or {}),\n        )\n        self.db_name = db_name\n        self.collection_name = collection_name\n        self.index_name = index_name\n        self.dimensions = dimensions\n        self.distance = distance\n        self.filter_fields = filter_fields or []\n        self.db_kwargs = db_kwargs or {}\n        self.collection_kwargs = collection_kwargs or {}\n\n        self._db = None\n        self._collection = None\n\n    async def _validate_db_and_collection(self) -> None:\n        \"\"\"Validate the database and collection exist, create if necessary.\n\n        This method ensures the database and collection are available,\n        and creates a vector search index for the collection.\n\n        Raises:\n            Exception: If database or collection creation fails.\n        \"\"\"\n        self._db = self._client.get_database(\n            self.db_name,\n            **self.db_kwargs,\n        )\n\n        if self.collection_name not in await self._db.list_collection_names():\n            self._collection = await self._db.create_collection(\n                self.collection_name,\n            )\n        else:\n            self._collection = self._db.get_collection(\n                self.collection_name,\n                **self.collection_kwargs,\n            )\n\n        from pymongo.operations import SearchIndexModel\n\n        # Build index fields: vector field + optional filter fields\n        index_fields: list[dict[str, Any]] = [\n            {\n                \"type\": \"vector\",\n                \"path\": \"vector\",\n                \"similarity\": self.distance,\n                \"numDimensions\": self.dimensions,\n            },\n        ]\n\n        # Add user-specified filter fields\n        for field_path in self.filter_fields:\n            index_fields.append(\n                {\n                    \"type\": \"filter\",\n                    \"path\": field_path,\n                },\n            )\n\n        search_index_model = SearchIndexModel(\n            definition={\"fields\": index_fields},\n            name=self.index_name,\n            type=\"vectorSearch\",\n        )\n\n        await self._collection.create_search_index(\n            model=search_index_model,\n        )\n\n    async def _wait_for_index_ready(self, timeout: int = 30) -> None:\n        \"\"\"Wait for the vector search index to be ready with timeout\n        protection.\n\n        Args:\n            timeout (`int`, defaults to 30):\n                Maximum time to wait in seconds.\n\n        Raises:\n            TimeoutError: If index is not ready within the timeout period.\n        \"\"\"\n\n        start_time = time.time()\n        while time.time() - start_time < timeout:\n            try:\n                indices = []\n                async for idx in await self._collection.list_search_indexes(\n                    self.index_name,\n                ):\n                    indices.append(idx)\n                if indices and indices[0].get(\"queryable\") is True:\n                    return\n            except Exception:\n                pass\n\n            await asyncio.sleep(0.2)\n\n        raise TimeoutError(f\"Index not ready after {timeout} seconds\")\n\n    async def add(self, documents: list[Document], **kwargs: Any) -> None:\n        \"\"\"Insert documents with embeddings into MongoDB.\n\n        This method automatically creates the database, collection, and vector\n        search index if they don't exist.\n\n        Args:\n            documents (`list[Document]`):\n                List of Document objects to insert.\n            **kwargs (`Any`):\n                Additional arguments (unused).\n\n        .. note::\n            Each inserted record has structure:\n\n            .. code-block:: python\n\n                {\n                    \"id\": str,                # Document ID\n                    \"vector\": list[float],    # Vector embedding\n                    \"payload\": dict,          # DocMetadata as dict\n                }\n        \"\"\"\n        await self._validate_db_and_collection()\n\n        # Prepare documents for insertion\n        docs_to_insert = []\n        for doc in documents:\n            # Convert DocMetadata to dict for storage\n            payload = {\n                \"doc_id\": doc.metadata.doc_id,\n                \"chunk_id\": doc.metadata.chunk_id,\n                \"total_chunks\": doc.metadata.total_chunks,\n                \"content\": doc.metadata.content,\n            }\n\n            # Create document record\n            doc_record = {\n                \"id\": f\"{doc.metadata.doc_id}_{doc.metadata.chunk_id}\",\n                \"vector\": doc.embedding,\n                \"payload\": payload,\n            }\n            docs_to_insert.append(doc_record)\n\n        # Insert documents using upsert to handle duplicates\n        if not docs_to_insert:\n            return\n        from pymongo import ReplaceOne\n\n        operations = [\n            ReplaceOne(\n                {\"id\": doc_record[\"id\"]},\n                doc_record,\n                upsert=True,\n            )\n            for doc_record in docs_to_insert\n        ]\n        await self._collection.bulk_write(operations)\n\n    async def search(\n        self,\n        query_embedding: Embedding,\n        limit: int,\n        score_threshold: float | None = None,\n        **kwargs: Any,\n    ) -> list[Document]:\n        \"\"\"Search relevant documents using MongoDB Vector Search.\n\n        This method uses MongoDB's $vectorSearch aggregation pipeline for\n        vector similarity search. It automatically waits for the vector search\n        index to be ready before performing the search.\n\n        Args:\n            query_embedding (`Embedding`):\n                The embedding vector to search for.\n            limit (`int`):\n                Maximum number of documents to return.\n            score_threshold (`float | None`, optional):\n                Minimum similarity score threshold. Documents with scores below\n                this threshold will be filtered out.\n            **kwargs (`Any`):\n                Additional arguments for the search operation.\n\n        Returns:\n            `list[Document]`: List of Document objects with embedding,\n            score, and metadata.\n\n        .. note::\n            - Requires MongoDB with vector search support\n            - Uses $vectorSearch aggregation pipeline\n        \"\"\"\n        await self._validate_db_and_collection()\n        # Wait for index to be ready before searching\n        await self._wait_for_index_ready()\n\n        # Construct aggregation pipeline for vector search\n        # See: https://www.mongodb.com/docs/atlas/atlas-search/vector-search/\n        num_candidates = int(\n            kwargs.pop(\n                \"num_candidates\",\n                max(\n                    100,\n                    limit * 20,\n                ),\n            ),\n        )\n\n        pipeline: list[dict[str, Any]] = [\n            {\n                \"$vectorSearch\": {\n                    \"index\": self.index_name,\n                    \"path\": \"vector\",\n                    \"queryVector\": list(query_embedding),\n                    \"numCandidates\": num_candidates,\n                    \"limit\": limit,\n                    **kwargs,\n                },\n            },\n            {\n                \"$project\": {\n                    \"vector\": 1,\n                    \"payload\": 1,\n                    \"score\": {\"$meta\": \"vectorSearchScore\"},\n                },\n            },\n        ]\n\n        cursor = await self._collection.aggregate(pipeline)\n        results: list[Document] = []\n        async for item in cursor:\n            score_val = float(item.get(\"score\", 0.0))\n            if score_threshold is not None and score_val < score_threshold:\n                continue\n\n            payload = item.get(\"payload\", {})\n            # Rebuild Document\n            metadata = DocMetadata(**payload)\n\n            results.append(\n                Document(\n                    embedding=[float(x) for x in item.get(\"vector\", [])],\n                    score=score_val,\n                    metadata=metadata,\n                ),\n            )\n\n        return results\n\n    async def delete(\n        self,\n        ids: str | list[str] | None = None,\n    ) -> None:\n        \"\"\"Delete documents from the MongoDB collection.\n\n        Args:\n            ids (`str | list[str] | None`, optional):\n                List of document IDs to delete. If provided, deletes documents\n                with matching doc_id in payload.\n        \"\"\"\n\n        if not ids:\n            return\n\n        if isinstance(ids, str):\n            ids = [ids]\n\n        await self._collection.delete_many({\"payload.doc_id\": {\"$in\": ids}})\n\n    def get_client(self) -> AsyncMongoClient:\n        \"\"\"Get the underlying MongoDB client for advanced operations.\n\n        Returns:\n            `AsyncMongoClient`: The AsyncMongoClient instance.\n        \"\"\"\n        return self._client\n\n    async def delete_collection(self) -> None:\n        \"\"\"Delete the entire collection.\n\n        .. warning::\n            This will permanently delete all documents in the collection.\n        \"\"\"\n        await self._collection.drop()\n\n    async def delete_database(self) -> None:\n        \"\"\"Delete the entire database.\n\n        .. warning::\n            This will permanently delete the entire database and all its\n            collections.\n        \"\"\"\n        await self._client.drop_database(self.db_name)\n\n    async def close(self) -> None:\n        \"\"\"Close the MongoDB connection.\n\n        This should be called when the store is no longer needed to properly\n        clean up resources.\n        \"\"\"\n        await self._client.close()\n"
  },
  {
    "path": "src/agentscope/rag/_store/_oceanbase_store.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The OceanBase vector store implementation.\"\"\"\nimport asyncio\nimport json\nfrom typing import Any, Callable, Literal, TYPE_CHECKING\n\nfrom .._reader import Document\nfrom ._store_base import VDBStoreBase\nfrom .._document import DocMetadata\nfrom ..._utils._common import _map_text_to_uuid\nfrom ...message import TextBlock\nfrom ...types import Embedding\n\nif TYPE_CHECKING:\n    from pyobvector import MilvusLikeClient\nelse:\n    MilvusLikeClient = \"pyobvector.MilvusLikeClient\"\n\n\n# Metric configuration: data-driven approach focusing on exceptions\n# Base metric names for pyobvector\n_METRIC_NAMES = {\n    \"COSINE\": \"cosine\",\n    \"L2\": \"l2\",\n    \"IP\": \"inner_product\",\n}\n\n# Only configure special cases that differ from the base\n_SEARCH_METRIC_OVERRIDES = {\n    \"IP\": \"ip\",  # IP uses \"ip\" for search to get positive inner product\n}\n\n_SCORE_CONVERTERS: dict[str, Callable[[float], float]] = {\n    \"COSINE\": lambda d: 1.0 - d,  # COSINE converts distance to similarity\n}\n\n\nclass OceanBaseStore(VDBStoreBase):\n    \"\"\"The OceanBase vector store implementation, supporting OceanBase and\n    seekdb via pyobvector.\"\"\"\n\n    # Field names - using descriptive constants to avoid magic strings\n    PRIMARY_FIELD = \"id\"\n    VECTOR_FIELD = \"embedding\"\n    DOC_ID_FIELD = \"doc_id\"\n    CHUNK_ID_FIELD = \"chunk_id\"\n    TOTAL_CHUNKS_FIELD = \"total_chunks\"\n    CONTENT_FIELD = \"content\"\n\n    # Index configuration\n    INDEX_NAME = \"vidx\"\n    INDEX_TYPE = \"hnsw\"\n\n    def __init__(\n        self,\n        collection_name: str,\n        dimensions: int,\n        uri: str = \"127.0.0.1:2881\",\n        user: str = \"root@test\",\n        password: str = \"\",\n        db_name: str = \"test\",\n        distance: Literal[\"COSINE\", \"L2\", \"IP\"] = \"COSINE\",\n        client_kwargs: dict[str, Any] | None = None,\n        collection_kwargs: dict[str, Any] | None = None,\n    ) -> None:\n        \"\"\"Initialize the OceanBase vector store.\n\n        Args:\n            collection_name (`str`):\n                The name of the collection to store the embeddings.\n            dimensions (`int`):\n                The dimension of the embeddings.\n            uri (`str`, defaults to `\"127.0.0.1:2881\"`):\n                The OceanBase server URI, such as \"127.0.0.1:2881\".\n            user (`str`, defaults to `\"root@test\"`):\n                The username for authentication.\n            password (`str`, defaults to `\"\"`):\n                The password for authentication.\n            db_name (`str`, defaults to `\"test\"`):\n                The database name to connect to.\n            distance (`Literal[\"COSINE\", \"L2\", \"IP\"]`, defaults to `\"COSINE\"`):\n                The distance metric to use for the collection. Can be one of\n                \"COSINE\", \"L2\", or \"IP\".\n            client_kwargs (`dict[str, Any] | None`, optional):\n                Keyword arguments passed to `pyobvector.MilvusLikeClient`.\n                Explicit connection arguments override matching keys here.\n            collection_kwargs (`dict[str, Any] | None`, optional):\n                Keyword arguments passed to `create_collection`.\n        \"\"\"\n        try:\n            import pyobvector\n        except ImportError as e:\n            raise ImportError(\n                \"OceanBase client is not installed. Please install it with \"\n                \"`pip install pyobvector`.\",\n            ) from e\n\n        self._pyobvector = pyobvector\n        client_kwargs = dict(client_kwargs or {})\n\n        self._client = pyobvector.MilvusLikeClient(\n            uri=uri,\n            user=user,\n            password=password,\n            db_name=db_name,\n            **client_kwargs,\n        )\n\n        self.collection_name = collection_name\n        self.dimensions = dimensions\n        self.distance = distance\n        self.collection_kwargs = collection_kwargs or {}\n        self._collection_ready = False\n\n    def _get_metric_type(self) -> str:\n        \"\"\"Get the metric type for index creation.\"\"\"\n        return _METRIC_NAMES[self.distance]\n\n    def _get_search_metric_type(self) -> str:\n        \"\"\"Get the search metric type for queries.\n\n        Returns the override value if exists, otherwise uses index metric.\n        This allows special handling (e.g., IP uses \"ip\" for positive values).\n        \"\"\"\n        return _SEARCH_METRIC_OVERRIDES.get(\n            self.distance,\n            self._get_metric_type(),\n        )\n\n    async def _validate_collection(self) -> None:\n        \"\"\"Validate the collection exists, if not, create it.\"\"\"\n        if self._collection_ready:\n            return\n\n        if await asyncio.to_thread(\n            self._client.has_collection,\n            self.collection_name,\n        ):\n            self._collection_ready = True\n            return\n\n        collection_kwargs = dict(self.collection_kwargs)\n\n        if \"schema\" not in collection_kwargs:\n            collection_kwargs[\"schema\"] = self._create_schema()\n\n        if \"index_params\" not in collection_kwargs:\n            collection_kwargs[\"index_params\"] = self._create_index_params()\n\n        await asyncio.to_thread(\n            self._client.create_collection,\n            collection_name=self.collection_name,\n            **collection_kwargs,\n        )\n        self._collection_ready = True\n\n    def _create_schema(self) -> Any:\n        \"\"\"Create the collection schema with all required fields.\n\n        Returns:\n            Schema object with primary, vector, and metadata fields configured.\n        \"\"\"\n        schema = self._client.create_schema()\n\n        # Primary key field\n        schema.add_field(\n            field_name=self.PRIMARY_FIELD,\n            datatype=self._pyobvector.DataType.VARCHAR,\n            is_primary=True,\n            auto_id=False,\n            max_length=36,\n        )\n\n        # Vector field\n        schema.add_field(\n            field_name=self.VECTOR_FIELD,\n            datatype=self._pyobvector.DataType.FLOAT_VECTOR,\n            dim=self.dimensions,\n        )\n\n        # Metadata fields\n        schema.add_field(\n            field_name=self.DOC_ID_FIELD,\n            datatype=self._pyobvector.DataType.STRING,\n        )\n        schema.add_field(\n            field_name=self.CHUNK_ID_FIELD,\n            datatype=self._pyobvector.DataType.INT64,\n        )\n        schema.add_field(\n            field_name=self.TOTAL_CHUNKS_FIELD,\n            datatype=self._pyobvector.DataType.INT64,\n        )\n        schema.add_field(\n            field_name=self.CONTENT_FIELD,\n            datatype=self._pyobvector.DataType.JSON,\n            nullable=True,\n        )\n\n        return schema\n\n    def _create_index_params(self) -> Any:\n        \"\"\"Create index parameters for vector search.\n\n        Returns:\n            Index parameters configured with HNSW index and appropriate metric.\n        \"\"\"\n        index_params = self._client.prepare_index_params()\n        index_params.add_index(\n            field_name=self.VECTOR_FIELD,\n            index_type=self.INDEX_TYPE,\n            index_name=self.INDEX_NAME,\n            metric_type=self._get_metric_type(),\n        )\n        return index_params\n\n    @staticmethod\n    def _content_to_text(content: Any) -> str:\n        \"\"\"Extract text string from content of various formats.\"\"\"\n        if isinstance(content, str):\n            return content\n        if isinstance(content, dict) and content.get(\"type\") == \"text\":\n            text = content.get(\"text\", \"\")\n            return text if isinstance(text, str) else \"\"\n        return \"\"\n\n    @staticmethod\n    def _normalize_content(content: Any, fallback_text: str) -> Any:\n        \"\"\"Normalize content to TextBlock format.\"\"\"\n        # Already in correct format\n        if isinstance(content, dict) and content.get(\"type\"):\n            return content\n        # Convert string to TextBlock\n        if isinstance(content, str):\n            return TextBlock(type=\"text\", text=content)\n        # Use fallback\n        return TextBlock(type=\"text\", text=fallback_text or \"\")\n\n    def _document_to_dict(self, doc: Document) -> dict[str, Any]:\n        \"\"\"Convert a Document to a dictionary for insertion.\n\n        Args:\n            doc (`Document`):\n                Document to convert\n\n        Returns:\n            `dict[str, Any]`:\n                Dictionary with fields ready for database insertion\n\n        Raises:\n            `ValueError`:\n                If document embedding is None\n        \"\"\"\n        if doc.embedding is None:\n            raise ValueError(\n                \"Document embedding is required for OceanBaseStore.add.\",\n            )\n\n        # Create unique ID from document metadata\n        unique_string = json.dumps(\n            {\n                \"doc_id\": doc.metadata.doc_id,\n                \"chunk_id\": doc.metadata.chunk_id,\n                \"content\": doc.metadata.content,\n            },\n            ensure_ascii=True,\n            sort_keys=True,\n        )\n\n        return {\n            self.PRIMARY_FIELD: _map_text_to_uuid(unique_string),\n            self.VECTOR_FIELD: doc.embedding,\n            self.DOC_ID_FIELD: doc.metadata.doc_id,\n            self.CHUNK_ID_FIELD: doc.metadata.chunk_id,\n            self.TOTAL_CHUNKS_FIELD: doc.metadata.total_chunks,\n            self.CONTENT_FIELD: doc.metadata.content,\n        }\n\n    async def add(self, documents: list[Document], **kwargs: Any) -> None:\n        \"\"\"Add embeddings to the OceanBase vector store.\n\n        Args:\n            documents (`list[Document]`):\n                A list of embedding records to be recorded in the store.\n        \"\"\"\n        await self._validate_collection()\n\n        data = [self._document_to_dict(doc) for doc in documents]\n\n        await asyncio.to_thread(\n            self._client.insert,\n            collection_name=self.collection_name,\n            data=data,\n            **kwargs,\n        )\n\n    @staticmethod\n    def _extract_distance(\n        row: dict[str, Any],\n        output_fields: list[str],\n    ) -> float | None:\n        \"\"\"Extract distance value from search result row.\n\n        The distance is stored in an extra field that's not in output_fields.\n        \"\"\"\n        extra_keys = [key for key in row if key not in output_fields]\n        return row.get(extra_keys[-1]) if extra_keys else None\n\n    def _create_document_from_row(\n        self,\n        row: dict[str, Any],\n        output_fields: list[str],\n    ) -> tuple[Document, float | None]:\n        \"\"\"Create a Document from a search result row.\n\n        Args:\n            row (`dict[str, Any]`):\n                Search result row containing document data\n            output_fields (`list[str]`):\n                List of fields requested in output\n\n        Returns:\n            `tuple[Document, float | None]`:\n                Tuple of (Document, score)\n        \"\"\"\n        distance = self._extract_distance(row, output_fields)\n        score = self._convert_distance_to_score(distance)\n\n        content_value = row.get(self.CONTENT_FIELD)\n        content_text = self._content_to_text(content_value)\n        content = self._normalize_content(content_value, content_text)\n\n        doc_metadata = DocMetadata(\n            content=content,\n            doc_id=str(row.get(self.DOC_ID_FIELD, \"\")),\n            chunk_id=int(row.get(self.CHUNK_ID_FIELD) or 0),\n            total_chunks=int(row.get(self.TOTAL_CHUNKS_FIELD) or 0),\n        )\n\n        return (\n            Document(\n                embedding=None,\n                score=score,\n                metadata=doc_metadata,\n            ),\n            score,\n        )\n\n    def _convert_distance_to_score(\n        self,\n        distance: float | None,\n    ) -> float | None:\n        \"\"\"Convert distance value to score based on metric type.\n\n        Args:\n            distance (`float | None`):\n                Raw distance value from database\n\n        Returns:\n            `float | None`:\n                Converted score (similarity for COSINE, raw value for others)\n\n        \"\"\"\n        if distance is None:\n            return None\n\n        # Apply converter if defined, otherwise return raw value (identity)\n        converter = _SCORE_CONVERTERS.get(self.distance)\n        return converter(distance) if converter else distance\n\n    async def search(\n        self,\n        query_embedding: Embedding,\n        limit: int,\n        score_threshold: float | None = None,\n        **kwargs: Any,\n    ) -> list[Document]:\n        \"\"\"Search relevant documents from the OceanBase vector store.\n\n        Args:\n            query_embedding (`Embedding`):\n                The embedding of the query text.\n            limit (`int`):\n                The number of relevant documents to retrieve.\n            score_threshold (`float | None`, optional):\n                The threshold of the score to filter the results.\n                Note: Score semantics (aligned with Milvus):\n                - COSINE: similarity [0,1], higher = more similar\n                - L2: Euclidean distance, smaller = more similar\n                - IP: inner product, larger = more similar\n            **kwargs (`Any`):\n                Additional arguments for the search API.\n                - flter (`list`): Filter conditions.\n                - partition_names (`list[str]`): Partition filter.\n                - output_fields (`list[str]`): Fields to include in results.\n                - search_params (`dict`): Search parameters.\n        \"\"\"\n        await self._validate_collection()\n\n        # Remove unsupported parameter\n        kwargs.pop(\"with_dist\", None)\n\n        # Prepare output fields and search parameters\n        output_fields = self._prepare_output_fields(\n            kwargs.pop(\"output_fields\", None),\n        )\n        search_params = self._prepare_search_params(\n            kwargs.pop(\"search_params\", None),\n        )\n\n        results = await asyncio.to_thread(\n            self._client.search,\n            collection_name=self.collection_name,\n            data=query_embedding,\n            anns_field=self.VECTOR_FIELD,\n            with_dist=True,\n            limit=limit,\n            output_fields=output_fields,\n            search_params=search_params,\n            **kwargs,\n        )\n\n        # Process results and filter by score threshold\n        return self._filter_results_by_threshold(\n            results,\n            output_fields,\n            score_threshold,\n        )\n\n    def _prepare_output_fields(\n        self,\n        output_fields: list[str] | None,\n    ) -> list[str]:\n        \"\"\"Prepare output fields ensuring all required fields are included.\n\n        Args:\n            output_fields (`list[str] | None`):\n                User-specified output fields or None\n\n        Returns:\n            `list[str]`:\n                List of output fields with required fields included\n        \"\"\"\n        required_fields = [\n            self.DOC_ID_FIELD,\n            self.CHUNK_ID_FIELD,\n            self.TOTAL_CHUNKS_FIELD,\n            self.CONTENT_FIELD,\n        ]\n\n        output_fields = output_fields or required_fields\n\n        # Use dict.fromkeys to preserve order while removing duplicates\n        return list(dict.fromkeys(output_fields + required_fields))\n\n    def _prepare_search_params(\n        self,\n        search_params: dict[str, Any] | None,\n    ) -> dict[str, Any]:\n        \"\"\"Prepare search parameters with appropriate metric type.\n\n        Args:\n            search_params (`dict[str, Any] | None`):\n                User-specified search parameters or None\n\n        Returns:\n            `dict[str, Any]`:\n                Search parameters dict with metric_type set\n        \"\"\"\n        search_params = dict(search_params or {})  # Create a copy\n        search_params.setdefault(\"metric_type\", self._get_search_metric_type())\n        return search_params\n\n    def _filter_results_by_threshold(\n        self,\n        results: list[dict[str, Any]],\n        output_fields: list[str],\n        score_threshold: float | None,\n    ) -> list[Document]:\n        \"\"\"Filter search results by score threshold.\n\n        Args:\n            results (`list[dict[str, Any]]`):\n                Raw search results from database\n            output_fields (`list[str]`):\n                List of output fields\n            score_threshold (`float | None`):\n                Minimum score threshold or None\n\n        Returns:\n            `list[Document]`:\n                List of filtered Document objects\n        \"\"\"\n        documents = []\n        for row in results:\n            doc, score = self._create_document_from_row(row, output_fields)\n\n            if score_threshold is not None and (\n                score is None or score < score_threshold\n            ):\n                continue\n\n            documents.append(doc)\n\n        return documents\n\n    async def delete(\n        self,\n        ids: list[str] | None = None,\n        where: Any | None = None,\n        where_document: Any | None = None,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Delete documents from the OceanBase vector store.\n\n        Args:\n            ids (`list[str] | None`, optional):\n                List of entity IDs to delete.\n            where (`Any | None`, optional):\n                Filter conditions for deletion.\n            where_document (`Any | None`, optional):\n                Unsupported in OceanBaseStore.\n        \"\"\"\n        await self._validate_collection()\n\n        if where_document is not None:\n            raise ValueError(\n                \"where_document is not supported for OceanBaseStore.delete.\",\n            )\n\n        if ids is None and where is None:\n            raise ValueError(\n                \"At least one of ids or where must be provided for deletion.\",\n            )\n\n        await asyncio.to_thread(\n            self._client.delete,\n            collection_name=self.collection_name,\n            ids=ids,\n            flter=where,\n            **kwargs,\n        )\n\n    def get_client(self) -> MilvusLikeClient:\n        \"\"\"Get the underlying OceanBase client, so that developers can access\n        the full functionality of OceanBase.\n\n        Returns:\n            `MilvusLikeClient`:\n                The underlying OceanBase client.\n        \"\"\"\n        return self._client\n"
  },
  {
    "path": "src/agentscope/rag/_store/_qdrant_store.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The Qdrant local vector store implementation.\"\"\"\nimport json\nfrom typing import Any, Literal, TYPE_CHECKING\n\nfrom .._reader import Document\nfrom ._store_base import VDBStoreBase\nfrom .._document import DocMetadata\nfrom ..._utils._common import _map_text_to_uuid\nfrom ...types import Embedding\n\nif TYPE_CHECKING:\n    from qdrant_client import AsyncQdrantClient\nelse:\n    AsyncQdrantClient = \"qdrant_client.AsyncQdrantClient\"\n\n\nclass QdrantStore(VDBStoreBase):\n    \"\"\"The Qdrant vector store implementation, supporting both local and\n    remote Qdrant instances.\n\n    .. note:: In Qdrant, we use the ``payload`` field to store the metadata,\n    including the document ID, chunk ID, and original content.\n\n    \"\"\"\n\n    def __init__(\n        self,\n        location: Literal[\":memory:\"] | str,\n        collection_name: str,\n        dimensions: int,\n        distance: Literal[\"Cosine\", \"Euclid\", \"Dot\", \"Manhattan\"] = \"Cosine\",\n        client_kwargs: dict[str, Any] | None = None,\n        collection_kwargs: dict[str, Any] | None = None,\n    ) -> None:\n        \"\"\"Initialize the local Qdrant vector store.\n\n        Args:\n            location (`Literal[\":memory:\"] | str`):\n                The location of the Qdrant instance. Use \":memory:\" for\n                in-memory Qdrant instance, or url for remote Qdrant instance,\n                e.g. \"http://localhost:6333\" or a path to a directory.\n            collection_name (`str`):\n                The name of the collection to store the embeddings.\n            dimensions (`int`):\n                The dimension of the embeddings.\n            distance (`Literal[\"Cosine\", \"Euclid\", \"Dot\", \"Manhattan\"]`, \\\n            default to \"Cosine\"):\n                The distance metric to use for the collection. Can be one of\n                \"Cosine\", \"Euclid\", \"Dot\", or \"Manhattan\". Defaults to\n                \"Cosine\".\n            client_kwargs (`dict[str, Any] | None`, optional):\n                Other keyword arguments for the Qdrant client.\n            collection_kwargs (`dict[str, Any] | None`, optional):\n                Other keyword arguments for creating the collection.\n        \"\"\"\n\n        try:\n            from qdrant_client import AsyncQdrantClient\n        except ImportError as e:\n            raise ImportError(\n                \"Qdrant client is not installed. Please install it with \"\n                \"`pip install qdrant-client`.\",\n            ) from e\n\n        client_kwargs = client_kwargs or {}\n        self._client = AsyncQdrantClient(location=location, **client_kwargs)\n\n        self.collection_name = collection_name\n        self.dimensions = dimensions\n        self.distance = distance\n        self.collection_kwargs = collection_kwargs or {}\n\n    async def _validate_collection(self) -> None:\n        \"\"\"Validate the collection exists, if not, create it.\"\"\"\n        if not await self._client.collection_exists(self.collection_name):\n            from qdrant_client import models\n\n            collections_kwargs = {\n                \"collection_name\": self.collection_name,\n                \"vectors_config\": models.VectorParams(\n                    size=self.dimensions,\n                    distance=getattr(models.Distance, self.distance.upper()),\n                ),\n                **self.collection_kwargs,\n            }\n            await self._client.create_collection(**collections_kwargs)\n\n    async def add(self, documents: list[Document], **kwargs: Any) -> None:\n        \"\"\"Add embeddings to the Qdrant vector store.\n\n        Args:\n            documents (`list[Document]`):\n                A list of embedding records to be recorded in the Qdrant store.\n        \"\"\"\n        await self._validate_collection()\n\n        from qdrant_client.models import PointStruct\n\n        await self._client.upsert(\n            collection_name=self.collection_name,\n            points=[\n                PointStruct(\n                    id=_map_text_to_uuid(\n                        json.dumps(\n                            {\n                                \"doc_id\": _.metadata.doc_id,\n                                \"chunk_id\": _.metadata.chunk_id,\n                                \"content\": _.metadata.content,\n                            },\n                            ensure_ascii=False,\n                        ),\n                    ),\n                    vector=_.embedding,\n                    payload=_.metadata,\n                )\n                for _ in documents\n            ],\n        )\n\n    async def search(\n        self,\n        query_embedding: Embedding,\n        limit: int,\n        score_threshold: float | None = None,\n        **kwargs: Any,\n    ) -> list[Document]:\n        \"\"\"Search relevant documents from the Qdrant vector store.\n\n        Args:\n            query_embedding (`Embedding`):\n                The embedding of the query text.\n            limit (`int`):\n                The number of relevant documents to retrieve.\n            score_threshold (`float | None`, optional):\n                The threshold of the score to filter the results.\n            **kwargs (`Any`):\n                Other keyword arguments for the Qdrant client search API.\n        \"\"\"\n        res = await self._client.query_points(\n            collection_name=self.collection_name,\n            query=query_embedding,\n            limit=limit,\n            score_threshold=score_threshold,\n            **kwargs,\n        )\n\n        collected_res = []\n        for point in res.points:\n            collected_res.append(\n                Document(\n                    embedding=point.vector,\n                    score=point.score,\n                    metadata=DocMetadata(**point.payload),\n                ),\n            )\n        return collected_res\n\n    async def delete(self, *args: Any, **kwargs: Any) -> None:\n        \"\"\"Delete is not implemented for QdrantStore.\"\"\"\n        raise NotImplementedError(\n            \"Delete is not implemented for QdrantStore.\",\n        )\n\n    def get_client(self) -> AsyncQdrantClient:\n        \"\"\"Get the underlying Qdrant client, so that developers can access\n        the full functionality of Qdrant.\n\n        Returns:\n            `AsyncQdrantClient`:\n                The underlying Qdrant client.\n        \"\"\"\n        return self._client\n"
  },
  {
    "path": "src/agentscope/rag/_store/_store_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The embedding store base class.\"\"\"\nfrom abc import abstractmethod\nfrom typing import Any\n\nfrom .. import Document\nfrom ...types import Embedding\n\n\nclass VDBStoreBase:\n    \"\"\"The vector database store base class, serving as a middle layer between\n    the knowledge base and the actual vector database implementation.\"\"\"\n\n    @abstractmethod\n    async def add(self, documents: list[Document], **kwargs: Any) -> None:\n        \"\"\"Record the documents into the vector database.\"\"\"\n\n    @abstractmethod\n    async def delete(self, *args: Any, **kwargs: Any) -> None:\n        \"\"\"Delete texts from the embedding store.\"\"\"\n\n    @abstractmethod\n    async def search(\n        self,\n        query_embedding: Embedding,\n        limit: int,\n        score_threshold: float | None = None,\n        **kwargs: Any,\n    ) -> list[Document]:\n        \"\"\"Retrieve relevant texts for the given queries.\n\n        Args:\n            query_embedding (`Embedding`):\n                The embedding of the query text.\n            limit (`int`):\n                The number of relevant documents to retrieve.\n            score_threshold (`float | None`, optional):\n                The threshold of the score to filter the results.\n            **kwargs (`Any`):\n                Other keyword arguments for the vector database search API.\n        \"\"\"\n\n    def get_client(self) -> Any:\n        \"\"\"Get the underlying vector database client, so that developers can\n        access the full functionality of the vector database.\"\"\"\n        raise NotImplementedError(\n            \"``get_client`` is not implemented for \"\n            f\"{self.__class__.__name__}.\",\n        )\n"
  },
  {
    "path": "src/agentscope/realtime/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The realtime module in AgentScope, providing realtime models and events.\"\"\"\n\nfrom ._events import (\n    ModelEvents,\n    ModelEventType,\n    ServerEvents,\n    ServerEventType,\n    ClientEvents,\n    ClientEventType,\n)\nfrom ._base import RealtimeModelBase\nfrom ._dashscope_realtime_model import DashScopeRealtimeModel\nfrom ._openai_realtime_model import OpenAIRealtimeModel\nfrom ._gemini_realtime_model import GeminiRealtimeModel\n\n__all__ = [\n    \"ModelEventType\",\n    \"ModelEvents\",\n    \"ServerEventType\",\n    \"ServerEvents\",\n    \"ClientEventType\",\n    \"ClientEvents\",\n    \"RealtimeModelBase\",\n    \"DashScopeRealtimeModel\",\n    \"OpenAIRealtimeModel\",\n    \"GeminiRealtimeModel\",\n]\n"
  },
  {
    "path": "src/agentscope/realtime/_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The realtime model base class.\"\"\"\nimport asyncio\nimport json\nfrom abc import abstractmethod\nfrom asyncio import Queue\nfrom typing import Any\n\nfrom ._events import ModelEvents\nfrom ..message import AudioBlock, TextBlock, ImageBlock, ToolResultBlock\n\n\nclass RealtimeModelBase:\n    \"\"\"The realtime model base class.\"\"\"\n\n    model_name: str\n    \"\"\"The model name\"\"\"\n\n    support_input_modalities: list[str]\n    \"\"\"The supported input modalities of the DashScope realtime model.\"\"\"\n\n    websocket_url: str\n    \"\"\"The websocket URL of the realtime model API.\"\"\"\n\n    websocket_headers: dict[str, str]\n    \"\"\"The websocket headers of the realtime model API.\"\"\"\n\n    input_sample_rate: int\n    \"\"\"The input audio sample rate.\"\"\"\n\n    output_sample_rate: int\n    \"\"\"The output audio sample rate.\"\"\"\n\n    def __init__(\n        self,\n        model_name: str,\n    ) -> None:\n        \"\"\"Initialize the RealtimeModelBase class.\n\n        Args:\n            model_name (`str`):\n                The model name.\n        \"\"\"\n\n        self.model_name = model_name\n\n        # The incoming queue to handle the data returned from the realtime\n        # model API.\n        self._incoming_queue = Queue()\n        self._incoming_task = None\n\n        from websockets import ClientConnection\n\n        self._websocket: ClientConnection | None = None\n\n    @abstractmethod\n    async def send(\n        self,\n        data: AudioBlock | TextBlock | ImageBlock | ToolResultBlock,\n    ) -> None:\n        \"\"\"Send data to the realtime model for processing.\n\n        Args:\n            data (`AudioBlock | TextBlock | ImageBlock | ToolResultBlock`):\n                The data to be sent to the realtime model.\n        \"\"\"\n\n    async def connect(\n        self,\n        outgoing_queue: Queue,\n        instructions: str,\n        tools: list[dict] | None = None,\n    ) -> None:\n        \"\"\"Establish a connection to the realtime model.\n\n        Args:\n            outgoing_queue (`Queue`):\n                The queue to push the model responses to the outside.\n            instructions (`str`):\n                The instructions to guide the realtime model's behavior.\n            tools (`list[dict]`, *optional*):\n                The list of tools JSON schemas.\n        \"\"\"\n        import websockets\n\n        self._websocket = await websockets.connect(\n            self.websocket_url,\n            additional_headers=self.websocket_headers,\n        )\n\n        self._incoming_task = asyncio.create_task(\n            self._receive_model_event_loop(outgoing_queue),\n        )\n\n        # Updating the session with instructions and other configurations\n        session_config = self._build_session_config(instructions, tools)\n        await self._websocket.send(\n            json.dumps(session_config, ensure_ascii=False),\n        )\n\n    @abstractmethod\n    def _build_session_config(\n        self,\n        instructions: str,\n        tools: list[dict] | None,\n        **kwargs: Any,\n    ) -> dict:\n        \"\"\"Build the session configuration message to initialize or update\n        the realtime model session.\n\n        Args:\n            instructions (`str`):\n                The instructions to guide the realtime model's behavior.\n            tools (`list[dict]`, optional):\n                The list of tools available to the realtime model.\n            **kwargs (`Any`):\n                Additional keyword arguments for session configuration.\n\n        Returns:\n            `dict`:\n                The session configuration message.\n        \"\"\"\n\n    async def disconnect(self) -> None:\n        \"\"\"Close the connection to the realtime model.\"\"\"\n        # TODO: session ended\n\n        if self._incoming_task and not self._incoming_task.done():\n            self._incoming_task.cancel()\n\n        if self._websocket:\n            await self._websocket.close()\n\n    async def _receive_model_event_loop(self, outgoing_queue: Queue) -> None:\n        \"\"\"The loop to receive and handle the model responses.\n\n        Args:\n            outgoing_queue (`Queue`):\n                The queue to push the model responses to the outside.\n        \"\"\"\n\n        async for message in self._websocket:\n            if isinstance(message, bytes):\n                message = message.decode(\"utf-8\")\n\n            # Parse the message into ModelEvent instance(s)\n            events = await self.parse_api_message(message)\n\n            if events is None:\n                continue\n\n            if isinstance(events, ModelEvents.EventBase):\n                events = [events]\n\n            for event in events:\n                # Send the event to the outgoing queue\n                await outgoing_queue.put(event)\n\n    @abstractmethod\n    async def parse_api_message(\n        self,\n        message: str,\n    ) -> ModelEvents.EventBase | list[ModelEvents.EventBase] | None:\n        \"\"\"Parse the message received from the realtime model API.\n\n        Args:\n            message (`str`):\n                The message received from the realtime model API.\n\n        Returns:\n            `ModelEvents.EventBase | list[ModelEvents.EventBase] | None`:\n                The unified model event(s) in agentscope format.\n        \"\"\"\n"
  },
  {
    "path": "src/agentscope/realtime/_dashscope_realtime_model.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The dashscope realtime model class.\"\"\"\nimport json\nfrom typing import Literal, Any\n\nimport shortuuid\n\nfrom ._events import ModelEvents\nfrom ._base import RealtimeModelBase\nfrom .._logging import logger\nfrom .._utils._common import _get_bytes_from_web_url\nfrom ..message import AudioBlock, TextBlock, ImageBlock, ToolResultBlock\n\n\nclass DashScopeRealtimeModel(RealtimeModelBase):\n    \"\"\"The DashScope realtime model class.\n\n    TODO:\n     - Support non-VAD mode\n     - Support update session config during the session\n    \"\"\"\n\n    support_input_modalities: list[str] = [\"text\", \"audio\", \"image\"]\n    \"\"\"The supported input modalities of the DashScope realtime model.\"\"\"\n\n    support_tools: bool = False\n    \"\"\"The DashScope Realtime API doesn't support tools yet (last updated in\n    20260129).\"\"\"\n\n    websocket_url: str = (\n        \"wss://dashscope.aliyuncs.com/api-ws/v1/realtime?model={model_name}\"\n    )\n    \"\"\"The websocket URL of the DashScope realtime model API.\"\"\"\n\n    websocket_headers: dict[str, str]\n    \"\"\"The websocket headers of the DashScope realtime model API.\"\"\"\n\n    input_sample_rate: int\n    \"\"\"The input audio sample rate.\"\"\"\n\n    output_sample_rate: int\n    \"\"\"The output audio sample rate.\"\"\"\n\n    def __init__(\n        self,\n        model_name: str,\n        api_key: str,\n        voice: str\n        | Literal[\"Cherry\", \"Serena\", \"Ethan\", \"Chelsie\"] = \"Cherry\",\n        enable_input_audio_transcription: bool = True,\n    ) -> None:\n        \"\"\"Initialize the DashScopeRealtimeModel class.\n\n        Args:\n            model_name (`str`):\n                The model name, e.g. \"qwen3-omni-flash-realtime\".\n            api_key (`str`):\n                The API key for authentication.\n            voice (`str | Literal[\"Cherry\", \"Serena\", \"Ethan\", \"Chelsie\"]`, \\\n            defaults to `\"Cherry\"`):\n                The voice to be used for text-to-speech.\n            enable_input_audio_transcription (`bool`, defaults to `True`):\n                Whether to enable input audio transcription.\n        \"\"\"\n        super().__init__(model_name)\n\n        self.voice = voice\n        self.enable_input_audio_transcription = (\n            enable_input_audio_transcription\n        )\n\n        # The dashscope realtime API requires 16kHz input sample rate\n        # for all models.\n        self.input_sample_rate = 16000\n\n        # The output sample rate depends on the model.\n        # For \"qwen3-omni-flash-realtime\" models, it's 24kHz.\n        # For others, it's 16kHz.\n        if model_name.startswith(\"qwen3-omni-flash-realtime\"):\n            self.output_sample_rate = 24000\n        else:\n            self.output_sample_rate = 16000\n\n        # Set the model name in the websocket URL.\n        self.websocket_url = self.websocket_url.format(model_name=model_name)\n\n        # Set the API key in the websocket headers.\n        self.websocket_headers = {\n            \"Authorization\": f\"Bearer {api_key}\",\n            \"X-DashScope-DataInspection\": \"disable\",\n        }\n\n        # Record the response ID for the current session.\n        self._response_id = \"\"\n\n    def _build_session_config(\n        self,\n        instructions: str,\n        tools: list[dict] | None,\n        **kwargs: Any,\n    ) -> dict:\n        \"\"\"Build the session configuration.\"\"\"\n        session_config: dict = {\n            \"instructions\": instructions,\n            # The output modalities of the model\n            \"modalities\": [\"audio\", \"text\"],\n            \"input_audio_format\": \"pcm\" + str(self.input_sample_rate // 1000),\n            \"output_audio_format\": \"pcm\"\n            + str(self.output_sample_rate // 1000),\n            \"voice\": self.voice,\n            **kwargs,\n        }\n\n        # Input audio transcription\n        if self.enable_input_audio_transcription:\n            session_config[\"input_audio_transcription\"] = {\n                \"model\": \"gummy-realtime-v1\",\n            }\n\n        # By default, we enable the VAD capability\n        # TODO: support none-VAD mode\n        session_config[\"turn_detection\"] = {\n            \"type\": \"server_vad\",\n            \"threshold\": 0.5,\n            \"silence_duration_ms\": 800,\n        }\n\n        return {\n            \"type\": \"session.update\",\n            \"session\": session_config,\n        }\n\n    async def send(\n        self,\n        data: AudioBlock | TextBlock | ImageBlock | ToolResultBlock,\n    ) -> None:\n        \"\"\"Send the data to the DashScope realtime model for processing.\n\n        .. note:: The DashScope Realtime API currently only supports audio and\n        image data input.\n\n        Args:\n            data (`AudioBlock | TextBlock | ImageBlock | ToolResultBlock`):\n                The data to be sent to the DashScope realtime model.\n        \"\"\"\n        from websockets import State\n\n        if not self._websocket or self._websocket.state != State.OPEN:\n            raise RuntimeError(\n                f\"WebSocket is not connected for model {self.model_name}. \"\n                \"Call the `connect` method first.\",\n            )\n\n        # Type checking\n        assert (\n            isinstance(data, dict) and \"type\" in data\n        ), \"Data must be a dict with a 'type' field.\"\n\n        # The source must be base64 for audio data\n        data_type = data.get(\"type\")\n\n        if data_type not in self.support_input_modalities:\n            logger.warning(\n                \"DashScope Realtime API does not support %s data input. \"\n                \"Supported modalities are: %s\",\n                data_type,\n                \", \".join(self.support_input_modalities),\n            )\n            return\n\n        # Process the data based on its type\n        if data_type == \"image\":\n            to_send_message = await self._parse_image_data(\n                ImageBlock(\n                    type=\"image\",\n                    source=data.get(\"source\"),\n                ),\n            )\n\n        elif data_type == \"audio\":\n            to_send_message = await self._parse_audio_data(\n                AudioBlock(\n                    type=\"audio\",\n                    source=data.get(\"source\"),\n                ),\n            )\n\n        elif data_type == \"text\":\n            # TODO: The following code doesn't work and cannot support text\n            #  input yet.\n            to_send_message = json.dumps(\n                {\n                    \"event_id\": shortuuid.uuid(),\n                    \"type\": \"response.create\",\n                    \"response\": {\n                        \"instructions\": data.get(\"text\", \"\"),\n                    },\n                },\n                ensure_ascii=False,\n            )\n\n        else:\n            raise ValueError(\n                f\"Unsupported data type: {data_type}\",\n            )\n\n        await self._websocket.send(to_send_message)\n\n    async def parse_api_message(\n        self,\n        message: str,\n    ) -> ModelEvents.EventBase | list[ModelEvents.EventBase] | None:\n        \"\"\"Parse the message received from the DashScope realtime model API.\n\n        Args:\n            message (`str`):\n                The message received from the DashScope realtime model API.\n\n        Returns:\n            `ModelEvents.EventBase | list[ModelEvents.EventBase] | None`:\n                The unified model event(s) in agentscope format.\n        \"\"\"\n        try:\n            data = json.loads(message)\n        except json.decoder.JSONDecodeError:\n            return None\n\n        if not isinstance(data, dict):\n            return None\n\n        model_event = None\n        match data.get(\"type\", \"\"):\n            # ================ Session related events ================\n            case \"session.created\":\n                model_event = ModelEvents.ModelSessionCreatedEvent(\n                    session_id=data.get(\"session\", {}).get(\"id\", \"\"),\n                )\n\n            case \"session.updated\":\n                # TODO: handle the session updated event\n                pass\n\n            # ================ Response related events ================\n            case \"response.created\":\n                self._response_id = data.get(\"response\", {}).get(\"id\", \"\")\n                model_event = ModelEvents.ModelResponseCreatedEvent(\n                    response_id=self._response_id,\n                )\n\n            case \"response.done\":\n                response = data.get(\"response\", {})\n                response_id = response.get(\"id\", \"\") or self._response_id\n                usage = response.get(\"usage\", {})\n                model_event = ModelEvents.ModelResponseDoneEvent(\n                    response_id=response_id,\n                    input_tokens=usage.get(\"input_tokens\", 0),\n                    output_tokens=usage.get(\"output_tokens\", 0),\n                )\n                # clear the response id\n                self._response_id = \"\"\n\n            case \"response.audio.delta\":\n                audio_data = data.get(\"delta\", \"\")\n                if audio_data:\n                    model_event = ModelEvents.ModelResponseAudioDeltaEvent(\n                        response_id=self._response_id,\n                        item_id=data.get(\"item_id\", \"\"),\n                        delta=audio_data,\n                        format={\n                            \"type\": \"audio/pcm\",\n                            \"rate\": self.output_sample_rate,\n                        },\n                    )\n\n            case \"response.audio.done\":\n                model_event = ModelEvents.ModelResponseAudioDoneEvent(\n                    response_id=self._response_id,\n                    item_id=data.get(\"item_id\", \"\"),\n                )\n\n            # ================ Transcription related events ================\n\n            case \"response.audio_transcript.delta\":\n                transcript_data = data.get(\"delta\", \"\")\n                if transcript_data:\n                    model_event = (\n                        ModelEvents.ModelResponseAudioTranscriptDeltaEvent(\n                            response_id=self._response_id,\n                            delta=transcript_data,\n                            item_id=data.get(\"item_id\", \"\"),\n                        )\n                    )\n\n            case \"response.audio_transcript.done\":\n                model_event = (\n                    ModelEvents.ModelResponseAudioTranscriptDoneEvent(\n                        response_id=self._response_id,\n                        item_id=data.get(\"item_id\", \"\"),\n                    )\n                )\n\n            case \"conversation.item.input_audio_transcription.completed\":\n                transcript_data = data.get(\"transcript\", \"\")\n                if transcript_data:\n                    model_event = ModelEvents.ModelInputTranscriptionDoneEvent(\n                        transcript=transcript_data,\n                        item_id=data.get(\"item_id\", \"\"),\n                    )\n\n            # ================= VAD related events =================\n            case \"input_audio_buffer.speech_started\":\n                model_event = ModelEvents.ModelInputStartedEvent(\n                    item_id=data.get(\"item_id\", \"\"),\n                    audio_start_ms=data.get(\"audio_start_ms\", 0),\n                )\n\n            case \"input_audio_buffer.speech_stopped\":\n                model_event = ModelEvents.ModelInputDoneEvent(\n                    item_id=data.get(\"item_id\", \"\"),\n                    audio_end_ms=data.get(\"audio_end_ms\", 0),\n                )\n\n            # ================= Error events =================\n            case \"error\":\n                error = data.get(\"error\", {})\n                model_event = ModelEvents.ModelErrorEvent(\n                    error_type=error.get(\"type\", \"unknown\"),\n                    code=error.get(\"code\", \"unknown\"),\n                    message=error.get(\"message\", \"An unknown error occurred.\"),\n                )\n\n            # ================= Unknown events =================\n            case _:\n                logger.debug(\n                    \"Unknown DashScope realtime model event type: %s\",\n                    data.get(\"type\", None),\n                )\n\n        return model_event\n\n    async def _parse_image_data(self, block: ImageBlock) -> str:\n        \"\"\"Parse the image data block to the format required by the DashScope\n        realtime model API.\n\n        Args:\n            block (`ImageBlock`):\n                The image data block.\n\n        Returns:\n            `str`: The parsed message to be sent to the DashScope realtime\n            model API.\n        \"\"\"\n        if block[\"source\"][\"type\"] == \"base64\":\n            return json.dumps(\n                {\n                    \"type\": \"input_image_buffer.append\",\n                    \"image\": block[\"source\"][\"data\"],\n                },\n            )\n\n        if block[\"source\"][\"type\"] == \"url\":\n            image = _get_bytes_from_web_url(block[\"source\"][\"url\"])\n            return json.dumps(\n                {\n                    \"type\": \"input_image_url.append\",\n                    \"image_url\": image,\n                },\n            )\n\n        raise ValueError(\n            f\"Unsupported image source type: {block['source']['type']}\",\n        )\n\n    async def _parse_audio_data(self, block: AudioBlock) -> str:\n        \"\"\"Parse the audio data block to the format required by the DashScope\n        realtime model API.\n\n        Args:\n            block (`AudioBlock`):\n                The audio data block.\n\n        Returns:\n            `str`: The parsed message to be sent to the DashScope realtime\n            model API.\n        \"\"\"\n        source_type = block[\"source\"][\"type\"]\n\n        if source_type == \"base64\":\n            audio_data = block[\"source\"][\"data\"]\n\n        elif source_type == \"url\":\n            audio_data = _get_bytes_from_web_url(block[\"source\"][\"url\"])\n\n        else:\n            raise ValueError(\n                f\"Unsupported audio source type: {source_type}\",\n            )\n\n        return json.dumps(\n            {\n                \"type\": \"input_audio_buffer.append\",\n                \"audio\": audio_data,\n            },\n        )\n"
  },
  {
    "path": "src/agentscope/realtime/_events/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The events in the realtime module.\"\"\"\n\nfrom ._model_event import ModelEvents, ModelEventType\nfrom ._client_event import ClientEvents, ClientEventType\nfrom ._server_event import ServerEvents, ServerEventType\n\n__all__ = [\n    \"ModelEventType\",\n    \"ModelEvents\",\n    \"ClientEventType\",\n    \"ClientEvents\",\n    \"ServerEventType\",\n    \"ServerEvents\",\n]\n"
  },
  {
    "path": "src/agentscope/realtime/_events/_client_event.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The client events for web-to-backend communication.\"\"\"\nfrom enum import Enum\nfrom typing import List\n\nfrom pydantic import BaseModel\n\nfrom ._utils import AudioFormat\nfrom ...message import TextBlock, AudioBlock, ImageBlock, VideoBlock\n\n\nclass ClientEventType(str, Enum):\n    \"\"\"Types of client events for web-to-backend communication.\"\"\"\n\n    # ============== Session control ================\n    CLIENT_SESSION_CREATE = \"client_session_create\"\n    \"\"\"The user creates a new session in the frontend.\"\"\"\n\n    CLIENT_SESSION_END = \"client_session_end\"\n    \"\"\"The user ends the current session in the frontend.\"\"\"\n\n    # ============== Response control ================\n    CLIENT_RESPONSE_CREATE = \"client_response_create\"\n    \"\"\"The user requests the agent to generate a response immediately.\"\"\"\n\n    CLIENT_RESPONSE_CANCEL = \"client_response_cancel\"\n    \"\"\"The user interrupts the agent's current response generation.\"\"\"\n\n    CLIENT_IMAGE_APPEND = \"client_image_append\"\n    \"\"\"The user appends an image input to the current session.\"\"\"\n\n    CLIENT_TEXT_APPEND = \"client_text_append\"\n    \"\"\"The user appends a text input to the current session.\"\"\"\n\n    CLIENT_AUDIO_APPEND = \"client_audio_append\"\n    \"\"\"The user appends an audio input to the current session.\"\"\"\n\n    CLIENT_AUDIO_COMMIT = \"client_audio_commit\"\n    \"\"\"The user commits the audio input to signal end of input.\"\"\"\n\n    CLIENT_TOOL_RESULT = \"client_tool_result\"\n    \"\"\"The tool result executed in the frontend is sent back to the backend.\"\"\"\n\n\nclass ClientEvents:\n    \"\"\"Realtime client events.\"\"\"\n\n    class EventBase(BaseModel):\n        \"\"\"The base class for all client events, used to unify the type\n        hinting.\"\"\"\n\n    class ClientSessionCreateEvent(EventBase):\n        \"\"\"Session create event in the frontend\"\"\"\n\n        type: ClientEventType = ClientEventType.CLIENT_SESSION_CREATE\n        \"\"\"The event type.\"\"\"\n\n        config: dict\n        \"\"\"The session config.\"\"\"\n\n    class ClientSessionEndEvent(EventBase):\n        \"\"\"Session end event in the frontend\"\"\"\n\n        type: ClientEventType = ClientEventType.CLIENT_SESSION_END\n        \"\"\"The event type.\"\"\"\n\n        session_id: str\n        \"\"\"The session ID.\"\"\"\n\n    class ClientResponseCreateEvent(EventBase):\n        \"\"\"Response create event in the frontend\"\"\"\n\n        type: ClientEventType = ClientEventType.CLIENT_RESPONSE_CREATE\n        \"\"\"The event type.\"\"\"\n\n        session_id: str\n        \"\"\"The session ID.\"\"\"\n\n    class ClientResponseCancelEvent(EventBase):\n        \"\"\"Response cancel event in the frontend\"\"\"\n\n        type: ClientEventType = ClientEventType.CLIENT_RESPONSE_CANCEL\n        \"\"\"The event type.\"\"\"\n\n        session_id: str\n        \"\"\"The session ID.\"\"\"\n\n    class ClientImageAppendEvent(EventBase):\n        \"\"\"Image append event in the frontend\"\"\"\n\n        type: ClientEventType = ClientEventType.CLIENT_IMAGE_APPEND\n        \"\"\"The event type.\"\"\"\n\n        session_id: str\n        \"\"\"The session ID.\"\"\"\n\n        image: str\n        \"\"\"The image data, encoded as base64 string.\"\"\"\n\n        format: dict\n        \"\"\"The image format information.\"\"\"\n\n    class ClientTextAppendEvent(EventBase):\n        \"\"\"Text append event in the frontend\"\"\"\n\n        type: ClientEventType = ClientEventType.CLIENT_TEXT_APPEND\n        \"\"\"The event type.\"\"\"\n\n        session_id: str\n        \"\"\"The session ID.\"\"\"\n\n        text: str\n        \"\"\"The text data.\"\"\"\n\n    class ClientAudioAppendEvent(EventBase):\n        \"\"\"Audio append event in the frontend\"\"\"\n\n        type: ClientEventType = ClientEventType.CLIENT_AUDIO_APPEND\n        \"\"\"The event type.\"\"\"\n\n        session_id: str\n        \"\"\"The session ID.\"\"\"\n\n        audio: str\n        \"\"\"The audio data, encoded as base64 string.\"\"\"\n\n        format: AudioFormat\n        \"\"\"The audio format information.\"\"\"\n\n    class ClientAudioCommitEvent(EventBase):\n        \"\"\"Audio commit event in the frontend\"\"\"\n\n        type: ClientEventType = ClientEventType.CLIENT_AUDIO_COMMIT\n        \"\"\"The event type.\"\"\"\n\n        session_id: str\n        \"\"\"The session ID.\"\"\"\n\n    class ClientToolResultEvent(EventBase):\n        \"\"\"Tool result event in the frontend\"\"\"\n\n        type: ClientEventType = ClientEventType.CLIENT_TOOL_RESULT\n        \"\"\"The event type.\"\"\"\n\n        session_id: str\n        \"\"\"The session ID.\"\"\"\n\n        id: str\n        \"\"\"The tool call ID.\"\"\"\n\n        name: str\n        \"\"\"The tool name.\"\"\"\n\n        output: str | List[TextBlock | ImageBlock | AudioBlock | VideoBlock]\n        \"\"\"The tool result.\"\"\"\n\n    MAPPING = {\n        ClientEventType.CLIENT_SESSION_CREATE: ClientSessionCreateEvent,\n        ClientEventType.CLIENT_SESSION_END: ClientSessionEndEvent,\n        ClientEventType.CLIENT_RESPONSE_CREATE: ClientResponseCreateEvent,\n        ClientEventType.CLIENT_RESPONSE_CANCEL: ClientResponseCancelEvent,\n        ClientEventType.CLIENT_IMAGE_APPEND: ClientImageAppendEvent,\n        ClientEventType.CLIENT_TEXT_APPEND: ClientTextAppendEvent,\n        ClientEventType.CLIENT_AUDIO_APPEND: ClientAudioAppendEvent,\n        ClientEventType.CLIENT_AUDIO_COMMIT: ClientAudioCommitEvent,\n        ClientEventType.CLIENT_TOOL_RESULT: ClientToolResultEvent,\n    }\n\n    @classmethod\n    def from_json(cls, json_data: dict) -> EventBase:\n        \"\"\"Parse the client event from JSON data and return the corresponding\n        event instance.\n\n        Args:\n            json_data (`dict`):\n                The JSON data, which must contain the \"type\" field.\n\n        Raises:\n            `ValueError`:\n                If the event type is unknown.\n\n        Returns:\n            `ClientEvents.EventBase`:\n                The corresponding client event instance.\n        \"\"\"\n        if not isinstance(json_data, dict) or \"type\" not in json_data:\n            raise ValueError(\n                f\"Invalid JSON data for ClientEvent: {json_data}\",\n            )\n\n        event_type = json_data[\"type\"]\n\n        if event_type not in cls.MAPPING:\n            raise ValueError(f\"Unknown ClientEvent type: {event_type}\")\n\n        # Obtain the event class from the mapping\n        event_class = cls.MAPPING[event_type]\n        return event_class(**json_data)\n"
  },
  {
    "path": "src/agentscope/realtime/_events/_model_event.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The unified event from realtime model APIs in AgentScope, which will be\nconsumed by the realtime agents.\"\"\"\nfrom enum import Enum\nfrom typing import Literal\n\nfrom pydantic import BaseModel\n\nfrom ._utils import AudioFormat\nfrom ...message import ToolUseBlock\n\n\nclass ModelEventType(str, Enum):\n    \"\"\"Types of model events from the API.\"\"\"\n\n    # API session lifecycle\n    MODEL_SESSION_CREATED = \"model_session_created\"\n    \"\"\"The realtime API session has been created.\"\"\"\n\n    MODEL_SESSION_ENDED = \"model_session_ended\"\n    \"\"\"The realtime API session has ended.\"\"\"\n\n    # ============= MODEL GENERATED EVENTS =============\n\n    MODEL_RESPONSE_CREATED = \"model_response_created\"\n    \"\"\"The realtime model begins generating a response.\"\"\"\n\n    MODEL_RESPONSE_DONE = \"model_response_done\"\n    \"\"\"The realtime model has finished generating a response.\"\"\"\n\n    # ============= MODEL RESPONSE CONTENT EVENTS =============\n\n    MODEL_RESPONSE_AUDIO_DELTA = \"model_response_audio_delta\"\n    \"\"\"The realtime model response audio delta event.\"\"\"\n\n    MODEL_RESPONSE_AUDIO_DONE = \"model_response_audio_done\"\n    \"\"\"The realtime model response audio done event.\"\"\"\n\n    MODEL_RESPONSE_AUDIO_TRANSCRIPT_DELTA = (\n        \"model_response_audio_transcript_delta\"\n    )\n    \"\"\"The realtime model response audio transcript delta event.\"\"\"\n\n    MODEL_RESPONSE_AUDIO_TRANSCRIPT_DONE = (\n        \"model_response_audio_transcript_done\"\n    )\n    \"\"\"The realtime model response audio transcript done event.\"\"\"\n\n    MODEL_RESPONSE_TOOL_USE_DELTA = \"model_response_tool_use_delta\"\n    \"\"\"The realtime model response tool use delta event.\"\"\"\n\n    MODEL_RESPONSE_TOOL_USE_DONE = \"model_response_tool_use_done\"\n    \"\"\"The realtime model response tool use done event.\"\"\"\n\n    # Input transcription\n    MODEL_INPUT_TRANSCRIPTION_DELTA = \"model_input_transcription_delta\"\n    \"\"\"The input transcription delta event.\"\"\"\n\n    MODEL_INPUT_TRANSCRIPTION_DONE = \"model_input_transcription_done\"\n    \"\"\"The input transcription done event.\"\"\"\n\n    # Input detection (VAD)\n    MODEL_INPUT_STARTED = \"model_input_started\"\n    \"\"\"The input has started event.\"\"\"\n\n    MODEL_INPUT_DONE = \"model_input_done\"\n    \"\"\"The input has done event.\"\"\"\n\n    # ============= ERROR EVENTS =============\n\n    MODEL_ERROR = \"model_error\"\n    \"\"\"An error event from the realtime model API.\"\"\"\n\n    # ============ WebSocket Events ============\n\n    # WebSocket events (if used)\n    MODEL_WEBSOCKET_CONNECT = \"model_websocket_connect\"\n    \"\"\"The model WebSocket has connected.\"\"\"\n\n    MODEL_WEBSOCKET_DISCONNECT = \"model_websocket_disconnect\"\n    \"\"\"The model WebSocket has disconnected.\"\"\"\n\n\nclass ModelEvents:\n    \"\"\"The realtime model events that will be consumed by the realtime\n    agents\"\"\"\n\n    class EventBase(BaseModel):\n        \"\"\"The base class for all model events, used to unify the type\n        hinting.\"\"\"\n\n    class ModelSessionCreatedEvent(EventBase):\n        \"\"\"Realtime model session created event.\n\n        .. note:: This session is the connection between the realtime API and\n              the client, not the conversation session.\n        \"\"\"\n\n        session_id: str\n        \"\"\"The session ID.\"\"\"\n\n        type: Literal[\n            ModelEventType.MODEL_SESSION_CREATED\n        ] = ModelEventType.MODEL_SESSION_CREATED\n        \"\"\"The event type.\"\"\"\n\n    class ModelSessionEndedEvent(EventBase):\n        \"\"\"Session ended event.\n\n        .. note:: This session is the connection between the realtime API and\n              the client, not the conversation session.\n        \"\"\"\n\n        session_id: str\n        \"\"\"The session ID.\"\"\"\n\n        reason: str\n        \"\"\"The reason for session end.\"\"\"\n\n        type: Literal[\n            ModelEventType.MODEL_SESSION_ENDED\n        ] = ModelEventType.MODEL_SESSION_ENDED\n        \"\"\"The event type.\"\"\"\n\n    class ModelResponseCreatedEvent(EventBase):\n        \"\"\"The realtime model begins generating a response.\"\"\"\n\n        response_id: str\n        \"\"\"The response ID.\"\"\"\n\n        type: Literal[\n            ModelEventType.MODEL_RESPONSE_CREATED\n        ] = ModelEventType.MODEL_RESPONSE_CREATED\n        \"\"\"The event type.\"\"\"\n\n    class ModelResponseDoneEvent(EventBase):\n        \"\"\"Model response done event.\"\"\"\n\n        response_id: str\n        \"\"\"The response ID.\"\"\"\n\n        input_tokens: int\n        \"\"\"The number of input tokens.\"\"\"\n\n        output_tokens: int\n        \"\"\"The number of output tokens.\"\"\"\n\n        metadata: dict[str, str] = {}\n        \"\"\"Additional metadata.\"\"\"\n\n        type: Literal[\n            ModelEventType.MODEL_RESPONSE_DONE\n        ] = ModelEventType.MODEL_RESPONSE_DONE\n        \"\"\"The event type.\"\"\"\n\n    class ModelResponseAudioDeltaEvent(EventBase):\n        \"\"\"Model response audio delta event.\"\"\"\n\n        response_id: str\n        \"\"\"The response ID.\"\"\"\n\n        item_id: str\n        \"\"\"The conversation item ID.\"\"\"\n\n        delta: str\n        \"\"\"The audio chunk data, encoded in base64.\"\"\"\n\n        format: AudioFormat\n        \"\"\"The audio format information.\"\"\"\n\n        type: Literal[\n            ModelEventType.MODEL_RESPONSE_AUDIO_DELTA\n        ] = ModelEventType.MODEL_RESPONSE_AUDIO_DELTA\n        \"\"\"The event type.\"\"\"\n\n    class ModelResponseAudioDoneEvent(EventBase):\n        \"\"\"Model response audio done event.\"\"\"\n\n        response_id: str\n        \"\"\"The response ID.\"\"\"\n\n        item_id: str\n        \"\"\"The conversation item ID.\"\"\"\n\n        type: Literal[\n            ModelEventType.MODEL_RESPONSE_AUDIO_DONE\n        ] = ModelEventType.MODEL_RESPONSE_AUDIO_DONE\n        \"\"\"The event type.\"\"\"\n\n    class ModelResponseAudioTranscriptDeltaEvent(EventBase):\n        \"\"\"Model response audio transcript delta event.\"\"\"\n\n        response_id: str\n        \"\"\"The response ID.\"\"\"\n\n        item_id: str\n        \"\"\"The conversation item ID.\"\"\"\n\n        delta: str\n        \"\"\"The transcript chunk data.\"\"\"\n\n        type: Literal[\n            ModelEventType.MODEL_RESPONSE_AUDIO_TRANSCRIPT_DELTA\n        ] = ModelEventType.MODEL_RESPONSE_AUDIO_TRANSCRIPT_DELTA\n        \"\"\"The event type.\"\"\"\n\n    class ModelResponseAudioTranscriptDoneEvent(EventBase):\n        \"\"\"Model response audio transcript done event.\"\"\"\n\n        response_id: str\n        \"\"\"The response ID.\"\"\"\n\n        item_id: str\n        \"\"\"The conversation item ID.\"\"\"\n\n        type: Literal[\n            ModelEventType.MODEL_RESPONSE_AUDIO_TRANSCRIPT_DONE\n        ] = ModelEventType.MODEL_RESPONSE_AUDIO_TRANSCRIPT_DONE\n        \"\"\"The event type.\"\"\"\n\n    class ModelResponseToolUseDeltaEvent(EventBase):\n        \"\"\"Model response tool use delta event.\"\"\"\n\n        response_id: str\n        \"\"\"The response ID.\"\"\"\n\n        item_id: str\n        \"\"\"The response item ID.\"\"\"\n\n        tool_use: ToolUseBlock\n        \"\"\"The tool use block delta, the arguments are accumulated in the\n        `raw_input` field.\"\"\"\n\n        type: Literal[\n            ModelEventType.MODEL_RESPONSE_TOOL_USE_DELTA\n        ] = ModelEventType.MODEL_RESPONSE_TOOL_USE_DELTA\n        \"\"\"The event type.\"\"\"\n\n    class ModelResponseToolUseDoneEvent(EventBase):\n        \"\"\"Model response tool use done event.\"\"\"\n\n        response_id: str\n        \"\"\"The response ID.\"\"\"\n\n        item_id: str\n        \"\"\"The response item ID.\"\"\"\n\n        tool_use: ToolUseBlock\n        \"\"\"The complete tool use block.\"\"\"\n\n        type: Literal[\n            ModelEventType.MODEL_RESPONSE_TOOL_USE_DONE\n        ] = ModelEventType.MODEL_RESPONSE_TOOL_USE_DONE\n        \"\"\"The event type.\"\"\"\n\n    class ModelInputTranscriptionDeltaEvent(EventBase):\n        \"\"\"Input transcription delta event.\"\"\"\n\n        item_id: str\n        \"\"\"The conversation item ID.\"\"\"\n\n        delta: str\n        \"\"\"The transcription delta.\"\"\"\n\n        type: Literal[\n            ModelEventType.MODEL_INPUT_TRANSCRIPTION_DELTA\n        ] = ModelEventType.MODEL_INPUT_TRANSCRIPTION_DELTA\n        \"\"\"The event type.\"\"\"\n\n    class ModelInputTranscriptionDoneEvent(EventBase):\n        \"\"\"Input transcription done event.\"\"\"\n\n        transcript: str\n        \"\"\"The complete transcription.\"\"\"\n\n        item_id: str\n        \"\"\"The conversation item ID.\"\"\"\n\n        input_tokens: int | None = None\n        \"\"\"The number of input tokens.\"\"\"\n\n        output_tokens: int | None = None\n        \"\"\"The number of output tokens.\"\"\"\n\n        type: Literal[\n            ModelEventType.MODEL_INPUT_TRANSCRIPTION_DONE\n        ] = ModelEventType.MODEL_INPUT_TRANSCRIPTION_DONE\n        \"\"\"The event type.\"\"\"\n\n    class ModelInputStartedEvent(EventBase):\n        \"\"\"Input started event.\"\"\"\n\n        item_id: str\n        \"\"\"The conversation item ID.\"\"\"\n\n        audio_start_ms: int\n        \"\"\"The audio start time in milliseconds.\"\"\"\n\n        type: Literal[\n            ModelEventType.MODEL_INPUT_STARTED\n        ] = ModelEventType.MODEL_INPUT_STARTED\n        \"\"\"The event type.\"\"\"\n\n    class ModelInputDoneEvent(EventBase):\n        \"\"\"Input done event.\"\"\"\n\n        item_id: str\n        \"\"\"The conversation item ID.\"\"\"\n\n        audio_end_ms: int\n        \"\"\"The audio end time in milliseconds.\"\"\"\n\n        type: Literal[\n            ModelEventType.MODEL_INPUT_DONE\n        ] = ModelEventType.MODEL_INPUT_DONE\n        \"\"\"The event type.\"\"\"\n\n    class ModelErrorEvent(EventBase):\n        \"\"\"Error event.\"\"\"\n\n        error_type: str\n        \"\"\"The error type.\"\"\"\n\n        code: str\n        \"\"\"The error code.\"\"\"\n\n        message: str\n        \"\"\"The error message.\"\"\"\n\n        type: Literal[ModelEventType.MODEL_ERROR] = ModelEventType.MODEL_ERROR\n        \"\"\"The event type.\"\"\"\n\n    class WebsocketConnectEvent(EventBase):\n        \"\"\"WebSocket connect event.\"\"\"\n\n        type: Literal[ModelEventType.MODEL_WEBSOCKET_CONNECT]\n        \"\"\"The event type.\"\"\"\n\n    class WebsocketDisconnectEvent(EventBase):\n        \"\"\"WebSocket disconnect event.\"\"\"\n\n        type: Literal[ModelEventType.MODEL_WEBSOCKET_DISCONNECT]\n        \"\"\"The event type.\"\"\"\n"
  },
  {
    "path": "src/agentscope/realtime/_events/_server_event.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The websocket events generated from the realtime agent and backend.\"\"\"\nfrom enum import Enum\nfrom typing import Literal\n\nfrom pydantic import BaseModel\n\nfrom ._utils import AudioFormat\nfrom ._model_event import ModelEvents\nfrom ...message import ToolUseBlock, ToolResultBlock\n\n\nclass ServerEventType(str, Enum):\n    \"\"\"Types of agent events for backend-to-web communication.\"\"\"\n\n    # Session lifecycle\n    SERVER_SESSION_CREATED = \"server_session_created\"\n    \"\"\"The session between the web frontend and backend is created.\"\"\"\n\n    SERVER_SESSION_UPDATED = \"server_session_updated\"\n    \"\"\"The session between the web frontend and backend is updated.\"\"\"\n\n    SERVER_SESSION_ENDED = \"server_session_ended\"\n    \"\"\"The session between the web frontend and backend is ended.\"\"\"\n\n    # ============== AGENT LIFECYCLE EVENTS ================\n\n    AGENT_READY = \"agent_ready\"\n    \"\"\"The agent is created and ready to receive inputs.\"\"\"\n\n    AGENT_ENDED = \"agent_ended\"\n    \"\"\"The agent ended.\"\"\"\n\n    # ============== AGENT RESPONSE EVENTS =================\n\n    # Response events\n    AGENT_RESPONSE_CREATED = \"agent_response_created\"\n    \"\"\"The agent starts generating a response.\"\"\"\n\n    AGENT_RESPONSE_DONE = \"agent_response_done\"\n    \"\"\"The agent finished generating a response.\"\"\"\n\n    # ============== Response content events =================\n\n    AGENT_RESPONSE_AUDIO_DELTA = \"agent_response_audio_delta\"\n    \"\"\"The agent's response audio data delta.\"\"\"\n\n    AGENT_RESPONSE_AUDIO_DONE = \"agent_response_audio_done\"\n    \"\"\"The agent's response audio data is complete.\"\"\"\n\n    AGENT_RESPONSE_AUDIO_TRANSCRIPT_DELTA = (\n        \"agent_response_audio_transcript_delta\"\n    )\n    \"\"\"The agent's response audio transcription delta.\"\"\"\n\n    AGENT_RESPONSE_AUDIO_TRANSCRIPT_DONE = (\n        \"agent_response_audio_transcript_done\"\n    )\n    \"\"\"The agent's response audio transcription is complete.\"\"\"\n\n    AGENT_RESPONSE_TOOL_USE_DELTA = \"agent_response_tool_use_delta\"\n    \"\"\"The agent's response tool use data delta.\"\"\"\n\n    AGENT_RESPONSE_TOOL_USE_DONE = \"agent_response_tool_use_done\"\n    \"\"\"The agent's response tool use data is complete.\"\"\"\n\n    AGENT_RESPONSE_TOOL_RESULT = \"agent_response_tool_result\"\n    \"\"\"The tool execution result.\"\"\"\n\n    # ============== INPUT AUDIO TRANSCRIPTION EVENTS =================\n\n    AGENT_INPUT_TRANSCRIPTION_DELTA = \"agent_input_transcription_delta\"\n    \"\"\"The input audio transcription delta.\"\"\"\n\n    AGENT_INPUT_TRANSCRIPTION_DONE = \"agent_input_transcription_done\"\n    \"\"\"The input audio transcription is complete.\"\"\"\n\n    # Input detection\n    AGENT_INPUT_STARTED = \"agent_input_started\"\n    \"\"\"Detected the start of user input audio.\"\"\"\n\n    AGENT_INPUT_DONE = \"agent_input_done\"\n    \"\"\"Detected the end of user input audio.\"\"\"\n\n    # ============== ERROR EVENTS =================\n\n    AGENT_ERROR = \"agent_error\"\n    \"\"\"An error occurred in the backend or agent.\"\"\"\n\n\nclass ServerEvents:\n    \"\"\"Realtime server events.\"\"\"\n\n    class EventBase(BaseModel):\n        \"\"\"The base class for all server events, used to unify the type\n        hinting.\"\"\"\n\n    class ServerSessionCreatedEvent(EventBase):\n        \"\"\"Server session created event in the backend\"\"\"\n\n        session_id: str\n        \"\"\"The session ID.\"\"\"\n\n        type: Literal[\n            ServerEventType.SERVER_SESSION_CREATED\n        ] = ServerEventType.SERVER_SESSION_CREATED\n        \"\"\"The event type.\"\"\"\n\n    class ServerSessionUpdatedEvent(EventBase):\n        \"\"\"Server session updated event in the backend\"\"\"\n\n        session_id: str\n        \"\"\"The session ID.\"\"\"\n\n        type: Literal[\n            ServerEventType.SERVER_SESSION_UPDATED\n        ] = ServerEventType.SERVER_SESSION_UPDATED\n        \"\"\"The event type.\"\"\"\n\n    class ServerSessionEndedEvent(EventBase):\n        \"\"\"Server Session ended event in the backend\"\"\"\n\n        session_id: str\n        \"\"\"The session ID.\"\"\"\n\n        type: Literal[\n            ServerEventType.SERVER_SESSION_ENDED\n        ] = ServerEventType.SERVER_SESSION_ENDED\n        \"\"\"The event type.\"\"\"\n\n    class AgentReadyEvent(EventBase):\n        \"\"\"Agent ready event in the backend\"\"\"\n\n        agent_id: str\n        \"\"\"The agent ID.\"\"\"\n\n        agent_name: str\n        \"\"\"The agent name.\"\"\"\n\n        type: Literal[\n            ServerEventType.AGENT_READY\n        ] = ServerEventType.AGENT_READY\n        \"\"\"The event type.\"\"\"\n\n    class AgentEndedEvent(EventBase):\n        \"\"\"Agent ended event in the backend\"\"\"\n\n        agent_id: str\n        \"\"\"The agent ID.\"\"\"\n\n        agent_name: str\n        \"\"\"The agent name.\"\"\"\n\n        type: Literal[\n            ServerEventType.AGENT_ENDED\n        ] = ServerEventType.AGENT_ENDED\n        \"\"\"The event type.\"\"\"\n\n    class AgentResponseCreatedEvent(EventBase):\n        \"\"\"Response created event in the backend\"\"\"\n\n        response_id: str\n        \"\"\"The response ID.\"\"\"\n\n        agent_id: str\n        \"\"\"The agent ID.\"\"\"\n\n        agent_name: str\n        \"\"\"The agent name.\"\"\"\n\n        type: Literal[\n            ServerEventType.AGENT_RESPONSE_CREATED\n        ] = ServerEventType.AGENT_RESPONSE_CREATED\n        \"\"\"The event type.\"\"\"\n\n    class AgentResponseDoneEvent(EventBase):\n        \"\"\"Response done event in the backend\"\"\"\n\n        response_id: str\n        \"\"\"The response ID.\"\"\"\n\n        input_tokens: int\n        \"\"\"The number of input tokens used.\"\"\"\n\n        output_tokens: int\n        \"\"\"The number of output tokens used.\"\"\"\n\n        metadata: dict[str, str] = {}\n        \"\"\"Additional metadata about the response.\"\"\"\n\n        agent_id: str\n        \"\"\"The agent ID.\"\"\"\n\n        agent_name: str\n        \"\"\"The agent name.\"\"\"\n\n        type: Literal[\n            ServerEventType.AGENT_RESPONSE_DONE\n        ] = ServerEventType.AGENT_RESPONSE_DONE\n        \"\"\"The event type.\"\"\"\n\n    class AgentResponseAudioDeltaEvent(EventBase):\n        \"\"\"Response audio delta event in the backend\"\"\"\n\n        response_id: str\n        \"\"\"The response ID.\"\"\"\n\n        item_id: str\n        \"\"\"The response item ID.\"\"\"\n\n        delta: str\n        \"\"\"The audio chunk data, encoded as base64 string.\"\"\"\n\n        format: AudioFormat\n        \"\"\"The audio format information.\"\"\"\n\n        agent_id: str\n        \"\"\"The agent ID.\"\"\"\n\n        agent_name: str\n        \"\"\"The agent name.\"\"\"\n\n        type: Literal[\n            ServerEventType.AGENT_RESPONSE_AUDIO_DELTA\n        ] = ServerEventType.AGENT_RESPONSE_AUDIO_DELTA\n        \"\"\"The event type.\"\"\"\n\n    class AgentResponseAudioDoneEvent(EventBase):\n        \"\"\"Response audio done event in the backend\"\"\"\n\n        response_id: str\n        \"\"\"The response ID.\"\"\"\n\n        item_id: str\n        \"\"\"The response item ID.\"\"\"\n\n        agent_id: str\n        \"\"\"The agent ID.\"\"\"\n\n        agent_name: str\n        \"\"\"The agent name.\"\"\"\n\n        type: Literal[\n            ServerEventType.AGENT_RESPONSE_AUDIO_DONE\n        ] = ServerEventType.AGENT_RESPONSE_AUDIO_DONE\n\n    class AgentResponseAudioTranscriptDeltaEvent(EventBase):\n        \"\"\"Response audio transcript delta event in the backend\"\"\"\n\n        response_id: str\n        \"\"\"The response ID.\"\"\"\n\n        item_id: str\n        \"\"\"The response item ID.\"\"\"\n\n        delta: str\n        \"\"\"The transcript chunk data.\"\"\"\n\n        agent_id: str\n        \"\"\"The agent ID.\"\"\"\n\n        agent_name: str\n        \"\"\"The agent name.\"\"\"\n\n        type: Literal[\n            ServerEventType.AGENT_RESPONSE_AUDIO_TRANSCRIPT_DELTA\n        ] = ServerEventType.AGENT_RESPONSE_AUDIO_TRANSCRIPT_DELTA\n        \"\"\"The event type.\"\"\"\n\n    class AgentResponseAudioTranscriptDoneEvent(EventBase):\n        \"\"\"Response audio transcript done event in the backend\"\"\"\n\n        response_id: str\n        \"\"\"The response ID.\"\"\"\n\n        item_id: str\n        \"\"\"The response item ID.\"\"\"\n\n        agent_id: str\n        \"\"\"The agent ID.\"\"\"\n\n        agent_name: str\n        \"\"\"The agent name.\"\"\"\n\n        type: Literal[\n            ServerEventType.AGENT_RESPONSE_AUDIO_TRANSCRIPT_DONE\n        ] = ServerEventType.AGENT_RESPONSE_AUDIO_TRANSCRIPT_DONE\n        \"\"\"The event type.\"\"\"\n\n    class AgentResponseToolUseDeltaEvent(EventBase):\n        \"\"\"Response tool use delta event in the backend\"\"\"\n\n        response_id: str\n        \"\"\"The response ID.\"\"\"\n\n        item_id: str\n        \"\"\"The response item ID.\"\"\"\n\n        tool_use: ToolUseBlock\n        \"\"\"The tool use block delta, the arguments are accumulated in the\n        `raw_input` field.\"\"\"\n\n        agent_id: str\n        \"\"\"The agent ID.\"\"\"\n\n        agent_name: str\n        \"\"\"The agent name.\"\"\"\n\n        type: Literal[\n            ServerEventType.AGENT_RESPONSE_TOOL_USE_DELTA\n        ] = ServerEventType.AGENT_RESPONSE_TOOL_USE_DELTA\n        \"\"\"The event type.\"\"\"\n\n    class AgentResponseToolUseDoneEvent(EventBase):\n        \"\"\"Response tool use done event in the backend\"\"\"\n\n        response_id: str\n        \"\"\"The response ID.\"\"\"\n\n        item_id: str\n        \"\"\"The response item ID.\"\"\"\n\n        tool_use: ToolUseBlock\n        \"\"\"The complete tool use block.\"\"\"\n\n        agent_id: str\n        \"\"\"The agent ID.\"\"\"\n\n        agent_name: str\n        \"\"\"The agent name.\"\"\"\n\n        type: Literal[\n            ServerEventType.AGENT_RESPONSE_TOOL_USE_DONE\n        ] = ServerEventType.AGENT_RESPONSE_TOOL_USE_DONE\n        \"\"\"The event type.\"\"\"\n\n    class AgentResponseToolResultEvent(EventBase):\n        \"\"\"Response tool result event\"\"\"\n\n        tool_result: ToolResultBlock\n        \"\"\"The tool result block.\"\"\"\n\n        agent_id: str\n        \"\"\"The agent ID.\"\"\"\n\n        agent_name: str\n        \"\"\"The agent name.\"\"\"\n\n        type: Literal[\n            ServerEventType.AGENT_RESPONSE_TOOL_RESULT\n        ] = ServerEventType.AGENT_RESPONSE_TOOL_RESULT\n        \"\"\"The event type.\"\"\"\n\n    class AgentInputTranscriptionDeltaEvent(EventBase):\n        \"\"\"Input transcription delta event in the backend\"\"\"\n\n        item_id: str\n        \"\"\"The conversation item ID.\"\"\"\n\n        delta: str\n        \"\"\"The transcription chunk data.\"\"\"\n\n        agent_id: str\n        \"\"\"The agent ID.\"\"\"\n\n        agent_name: str\n        \"\"\"The agent name.\"\"\"\n\n        type: Literal[\n            ServerEventType.AGENT_INPUT_TRANSCRIPTION_DELTA\n        ] = ServerEventType.AGENT_INPUT_TRANSCRIPTION_DELTA\n        \"\"\"The event type.\"\"\"\n\n    class AgentInputTranscriptionDoneEvent(EventBase):\n        \"\"\"Input transcription done event in the backend\"\"\"\n\n        transcript: str\n        \"\"\"The complete transcription text.\"\"\"\n\n        item_id: str\n        \"\"\"The conversation item ID.\"\"\"\n\n        input_tokens: int | None = None\n        \"\"\"The number of input tokens.\"\"\"\n\n        output_tokens: int | None = None\n        \"\"\"The number of output tokens.\"\"\"\n\n        agent_id: str\n        \"\"\"The agent ID.\"\"\"\n\n        agent_name: str\n        \"\"\"The agent name.\"\"\"\n\n        type: Literal[\n            ServerEventType.AGENT_INPUT_TRANSCRIPTION_DONE\n        ] = ServerEventType.AGENT_INPUT_TRANSCRIPTION_DONE\n        \"\"\"The event type.\"\"\"\n\n    class AgentInputStartedEvent(EventBase):\n        \"\"\"Input started event in the backend\"\"\"\n\n        item_id: str\n        \"\"\"The conversation item ID.\"\"\"\n\n        audio_start_ms: int\n        \"\"\"The audio start time in milliseconds.\"\"\"\n\n        agent_id: str\n        \"\"\"The agent ID.\"\"\"\n\n        agent_name: str\n        \"\"\"The agent name.\"\"\"\n\n        type: Literal[\n            ServerEventType.AGENT_INPUT_STARTED\n        ] = ServerEventType.AGENT_INPUT_STARTED\n        \"\"\"The event type.\"\"\"\n\n    class AgentInputDoneEvent(EventBase):\n        \"\"\"Input done event in the backend\"\"\"\n\n        item_id: str\n        \"\"\"The conversation item ID.\"\"\"\n\n        audio_end_ms: int\n        \"\"\"The audio end time in milliseconds.\"\"\"\n\n        agent_id: str\n        \"\"\"The agent ID.\"\"\"\n\n        agent_name: str\n        \"\"\"The agent name.\"\"\"\n\n        type: Literal[\n            ServerEventType.AGENT_INPUT_DONE\n        ] = ServerEventType.AGENT_INPUT_DONE\n        \"\"\"The event type.\"\"\"\n\n    class AgentErrorEvent(EventBase):\n        \"\"\"Error event in the backend\"\"\"\n\n        error_type: str\n        \"\"\"The error type.\"\"\"\n\n        code: str\n        \"\"\"The error code.\"\"\"\n\n        message: str\n        \"\"\"The error message.\"\"\"\n\n        agent_id: str\n        \"\"\"The agent ID.\"\"\"\n\n        agent_name: str\n        \"\"\"The agent name.\"\"\"\n\n        type: Literal[\n            ServerEventType.AGENT_ERROR\n        ] = ServerEventType.AGENT_ERROR\n        \"\"\"The event type.\"\"\"\n\n    @classmethod\n    def from_model_event(\n        cls,\n        model_event: ModelEvents.ModelResponseCreatedEvent\n        | ModelEvents.ModelResponseDoneEvent\n        | ModelEvents.ModelResponseAudioDeltaEvent\n        | ModelEvents.ModelResponseAudioDoneEvent\n        | ModelEvents.ModelResponseAudioTranscriptDeltaEvent\n        | ModelEvents.ModelResponseAudioTranscriptDoneEvent\n        | ModelEvents.ModelResponseToolUseDeltaEvent\n        | ModelEvents.ModelResponseToolUseDoneEvent\n        | ModelEvents.ModelInputTranscriptionDeltaEvent\n        | ModelEvents.ModelInputTranscriptionDoneEvent\n        | ModelEvents.ModelInputStartedEvent\n        | ModelEvents.ModelInputDoneEvent\n        | ModelEvents.ModelErrorEvent,\n        agent_id: str,\n        agent_name: str,\n    ) -> EventBase:\n        \"\"\"Convert a model event to a server event quickly with\n        1) replace the \"model_\" prefix with \"agent_\" in the type field; 2) add\n        agent_id and agent_name fields\n\n        Args:\n            model_event (`ModelEvents.EventBase`):\n                The model event to convert.\n            agent_id (`str`):\n                The agent ID.\n            agent_name (`str`):\n                The agent name.\n\n        Returns:\n            `ServerEvents.EventBase`:\n                The converted server event.\n        \"\"\"\n        # Obtain the corresponding agent event class\n        cls_name = model_event.__class__.__name__.replace(\"Model\", \"Agent\")\n        agent_event_cls = getattr(cls, cls_name)\n\n        # The data dict of the model event\n        model_event_dict = model_event.model_dump()\n\n        # 1) Replace the \"model_\" prefix with \"agent_\" in the type field\n        if \"type\" in model_event_dict:\n            model_event_dict[\"type\"] = model_event_dict[\"type\"].replace(\n                \"model_\",\n                \"agent_\",\n            )\n\n        try:\n            # 2) Add agent_id and agent_name fields\n            model_event_dict[\"agent_id\"] = agent_id\n            model_event_dict[\"agent_name\"] = agent_name\n            agent_event = agent_event_cls.model_validate(model_event_dict)\n\n        except Exception as e:\n            raise RuntimeError(\n                f\"Failed to convert model event {model_event} to agent \"\n                f\"event {agent_event_cls}: {e}\",\n            ) from e\n\n        return agent_event\n"
  },
  {
    "path": "src/agentscope/realtime/_events/_utils.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The utils for realtime events.\"\"\"\nfrom pydantic import BaseModel, ConfigDict\n\n\nclass AudioFormat(BaseModel):\n    \"\"\"The audio format class\"\"\"\n\n    model_config = ConfigDict(extra=\"allow\")\n\n    type: str\n    \"\"\"The audio type, e.g., 'audio/pcm'\"\"\"\n\n    rate: int\n    \"\"\"The audio sample rate, e.g., 16000\"\"\"\n"
  },
  {
    "path": "src/agentscope/realtime/_gemini_realtime_model.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The Gemini realtime model class.\"\"\"\nimport json\nfrom typing import Literal, Any\n\nimport shortuuid\n\nfrom ._events import ModelEvents\nfrom ._base import RealtimeModelBase\nfrom .._logging import logger\nfrom .._utils._common import _get_bytes_from_web_url\nfrom ..message import (\n    AudioBlock,\n    ImageBlock,\n    TextBlock,\n    ToolResultBlock,\n    ToolUseBlock,\n)\n\n\nclass GeminiRealtimeModel(RealtimeModelBase):\n    \"\"\"The Gemini realtime model class.\"\"\"\n\n    support_input_modalities: list[str] = [\n        \"audio\",\n        \"text\",\n        \"image\",\n        \"tool_result\",\n    ]\n    \"\"\"The supported input modalities of the Gemini realtime model.\"\"\"\n\n    websocket_url: str = (\n        \"wss://generativelanguage.googleapis.com/ws/\"\n        \"google.ai.generativelanguage.v1alpha.GenerativeService.\"\n        \"BidiGenerateContent?key=\"\n    )\n    \"\"\"The websocket URL of the Gemini realtime model API.\"\"\"\n\n    websocket_headers: dict[str, str] = {\n        \"Content-Type\": \"application/json\",\n    }\n    \"\"\"The websocket headers of the Gemini realtime model API.\"\"\"\n\n    input_sample_rate: int\n    \"\"\"The input audio sample rate.\"\"\"\n\n    output_sample_rate: int\n    \"\"\"The output audio sample rate.\"\"\"\n\n    def __init__(\n        self,\n        model_name: str,\n        api_key: str,\n        voice: Literal[\"Puck\", \"Charon\", \"Kore\", \"Fenrir\"] | str = \"Puck\",\n        enable_input_audio_transcription: bool = True,\n    ) -> None:\n        \"\"\"Initialize the GeminiRealtimeModel class.\n\n        Args:\n            model_name (`str`):\n                The model name, e.g. \"gemini-2.5-flash-native-audio-preview\n                -09-2025\". Refers to `official documentation\n                <https://ai.google.dev/gemini-api/docs/live?hl=zh-cn&example=mic-stream>`_\n                for more details.\n            api_key (`str`):\n                The Gemini API key for authentication.\n            voice (`Literal[\"Puck\", \"Charon\", \"Kore\", \"Fenrir\"] str`,\n            defaults to `\"Puck\"`):\n                The voice to be used for text-to-speech.\n            enable_input_audio_transcription (`bool`, defaults to `True`):\n                Whether to enable input audio transcription.\n        \"\"\"\n        super().__init__(model_name)\n\n        self.voice = voice\n        self.enable_input_audio_transcription = (\n            enable_input_audio_transcription\n        )\n\n        # The Gemini realtime API uses 16kHz input and 24kHz output.\n        self.input_sample_rate = 16000\n        self.output_sample_rate = 24000\n\n        # Set the API key in the websocket URL.\n        self.websocket_url = self.websocket_url + api_key\n\n        # Response tracking state.\n        # Note: Unlike DashScope/OpenAI which send explicit `response.created`\n        # events, Gemini does not. We generate response IDs ourselves using\n        # short UUID to ensure uniqueness.\n        self._response_id: str | None = None\n\n    def _build_session_config(\n        self,\n        instructions: str,\n        tools: list[dict] | None,\n        **kwargs: Any,\n    ) -> dict:\n        \"\"\"Build Gemini setup message.\n\n        Gemini Live API requires a \"setup\" message as the first message\n        to configure the session.\n\n        Args:\n            instructions (`str`):\n                The system instructions for the model.\n            tools (`list[dict]`):\n                The list of tool JSON schemas.\n            **kwargs:\n                Additional configuration parameters.\n\n        Returns:\n            `dict`:\n                The session configuration dict.\n        \"\"\"\n        # Model configuration\n        session_config: dict = {\n            \"model\": f\"models/{self.model_name}\",\n            \"systemInstruction\": {\n                \"parts\": [{\"text\": instructions}],\n            },\n            \"outputAudioTranscription\": {},\n        }\n\n        # Audio transcription configuration\n        if self.enable_input_audio_transcription:\n            session_config[\"inputAudioTranscription\"] = {}\n\n        # Generation configuration\n        generation_config: dict = {\n            \"responseModalities\": [\"AUDIO\"],\n            **kwargs,\n        }\n\n        # Voice configuration\n        if self.voice:\n            generation_config[\"speechConfig\"] = {\n                \"voiceConfig\": {\n                    \"prebuiltVoiceConfig\": {\"voiceName\": self.voice},\n                },\n            }\n\n        session_config[\"generationConfig\"] = generation_config\n\n        # Tools configuration\n        if tools:\n            session_config[\"tools\"] = self._format_toolkit_schema(tools)\n\n        setup_msg = {\"setup\": session_config}\n        return setup_msg\n\n    def _format_toolkit_schema(\n        self,\n        schemas: list[dict[str, Any]],\n    ) -> list[dict[str, Any]]:\n        \"\"\"Format the tools JSON schema into Gemini format.\n\n        Args:\n            schemas (`list[dict[str, Any]]`):\n                The tool schemas.\n\n        Returns:\n            `list[dict[str, Any]]`:\n                The formatted tools for Gemini.\n        \"\"\"\n        function_declarations = []\n        for schema in schemas:\n            if \"function\" not in schema:\n                continue\n            func = schema[\"function\"].copy()\n            function_declarations.append(func)\n\n        return [{\"function_declarations\": function_declarations}]\n\n    async def send(\n        self,\n        data: AudioBlock | TextBlock | ImageBlock | ToolResultBlock,\n    ) -> None:\n        \"\"\"Send the data to the Gemini realtime model for processing.\n\n        Args:\n            data (`AudioBlock | TextBlock | ImageBlock | ToolResultBlock`):\n                The data to be sent to the Gemini realtime model.\n        \"\"\"\n        from websockets import State\n\n        if not self._websocket or self._websocket.state != State.OPEN:\n            raise RuntimeError(\n                f\"WebSocket is not connected for model {self.model_name}. \"\n                \"Call the `connect` method first.\",\n            )\n\n        # Type checking\n        assert (\n            isinstance(data, dict) and \"type\" in data\n        ), \"Data must be a dict with a 'type' field.\"\n\n        # The source must be base64 for audio data\n        data_type = data.get(\"type\")\n\n        if data_type not in self.support_input_modalities:\n            logger.warning(\n                \"Gemini Realtime API does not support %s data input. \"\n                \"Supported modalities are: %s\",\n                data_type,\n                \", \".join(self.support_input_modalities),\n            )\n            return\n\n        # Process the data based on its type\n        if data_type == \"image\":\n            to_send_message = await self._parse_image_data(\n                ImageBlock(\n                    type=\"image\",\n                    source=data.get(\"source\"),\n                ),\n            )\n\n        elif data_type == \"audio\":\n            to_send_message = await self._parse_audio_data(\n                AudioBlock(\n                    type=\"audio\",\n                    source=data.get(\"source\"),\n                ),\n            )\n\n        elif data_type == \"text\":\n            to_send_message = await self._parse_text_data(\n                TextBlock(\n                    type=\"text\",\n                    text=data.get(\"text\"),\n                ),\n            )\n\n        elif data_type == \"tool_result\":\n            to_send_message = await self._parse_tool_result_data(\n                ToolResultBlock(\n                    type=\"tool_result\",\n                    name=data.get(\"name\"),\n                    output=data.get(\"output\"),\n                    id=data.get(\"id\"),\n                ),\n            )\n\n        else:\n            raise RuntimeError(f\"Unsupported data type {data_type}\")\n\n        if to_send_message:\n            await self._websocket.send(to_send_message)\n\n    async def parse_api_message(\n        self,\n        message: str,\n    ) -> ModelEvents.EventBase | list[ModelEvents.EventBase] | None:\n        \"\"\"Parse the message received from the Gemini realtime model API.\n\n        Args:\n            message (`str`):\n                The message received from the Gemini realtime model API.\n\n        Returns:\n            `ModelEvents.EventBase | list[ModelEvents.EventBase] | None`:\n                The unified model event(s) in agentscope format.\n        \"\"\"\n        try:\n            data = json.loads(message)\n        except json.decoder.JSONDecodeError:\n            return None\n\n        if not isinstance(data, dict):\n            return None\n\n        model_event = None\n\n        # ================ Setup related events ================\n        if \"setupComplete\" in data:\n            model_event = ModelEvents.ModelSessionCreatedEvent(\n                session_id=\"gemini_session\",\n            )\n\n        # ================ Server content events ================\n        elif \"serverContent\" in data:\n            model_event = await self._parse_server_content(\n                data[\"serverContent\"],\n            )\n\n        # ================ Tool call events ================\n        elif \"toolCall\" in data:\n            model_event = await self._parse_tool_call(data[\"toolCall\"])\n\n        # ================ Tool call cancellation ================\n        elif \"toolCallCancellation\" in data:\n            # Tool call was cancelled\n            # This effectively ends the current response.\n            logger.info(\n                \"Tool call cancelled: %s\",\n                data[\"toolCallCancellation\"],\n            )\n            response_id = self._response_id or \"\"\n            self._response_id = None  # Clear response ID\n            model_event = ModelEvents.ModelResponseDoneEvent(\n                response_id=response_id,\n                input_tokens=0,\n                output_tokens=0,\n            )\n\n        # ================ Error events ================\n        elif \"error\" in data:\n            error = data[\"error\"]\n            model_event = ModelEvents.ModelErrorEvent(\n                error_type=error.get(\"status\", \"unknown\"),\n                code=str(error.get(\"code\", \"unknown\")),\n                message=error.get(\"message\", \"An unknown error occurred.\"),\n            )\n\n        else:\n            logger.debug(\n                \"Unknown Gemini realtime model message keys: %s\",\n                list(data.keys()),\n            )\n\n        return model_event\n\n    def _ensure_response_id(self) -> str:\n        \"\"\"Ensure a response ID exists, creating one if necessary.\n\n        Gemini doesn't send explicit response.created events, so we generate\n        the response ID on first audio/text chunk using short UUID.\n\n        Returns:\n            `str`: The current response ID.\n        \"\"\"\n        if not self._response_id:\n            self._response_id = f\"resp_{shortuuid.uuid()}\"\n        # After the check above, _response_id is guaranteed to be non-None\n        assert self._response_id is not None\n        return self._response_id\n\n    def _parse_model_turn(\n        self,\n        model_turn: dict,\n    ) -> ModelEvents.EventBase | None:\n        \"\"\"Parse the modelTurn content from Gemini API.\n\n        Args:\n            model_turn (`dict`):\n                The modelTurn dictionary containing parts with audio/text.\n\n        Returns:\n            `ModelEvents.EventBase | None`:\n                The parsed model event, or None if no valid content found.\n        \"\"\"\n        parts = model_turn.get(\"parts\", [])\n\n        for part in parts:\n            # Check for audio data\n            if \"inlineData\" in part:\n                event = self._parse_inline_data(part[\"inlineData\"])\n                if event:\n                    return event\n\n            # Check for text data\n            if \"text\" in part:\n                text_data = part[\"text\"]\n                if text_data:\n                    response_id = self._ensure_response_id()\n                    return ModelEvents.ModelResponseAudioTranscriptDeltaEvent(\n                        response_id=response_id,\n                        delta=text_data,\n                        item_id=\"\",\n                    )\n\n        return None\n\n    def _parse_inline_data(\n        self,\n        inline_data: dict,\n    ) -> ModelEvents.EventBase | None:\n        \"\"\"Parse inline data (audio) from a model turn part.\n\n        Args:\n            inline_data (`dict`):\n                The inlineData dictionary containing mimeType and data.\n\n        Returns:\n            `ModelEvents | None`:\n                Audio delta event if valid audio data, None otherwise.\n        \"\"\"\n        mime_type = inline_data.get(\"mimeType\", \"\")\n        if not mime_type.startswith(\"audio/\"):\n            return None\n\n        audio_data = inline_data.get(\"data\", \"\")\n        if not audio_data:\n            return None\n\n        response_id = self._ensure_response_id()\n        return ModelEvents.ModelResponseAudioDeltaEvent(\n            response_id=response_id,\n            item_id=\"\",\n            delta=audio_data,\n            format={\n                \"type\": \"audio/pcm\",\n                \"rate\": self.output_sample_rate,\n            },\n        )\n\n    async def _parse_server_content(\n        self,\n        server_content: dict,\n    ) -> ModelEvents.EventBase | None:\n        \"\"\"Parse the serverContent message from Gemini API.\n\n        Args:\n            server_content (`dict`):\n                The serverContent dictionary from the API response.\n\n        Returns:\n            `ModelEvents.EventBase | None`:\n                The unified model event in agentscope format.\n        \"\"\"\n        model_event = None\n\n        # Handle model turn (response with audio/text)\n        if \"modelTurn\" in server_content:\n            model_event = self._parse_model_turn(server_content[\"modelTurn\"])\n\n        # Handle output transcription\n        elif \"outputTranscription\" in server_content:\n            text = server_content[\"outputTranscription\"].get(\"text\", \"\")\n            if text:\n                model_event = (\n                    ModelEvents.ModelResponseAudioTranscriptDeltaEvent(\n                        response_id=self._response_id or \"\",\n                        delta=text,\n                        item_id=\"\",\n                    )\n                )\n\n        # Handle input transcription\n        elif \"inputTranscription\" in server_content:\n            text = server_content[\"inputTranscription\"].get(\"text\", \"\")\n            if text:\n                model_event = ModelEvents.ModelInputTranscriptionDoneEvent(\n                    transcript=text,\n                    item_id=\"\",\n                )\n\n        # Handle generation complete (response done)\n        elif \"generationComplete\" in server_content:\n            response_id = self._response_id or \"\"\n            self._response_id = None\n            model_event = ModelEvents.ModelResponseDoneEvent(\n                response_id=response_id,\n                input_tokens=0,\n                output_tokens=0,\n            )\n\n        # Handle turn complete\n        elif \"turnComplete\" in server_content:\n            logger.debug(\"Gemini: turnComplete received\")\n            # turnComplete without generationComplete means interrupted\n            if self._response_id:\n                response_id = self._response_id\n                self._response_id = None\n                model_event = ModelEvents.ModelResponseDoneEvent(\n                    response_id=response_id,\n                    input_tokens=0,\n                    output_tokens=0,\n                )\n\n        # Handle interrupted\n        elif \"interrupted\" in server_content:\n            logger.debug(\"Gemini: response interrupted\")\n\n        return model_event\n\n    async def _parse_tool_call(\n        self,\n        tool_call: dict,\n    ) -> list[ModelEvents.EventBase] | None:\n        \"\"\"Parse the tool call message from Gemini API.\n\n        Args:\n            tool_call (`dict`):\n                The toolCall dictionary from the API response.\n\n        Returns:\n            `list[ModelEvents.EventBase] | None`:\n                The unified model event(s) in agentscope format.\n        \"\"\"\n        function_calls = tool_call.get(\"functionCalls\", [])\n        if not function_calls:\n            return None\n\n        events = []\n        for func_call in function_calls:\n            name = func_call.get(\"name\", \"\")\n            call_id = func_call.get(\"id\", \"\")\n            args = func_call.get(\"args\", {})\n\n            # Return the accumulated arguments instead of just the delta\n            model_event = ModelEvents.ModelResponseToolUseDoneEvent(\n                response_id=self._response_id or \"\",\n                item_id=\"\",\n                tool_use=ToolUseBlock(\n                    type=\"tool_use\",\n                    id=call_id,\n                    name=name,\n                    input=args,\n                    raw_input=json.dumps(args, ensure_ascii=False),\n                ),\n            )\n            events.append(model_event)\n\n        return events if events else None\n\n    async def _parse_image_data(self, block: ImageBlock) -> str | None:\n        \"\"\"Parse the image data block to the format required by the Gemini\n        realtime model API.\n\n        Args:\n            block (`ImageBlock`):\n                The image data block.\n\n        Returns:\n            `str | None`: The parsed message to be sent to the Gemini realtime\n            model API.\n        \"\"\"\n        source = block.get(\"source\", {})\n        source_type = source.get(\"type\", \"\")\n        # media_type is in Base64Source, use default for URLSource\n        media_type = source.get(\"media_type\", \"image/jpeg\")\n\n        if source_type == \"base64\":\n            image_data = source.get(\"data\", \"\")\n        elif source_type == \"url\":\n            image_data = _get_bytes_from_web_url(str(source.get(\"url\", \"\")))\n        else:\n            raise ValueError(f\"Unsupported image source type: {source_type}\")\n\n        return json.dumps(\n            {\n                \"realtimeInput\": {\n                    \"video\": {\n                        \"mimeType\": media_type,\n                        \"data\": image_data,\n                    },\n                },\n            },\n        )\n\n    async def _parse_audio_data(self, block: AudioBlock) -> str:\n        \"\"\"Parse the audio data block to the format required by the Gemini\n        realtime model API.\n\n        Args:\n            block (`AudioBlock`):\n                The audio data block.\n\n        Returns:\n            `str`: The parsed message to be sent to the Gemini realtime\n            model API.\n        \"\"\"\n        source = block.get(\"source\", {})\n        source_type = source.get(\"type\", \"\")\n\n        if source_type == \"base64\":\n            audio_data = source.get(\"data\", \"\")\n        elif source_type == \"url\":\n            audio_data = _get_bytes_from_web_url(str(source.get(\"url\", \"\")))\n        else:\n            raise ValueError(f\"Unsupported audio source type: {source_type}\")\n\n        return json.dumps(\n            {\n                \"realtimeInput\": {\n                    \"audio\": {\n                        \"mimeType\": f\"audio/pcm;rate={self.input_sample_rate}\",\n                        \"data\": audio_data,\n                    },\n                },\n            },\n        )\n\n    async def _parse_text_data(self, block: TextBlock) -> str:\n        \"\"\"Parse the text data block to the format required by the Gemini\n        realtime model API.\n\n        Args:\n            block (`TextBlock`):\n                The text data block.\n\n        Returns:\n            `str`: The parsed message to be sent to the Gemini realtime\n            model API.\n        \"\"\"\n        text = block.get(\"text\", \"\")\n\n        return json.dumps(\n            {\n                \"clientContent\": {\n                    \"turns\": [\n                        {\n                            \"role\": \"user\",\n                            \"parts\": [{\"text\": text}],\n                        },\n                    ],\n                    # TODO: should be set to False?\n                    \"turnComplete\": True,\n                },\n            },\n        )\n\n    async def _parse_tool_result_data(self, block: ToolResultBlock) -> str:\n        \"\"\"Parse the tool result data block to the format required by the\n        Gemini realtime model API.\n\n        Args:\n            block (`ToolResultBlock`):\n                The tool result data block.\n\n        Returns:\n            `str`: The parsed message to be sent to the Gemini realtime\n            model API.\n        \"\"\"\n        tool_id = block.get(\"id\", \"\")\n        tool_name = block.get(\"name\", \"\")\n        output = block.get(\"output\", \"\")\n\n        # Extract text from list of blocks (most common case)\n        if isinstance(output, list):\n            text = \"\".join(\n                str(item.get(\"text\", \"\"))\n                if isinstance(item, dict) and item.get(\"type\") == \"text\"\n                else str(item)\n                for item in output\n            )\n            result_obj = {\"result\": text}\n        elif isinstance(output, str):\n            try:\n                result_obj = json.loads(output)\n            except json.JSONDecodeError:\n                result_obj = {\"result\": output}\n        else:\n            result_obj = (\n                output if isinstance(output, dict) else {\"result\": str(output)}\n            )\n\n        return json.dumps(\n            {\n                \"toolResponse\": {\n                    \"functionResponses\": [\n                        {\n                            \"id\": tool_id,\n                            \"name\": tool_name,\n                            \"response\": result_obj,\n                        },\n                    ],\n                },\n            },\n        )\n"
  },
  {
    "path": "src/agentscope/realtime/_openai_realtime_model.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The OpenAI realtime model class.\"\"\"\nimport json\nfrom typing import Literal, Any\n\nfrom ._events import ModelEvents\nfrom ._base import RealtimeModelBase\nfrom .._logging import logger\nfrom .._utils._common import _get_bytes_from_web_url, _json_loads_with_repair\nfrom ..message import (\n    AudioBlock,\n    TextBlock,\n    ImageBlock,\n    ToolResultBlock,\n    ToolUseBlock,\n)\n\n\nclass OpenAIRealtimeModel(RealtimeModelBase):\n    \"\"\"The OpenAI realtime model class.\"\"\"\n\n    support_input_modalities: list[str] = [\"audio\", \"text\", \"tool_result\"]\n    \"\"\"The supported input modalities of the OpenAI realtime model.\"\"\"\n\n    support_tools: bool = True\n    \"\"\"The OpenAI realtime model supports tools API.\"\"\"\n\n    websocket_url: str = \"wss://api.openai.com/v1/realtime?model={model_name}\"\n    \"\"\"The websocket URL of the OpenAI realtime model API.\"\"\"\n\n    websocket_headers: dict[str, str]\n    \"\"\"The websocket headers of the OpenAI realtime model API.\"\"\"\n\n    input_sample_rate: int\n    \"\"\"The input audio sample rate.\"\"\"\n\n    output_sample_rate: int\n    \"\"\"The output audio sample rate.\"\"\"\n\n    def __init__(\n        self,\n        model_name: str,\n        api_key: str,\n        voice: Literal[\"alloy\", \"echo\", \"marin\", \"cedar\"] | str = \"alloy\",\n        enable_input_audio_transcription: bool = True,\n    ) -> None:\n        \"\"\"Initialize the OpenAIRealtimeModel class.\n\n        Args:\n            model_name (`str`):\n                The model name, e.g. \"gpt-4o-realtime-preview\".\n            api_key (`str`):\n                The API key for authentication.\n            voice (`Literal[\"alloy\", \"echo\", \"marin\", \"cedar\"] | str`, \\\n            defaults to `\"alloy\"`):\n                The voice to be used for text-to-speech.\n            enable_input_audio_transcription (`bool`, defaults to `True`):\n                Whether to enable input audio transcription.\n        \"\"\"\n        super().__init__(model_name)\n\n        self.voice = voice\n        self.enable_input_audio_transcription = (\n            enable_input_audio_transcription\n        )\n\n        # The OpenAI realtime API uses 24kHz for both input and output.\n        self.input_sample_rate = 24000\n        self.output_sample_rate = 24000\n\n        # Set the model name in the websocket URL.\n        self.websocket_url = self.websocket_url.format(model_name=model_name)\n\n        # Set the API key in the websocket headers.\n        self.websocket_headers = {\n            \"Authorization\": f\"Bearer {api_key}\",\n            \"OpenAI-Beta\": \"realtime=v1\",\n        }\n\n        # Record the response ID for the current session.\n        self._response_id = \"\"\n\n        # Tool arguments accumulator for tracking tool call parameters\n        self._tool_args_accumulator: dict[str, str] = {}\n\n    def _build_session_config(\n        self,\n        instructions: str,\n        tools: list[dict] | None,\n        **kwargs: Any,\n    ) -> dict:\n        \"\"\"Build the session configuration for the OpenAI realtime model.\"\"\"\n\n        session_config: dict[str, Any] = {\n            \"type\": \"realtime\",\n            \"output_modalities\": [\"audio\"],\n            \"audio\": {\n                \"input\": {\n                    \"turn_detection\": {\n                        \"type\": \"server_vad\",\n                        \"create_response\": True,\n                    },\n                },\n                \"output\": {\n                    \"voice\": self.voice,\n                },\n            },\n            \"instructions\": instructions,\n            **kwargs,\n        }\n\n        # Input audio transcription\n        if self.enable_input_audio_transcription:\n            session_config[\"audio\"][\"input\"][\"transcription\"] = {\n                \"model\": \"whisper-1\",\n            }\n\n        # Tools configuration\n        if tools:\n            session_config[\"tools\"] = self._format_toolkit_schema(tools)\n\n        return {\n            \"type\": \"session.update\",\n            \"session\": session_config,\n        }\n\n    def _format_toolkit_schema(\n        self,\n        schemas: list[dict[str, Any]],\n    ) -> list[dict[str, Any]]:\n        \"\"\"Format the tools JSON schema into OpenAI realtime model format.\n\n        Args:\n            schemas (`list[dict[str, Any]]`):\n                The tool schemas.\n\n        Returns:\n            `list[dict[str, Any]]`:\n                The formatted tools for OpenAI realtime model.\n\n        .. note::\n            The OpenAI Realtime API uses a different tool format compared to\n            the regular Chat Completions API. While the Chat API expects tools\n            to be wrapped in ``{\"type\": \"function\", \"function\": {...}}``, the\n            Realtime API expects a flattened structure where the function\n            definition is directly at the top level with an added ``\"type\":\n            \"function\"`` field.\n        \"\"\"\n        return [{\"type\": \"function\", **tool[\"function\"]} for tool in schemas]\n\n    async def send(\n        self,\n        data: AudioBlock | TextBlock | ImageBlock | ToolResultBlock,\n    ) -> None:\n        \"\"\"Send the data to the OpenAI realtime model for processing.\n\n        Args:\n            data (`AudioBlock | TextBlock | ImageBlock | ToolResultBlock`):\n                The data to be sent to the OpenAI realtime model.\n        \"\"\"\n        from websockets import State\n\n        if not self._websocket or self._websocket.state != State.OPEN:\n            raise RuntimeError(\n                f\"WebSocket is not connected for model {self.model_name}. \"\n                \"Call the `connect` method first.\",\n            )\n\n        # Type checking\n        assert (\n            isinstance(data, dict) and \"type\" in data\n        ), \"Data must be a dict with a 'type' field.\"\n\n        # The source must be base64 for audio data\n        data_type = data.get(\"type\")\n\n        if data_type not in self.support_input_modalities:\n            logger.warning(\n                \"OpenAI Realtime API does not support %s data input. \"\n                \"Supported modalities are: %s\",\n                data_type,\n                \", \".join(self.support_input_modalities),\n            )\n            return\n\n        # Process the data based on its type\n        if data_type == \"audio\":\n            to_send_message = await self._parse_audio_data(\n                AudioBlock(\n                    type=\"audio\",\n                    source=data.get(\"source\"),\n                ),\n            )\n\n        elif data_type == \"text\":\n            to_send_message = await self._parse_text_data(\n                TextBlock(\n                    type=\"text\",\n                    text=data.get(\"text\"),\n                ),\n            )\n\n        elif data_type == \"tool_result\":\n            to_send_message = await self._parse_tool_result_data(\n                ToolResultBlock(\n                    type=\"tool_result\",\n                    id=data.get(\"id\"),\n                    output=data.get(\"output\"),\n                    name=data.get(\"name\"),\n                ),\n            )\n\n        else:\n            raise RuntimeError(f\"Unsupported data type {data_type}\")\n\n        await self._websocket.send(to_send_message)\n\n    async def parse_api_message(\n        self,\n        message: str,\n    ) -> ModelEvents.EventBase | list[ModelEvents.EventBase] | None:\n        \"\"\"Parse the message received from the OpenAI realtime model API.\n\n        Args:\n            message (`str`):\n                The message received from the OpenAI realtime model API.\n\n        Returns:\n            `ModelEvents.EventBase | list[ModelEvents.EventBase] | None`:\n                The unified model event(s) in agentscope format.\n        \"\"\"\n        try:\n            data = json.loads(message)\n        except json.decoder.JSONDecodeError:\n            return None\n\n        if not isinstance(data, dict):\n            return None\n\n        model_event = None\n        match data.get(\"type\", \"\"):\n            # ================ Session related events ================\n            case \"session.created\":\n                model_event = ModelEvents.ModelSessionCreatedEvent(\n                    session_id=data.get(\"session\", {}).get(\"id\", \"\"),\n                )\n\n            case \"session.updated\":\n                # TODO: handle the session updated event\n                pass\n\n            # ================ Response related events ================\n            case \"response.created\":\n                self._response_id = data.get(\"response\", {}).get(\"id\", \"\")\n                model_event = ModelEvents.ModelResponseCreatedEvent(\n                    response_id=self._response_id,\n                )\n\n            case \"response.done\":\n                response = data.get(\"response\", {})\n                response_id = response.get(\"id\", self._response_id)\n                usage = response.get(\"usage\", {})\n                model_event = ModelEvents.ModelResponseDoneEvent(\n                    response_id=response_id,\n                    input_tokens=usage.get(\"input_tokens\", 0),\n                    output_tokens=usage.get(\"output_tokens\", 0),\n                )\n                # clear the response id\n                self._response_id = \"\"\n\n            case \"response.output_audio.delta\":\n                audio_data = data.get(\"delta\", \"\")\n                if audio_data:\n                    model_event = ModelEvents.ModelResponseAudioDeltaEvent(\n                        response_id=self._response_id,\n                        item_id=data.get(\"item_id\", \"\"),\n                        delta=audio_data,\n                        format={\n                            \"type\": \"audio/pcm\",\n                            \"rate\": self.output_sample_rate,\n                        },\n                    )\n\n            case \"response.output_audio.done\":\n                model_event = ModelEvents.ModelResponseAudioDoneEvent(\n                    response_id=self._response_id,\n                    item_id=data.get(\"item_id\", \"\"),\n                )\n\n            # ================ Transcription related events ================\n            case \"response.output_audio_transcript.delta\":\n                transcript_data = data.get(\"delta\", \"\")\n                if transcript_data:\n                    model_event = (\n                        ModelEvents.ModelResponseAudioTranscriptDeltaEvent(\n                            response_id=self._response_id,\n                            delta=transcript_data,\n                            item_id=data.get(\"item_id\", \"\"),\n                        )\n                    )\n\n            case \"response.output_audio_transcript.done\":\n                model_event = (\n                    ModelEvents.ModelResponseAudioTranscriptDoneEvent(\n                        response_id=self._response_id,\n                        item_id=data.get(\"item_id\", \"\"),\n                    )\n                )\n\n            case \"response.function_call_arguments.delta\":\n                arguments_delta = data.get(\"delta\")\n                call_id = data.get(\"call_id\", \"\")\n                if arguments_delta:\n                    # Accumulate arguments\n                    if call_id not in self._tool_args_accumulator:\n                        self._tool_args_accumulator[call_id] = \"\"\n                    self._tool_args_accumulator[call_id] += arguments_delta\n\n                    # Return the accumulated arguments instead of just the\n                    # delta\n                    model_event = ModelEvents.ModelResponseToolUseDeltaEvent(\n                        response_id=self._response_id,\n                        item_id=data.get(\"item_id\", \"\"),\n                        tool_use=ToolUseBlock(\n                            type=\"tool_use\",\n                            id=call_id,\n                            name=data.get(\"name\", \"\"),\n                            input={},\n                            raw_input=self._tool_args_accumulator[call_id],\n                        ),\n                    )\n                    # TODO: This handles only one tool call at a time. For\n                    #  parallel tool calls, we might need to reconsider the\n                    #  event handling mechanism.\n\n            case \"response.function_call_arguments.done\":\n                call_id = data.get(\"call_id\", \"\")\n                current_input = self._tool_args_accumulator[call_id]\n                model_event = ModelEvents.ModelResponseToolUseDoneEvent(\n                    response_id=self._response_id,\n                    item_id=data.get(\"item_id\", \"\"),\n                    tool_use=ToolUseBlock(\n                        type=\"tool_use\",\n                        id=call_id,\n                        name=data.get(\"name\", \"\"),\n                        input=_json_loads_with_repair(current_input),\n                        raw_input=current_input,\n                    ),\n                )\n                # Clear the accumulator for this call_id when done\n                if call_id in self._tool_args_accumulator:\n                    del self._tool_args_accumulator[call_id]\n\n            case \"conversation.item.input_audio_transcription.delta\":\n                delta = data.get(\"delta\", \"\")\n                if delta:\n                    model_event = (\n                        ModelEvents.ModelInputTranscriptionDeltaEvent(\n                            item_id=data.get(\"item_id\", \"\"),\n                            delta=delta,\n                        )\n                    )\n\n            case \"conversation.item.input_audio_transcription.completed\":\n                transcript_data = data.get(\"transcript\", \"\")\n                if transcript_data:\n                    model_event = ModelEvents.ModelInputTranscriptionDoneEvent(\n                        transcript=transcript_data,\n                        item_id=data.get(\"item_id\", \"\"),\n                    )\n\n            # ================= VAD related events =================\n            case \"input_audio_buffer.speech_started\":\n                model_event = ModelEvents.ModelInputStartedEvent(\n                    item_id=data.get(\"item_id\", \"\"),\n                    audio_start_ms=data.get(\"audio_start_ms\", 0),\n                )\n\n            case \"input_audio_buffer.speech_stopped\":\n                model_event = ModelEvents.ModelInputDoneEvent(\n                    item_id=data.get(\"item_id\", \"\"),\n                    audio_end_ms=data.get(\"audio_end_ms\", 0),\n                )\n\n            # ================= Error events =================\n            case \"error\":\n                error = data.get(\"error\", {})\n                model_event = ModelEvents.ModelErrorEvent(\n                    error_type=error.get(\"type\", \"unknown\"),\n                    code=error.get(\"code\", \"unknown\"),\n                    message=error.get(\"message\", \"An unknown error occurred.\"),\n                )\n\n            # ================= Unknown events =================\n            case _:\n                logger.debug(\n                    \"Unknown OpenAI realtime model event type: %s\",\n                    data.get(\"type\", None),\n                )\n\n        return model_event\n\n    async def _parse_audio_data(self, block: AudioBlock) -> str:\n        \"\"\"Parse the audio data block to the format required by the OpenAI\n        realtime model API.\n\n        Args:\n            block (`AudioBlock`):\n                The audio data block.\n\n        Returns:\n            `str`: The parsed message to be sent to the OpenAI realtime\n            model API.\n        \"\"\"\n        if block[\"source\"][\"type\"] == \"base64\":\n            audio_data = block[\"source\"][\"data\"]\n\n        elif block[\"source\"][\"type\"] == \"url\":\n            audio_data = _get_bytes_from_web_url(block[\"source\"][\"url\"])\n\n        else:\n            raise ValueError(\n                f\"Unsupported audio source type: {block['source']['type']}\",\n            )\n\n        return json.dumps(\n            {\n                \"type\": \"input_audio_buffer.append\",\n                \"audio\": audio_data,\n            },\n        )\n\n    async def _parse_text_data(self, block: TextBlock) -> str:\n        \"\"\"Parse the text data block to the format required by the OpenAI\n        realtime model API.\n\n        Args:\n            block (`TextBlock`):\n                The text data block.\n\n        Returns:\n            `str`: The parsed message to be sent to the OpenAI realtime\n            model API.\n        \"\"\"\n        text = block.get(\"text\", \"\")\n\n        return json.dumps(\n            {\n                \"type\": \"conversation.item.create\",\n                \"item\": {\n                    \"type\": \"message\",\n                    \"role\": \"user\",\n                    \"content\": [\n                        {\n                            \"type\": \"input_text\",\n                            \"text\": text,\n                        },\n                    ],\n                },\n            },\n        )\n\n    async def _parse_tool_result_data(self, block: ToolResultBlock) -> str:\n        \"\"\"Parse the tool result data block to the format required by the\n        OpenAI realtime model API.\n\n        Args:\n            block (`ToolResultBlock`):\n                The tool result data block.\n\n        Returns:\n            `str`: The parsed message to be sent to the OpenAI realtime\n            model API.\n        \"\"\"\n        return json.dumps(\n            {\n                \"type\": \"conversation.item.create\",\n                \"item\": {\n                    \"type\": \"function_call_output\",\n                    \"call_id\": block.get(\"id\"),\n                    \"output\": block.get(\"output\"),\n                },\n            },\n        )\n"
  },
  {
    "path": "src/agentscope/session/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The session module in agentscope.\"\"\"\n\nfrom ._session_base import SessionBase\nfrom ._json_session import JSONSession\nfrom ._redis_session import RedisSession\n\n__all__ = [\n    \"SessionBase\",\n    \"JSONSession\",\n    \"RedisSession\",\n]\n"
  },
  {
    "path": "src/agentscope/session/_json_session.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The JSON session class.\"\"\"\nimport json\nimport os\nimport aiofiles\n\nfrom ._session_base import SessionBase\nfrom .._logging import logger\nfrom ..module import StateModule\n\n\nclass JSONSession(SessionBase):\n    \"\"\"The JSON session class.\"\"\"\n\n    def __init__(\n        self,\n        save_dir: str = \"./\",\n    ) -> None:\n        \"\"\"Initialize the JSON session class.\n\n        Args:\n            save_dir (`str`, defaults to `\"./\"):\n                The directory to save the session state.\n        \"\"\"\n        self.save_dir = save_dir\n\n    def _get_save_path(self, session_id: str, user_id: str) -> str:\n        \"\"\"The path to save the session state.\n\n        Args:\n            session_id (`str`):\n                The session id.\n            user_id (`str`):\n                The user ID for the storage.\n\n        Returns:\n            `str`:\n                The path to save the session state.\n        \"\"\"\n        os.makedirs(self.save_dir, exist_ok=True)\n        if user_id:\n            file_path = f\"{user_id}_{session_id}.json\"\n        else:\n            file_path = f\"{session_id}.json\"\n        return os.path.join(self.save_dir, file_path)\n\n    async def save_session_state(\n        self,\n        session_id: str,\n        user_id: str = \"\",\n        **state_modules_mapping: StateModule,\n    ) -> None:\n        \"\"\"Load the state dictionary from a JSON file.\n\n        Args:\n            session_id (`str`):\n                The session id.\n            user_id (`str`, default to `\"\"`):\n                The user ID for the storage.\n            **state_modules_mapping (`dict[str, StateModule]`):\n                A dictionary mapping of state module names to their instances.\n        \"\"\"\n        state_dicts = {\n            name: state_module.state_dict()\n            for name, state_module in state_modules_mapping.items()\n        }\n        session_save_path = self._get_save_path(session_id, user_id=user_id)\n        async with aiofiles.open(\n            session_save_path,\n            \"w\",\n            encoding=\"utf-8\",\n            errors=\"surrogatepass\",\n        ) as f:\n            await f.write(json.dumps(state_dicts, ensure_ascii=False))\n\n        logger.info(\n            \"Saved session state to %s successfully.\",\n            session_save_path,\n        )\n\n    async def load_session_state(\n        self,\n        session_id: str,\n        user_id: str = \"\",\n        allow_not_exist: bool = True,\n        **state_modules_mapping: StateModule,\n    ) -> None:\n        \"\"\"Get the state dictionary to be saved to a JSON file.\n\n        Args:\n            session_id (`str`):\n                The session id.\n            user_id (`str`, default to `\"\"`):\n                The user ID for the storage.\n            allow_not_exist (`bool`, defaults to `True`):\n                Whether to allow the session to not exist. If `False`, raises\n                an error if the session does not exist.\n            state_modules_mapping (`list[StateModule]`):\n                The list of state modules to be loaded.\n        \"\"\"\n        session_save_path = self._get_save_path(session_id, user_id=user_id)\n        if os.path.exists(session_save_path):\n            async with aiofiles.open(\n                session_save_path,\n                \"r\",\n                encoding=\"utf-8\",\n                errors=\"surrogatepass\",\n            ) as f:\n                content = await f.read()\n                states = json.loads(content)\n\n            for name, state_module in state_modules_mapping.items():\n                if name in states:\n                    state_module.load_state_dict(states[name])\n            logger.info(\n                \"Load session state from %s successfully.\",\n                session_save_path,\n            )\n\n        elif allow_not_exist:\n            logger.info(\n                \"Session file %s does not exist. Skip loading session state.\",\n                session_save_path,\n            )\n\n        else:\n            raise ValueError(\n                f\"Failed to load session state for file {session_save_path} \"\n                \"does not exist.\",\n            )\n"
  },
  {
    "path": "src/agentscope/session/_redis_session.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The Redis session class.\"\"\"\nimport json\nfrom typing import Any, TYPE_CHECKING\n\nfrom ._session_base import SessionBase\nfrom .._logging import logger\nfrom ..module import StateModule\n\nif TYPE_CHECKING:\n    from redis.asyncio import ConnectionPool, Redis\nelse:\n    ConnectionPool = Any\n    Redis = Any\n\n\nclass RedisSession(SessionBase):\n    \"\"\"The Redis session class.\"\"\"\n\n    SESSION_KEY = \"user_id:{user_id}:session:{session_id}:state\"\n    \"\"\"Redis key pattern (without prefix) for storing sessions state for\n    a specific session.\n    \"\"\"\n\n    def __init__(\n        self,\n        host: str = \"localhost\",\n        port: int = 6379,\n        db: int = 0,\n        password: str | None = None,\n        connection_pool: ConnectionPool | None = None,\n        key_ttl: int | None = None,\n        key_prefix: str = \"\",\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Initialize the Redis session class by connecting to Redis.\n\n        Args:\n            host (`str`, defaults to `\"localhost\"`):\n                Redis server host.\n            port (`int`, defaults to `6379`):\n                Redis server port.\n            db (`int`, defaults to `0`):\n                Redis database index.\n            password (`str | None`, optional):\n                Redis password if required.\n            connection_pool (`ConnectionPool | None`, optional):\n                Optional Redis connection pool.\n            key_ttl (`int | None`, optional):\n                Expire time in seconds for each session state key. If provided,\n                the expiration will be refreshed on every save/load\n                (sliding TTL). If `None`, the session state will not expire.\n            key_prefix (`str`, default to `\"\"`):\n                Optional Redis key prefix prepended to every key generated by\n                this storage. Useful for isolating keys across\n                apps/environments (e.g. `\"prod:\"`, `\"staging:\"`, `\"myapp:\"`).\n            **kwargs (`Any`):\n                Additional keyword arguments passed to redis client.\n        \"\"\"\n        try:\n            import redis.asyncio as redis\n        except ImportError as e:\n            raise ImportError(\n                \"The 'redis' package is required for RedisSession. \"\n                \"Please install it via 'pip install redis[async]'.\",\n            ) from e\n\n        self.key_ttl = key_ttl\n        self.key_prefix = key_prefix or \"\"\n\n        self._client = redis.Redis(\n            host=host,\n            port=port,\n            db=db,\n            password=password,\n            connection_pool=connection_pool,\n            decode_responses=True,\n            **kwargs,\n        )\n\n    def get_client(self) -> Redis:\n        \"\"\"Get the underlying Redis client.\n\n        Returns:\n            `Redis`:\n                The Redis client instance.\n        \"\"\"\n        return self._client\n\n    def _get_session_key(\n        self,\n        session_id: str,\n        user_id: str,\n    ) -> str:\n        \"\"\"Get the Redis key to store a given session state.\"\"\"\n        return self.key_prefix + self.SESSION_KEY.format(\n            user_id=user_id,\n            session_id=session_id,\n        )\n\n    async def save_session_state(\n        self,\n        session_id: str,\n        user_id: str = \"default_user\",\n        **state_modules_mapping: StateModule,\n    ) -> None:\n        \"\"\"Save the state dictionary to Redis.\n\n        Args:\n            session_id (`str`):\n                The session id.\n            user_id (`str`, default to `\"default_user\"`):\n                The user ID for the storage.\n            **state_modules_mapping (`dict[str, StateModule]`):\n                A dictionary mapping of state module names to their instances.\n        \"\"\"\n        state_dicts = {\n            name: state_module.state_dict()\n            for name, state_module in state_modules_mapping.items()\n        }\n\n        key = self._get_session_key(session_id, user_id=user_id)\n        value = json.dumps(state_dicts, ensure_ascii=False)\n\n        await self._client.set(key, value, ex=self.key_ttl)\n\n        logger.info(\"Save session state to redis key %s successfully.\", key)\n\n    async def load_session_state(\n        self,\n        session_id: str,\n        user_id: str = \"default_user\",\n        allow_not_exist: bool = True,\n        **state_modules_mapping: StateModule,\n    ) -> None:\n        \"\"\"Load the state dictionary from Redis.\n\n        Args:\n            session_id (`str`):\n                The session id.\n            user_id (`str`, default to `\"default_user\"`):\n                The user ID for the storage.\n            allow_not_exist (`bool`, defaults to `True`):\n                Whether to allow the session to not exist.\n            **state_modules_mapping (`dict[str, StateModule]`):\n                The mapping of state modules to be loaded.\n        \"\"\"\n        key = self._get_session_key(session_id, user_id=user_id)\n\n        # Use GETEX to get and refresh TTL in a single atomic operation\n        if self.key_ttl is not None:\n            data = await self._client.getex(key, ex=self.key_ttl)\n        else:\n            data = await self._client.get(key)\n\n        if data is None:\n            if allow_not_exist:\n                logger.info(\n                    \"Session key %s does not exist in Redis. Skip loading \"\n                    \"session state.\",\n                    key,\n                )\n                return\n            raise ValueError(\n                f\"Failed to load session state because redis key {key} \"\n                \"does not exist.\",\n            )\n\n        if isinstance(data, (bytes, bytearray)):\n            data = data.decode(\"utf-8\")\n\n        states = json.loads(data)\n\n        for name, state_module in state_modules_mapping.items():\n            if name in states:\n                state_module.load_state_dict(states[name])\n\n        logger.info(\"Load session state from redis key %s successfully.\", key)\n\n    async def close(self) -> None:\n        \"\"\"Close the Redis client connection.\"\"\"\n        await self._client.close()\n\n    async def __aenter__(self) -> \"RedisSession\":\n        \"\"\"Enter the async context manager.\n\n        Returns:\n            `RedisSession`:\n                The current `RedisSession` instance.\n        \"\"\"\n        return self\n\n    async def __aexit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_value: BaseException | None,\n        traceback: Any,\n    ) -> None:\n        \"\"\"Exit the async context manager and close the connection.\n\n        Args:\n            exc_type (`type[BaseException] | None`):\n                The type of the exception.\n            exc_value (`BaseException | None`):\n                The exception instance.\n            traceback (`Any`):\n                The traceback.\n        \"\"\"\n        await self.close()\n"
  },
  {
    "path": "src/agentscope/session/_session_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The session base class in agentscope.\"\"\"\nfrom abc import abstractmethod\n\nfrom ..module import StateModule\n\n\nclass SessionBase:\n    \"\"\"The base class for session in agentscope.\"\"\"\n\n    @abstractmethod\n    async def save_session_state(\n        self,\n        session_id: str,\n        user_id: str = \"\",\n        **state_modules_mapping: StateModule,\n    ) -> None:\n        \"\"\"Save the session state\n\n        Args:\n            session_id (`str`):\n                The session id.\n            user_id (`str`, default to `\"\"`):\n                The user ID for the storage.\n            **state_modules_mapping (`dict[str, StateModule]`):\n                A dictionary mapping of state module names to their instances.\n        \"\"\"\n\n    @abstractmethod\n    async def load_session_state(\n        self,\n        session_id: str,\n        user_id: str = \"\",\n        allow_not_exist: bool = True,\n        **state_modules_mapping: StateModule,\n    ) -> None:\n        \"\"\"Load the session state\n\n        Args:\n            session_id (`str`):\n                The session id.\n            user_id (`str`, default to `\"\"`):\n                The user ID for the storage.\n            allow_not_exist (`bool`, defaults to `True`):\n                Whether to allow the session to not exist.\n            **state_modules_mapping (`dict[str, StateModule]`):\n                The mapping of state modules to be loaded.\n        \"\"\"\n"
  },
  {
    "path": "src/agentscope/token/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The token module in agentscope\"\"\"\n\nfrom ._token_base import TokenCounterBase\nfrom ._gemini_token_counter import GeminiTokenCounter\nfrom ._openai_token_counter import OpenAITokenCounter\nfrom ._anthropic_token_counter import AnthropicTokenCounter\nfrom ._huggingface_token_counter import HuggingFaceTokenCounter\nfrom ._char_token_counter import CharTokenCounter\n\n\n__all__ = [\n    \"TokenCounterBase\",\n    \"CharTokenCounter\",\n    \"GeminiTokenCounter\",\n    \"OpenAITokenCounter\",\n    \"AnthropicTokenCounter\",\n    \"HuggingFaceTokenCounter\",\n]\n"
  },
  {
    "path": "src/agentscope/token/_anthropic_token_counter.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The Anthropic token counter class.\"\"\"\nfrom typing import Any\nfrom ._token_base import TokenCounterBase\n\n\nclass AnthropicTokenCounter(TokenCounterBase):\n    \"\"\"The Anthropic token counter class.\"\"\"\n\n    def __init__(self, model_name: str, api_key: str, **kwargs: Any) -> None:\n        \"\"\"Initialize the Anthropic token counter.\n\n        Args:\n            model_name (`str`):\n                The name of the Anthropic model to use, e.g. \"claude-2\".\n            api_key (`str`):\n                The API key for Anthropic.\n        \"\"\"\n        import anthropic\n\n        self.client = anthropic.AsyncAnthropic(api_key=api_key, **kwargs)\n        self.model_name = model_name\n\n    async def count(\n        self,\n        messages: list[dict],\n        tools: list[dict] | None = None,\n        **kwargs: Any,\n    ) -> int:\n        \"\"\"Count the number of tokens for the given messages\n\n        .. note:: The Anthropic token counting API requires the multimodal\n         data to be in base64 format,\n\n        Args:\n            messages (`list[dict]`):\n                A list of dictionaries, where `role` and `content` fields are\n                required.\n            tools (`list[dict] | None`, defaults to `None`):\n                The tools JSON schemas that the model can use.\n            **kwargs (`Any`):\n                Additional keyword arguments for the token counting API.\n        \"\"\"\n        system_message = None\n        if messages[0].get(\"role\") == \"system\":\n            system_message = messages.pop(0)\n\n        extra_kwargs: dict = {\n            \"model\": self.model_name,\n            \"messages\": messages,\n            **kwargs,\n        }\n\n        if tools:\n            extra_kwargs[\"tools\"] = tools\n\n        if system_message:\n            extra_kwargs[\"system\"] = system_message\n\n        res = await self.client.messages.count_tokens(**extra_kwargs)\n\n        return res.input_tokens\n"
  },
  {
    "path": "src/agentscope/token/_char_token_counter.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"A simple character-based token counter implementation.\"\"\"\nfrom typing import Any\n\nfrom ._token_base import TokenCounterBase\n\n\nclass CharTokenCounter(TokenCounterBase):\n    \"\"\"A very simple implementation that counts tokens based on character\n    length.\n\n    .. note:: This counter does not handle multi-modal data well, as base64\n     encoding can significantly increase character count.\n\n    \"\"\"\n\n    async def count(\n        self,\n        messages: list[dict],\n        tools: list[dict] | None = None,\n        **kwargs: Any,\n    ) -> int:\n        \"\"\"Count the number of tokens in the messages based on the characters\n\n        Args:\n            messages (`list[dict]`):\n                The list of messages to be counted.\n            tools (`list[dict] | None`, *optional*):\n                The list of tools, not used in this counter.\n\n        Returns:\n            `int`:\n                The total number of tokens counted.\n        \"\"\"\n        texts = []\n        for msg in messages:\n            texts.append(str(msg))\n\n        if tools:\n            texts.append(str(tools))\n\n        text = \"\\n\".join(texts)\n        return len(text)\n"
  },
  {
    "path": "src/agentscope/token/_gemini_token_counter.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The gemini token counter class in agentscope.\"\"\"\nfrom typing import Any\n\nfrom agentscope.token._token_base import TokenCounterBase\n\n\nclass GeminiTokenCounter(TokenCounterBase):\n    \"\"\"The Gemini token counter class.\"\"\"\n\n    def __init__(self, model_name: str, api_key: str, **kwargs: Any) -> None:\n        \"\"\"Initialize the Gemini token counter.\n\n        Args:\n            model_name (`str`):\n                The name of the Gemini model to use, e.g. \"gemini-2.5-flash\".\n            api_key (`str`):\n                The API key for Google Gemini.\n            **kwargs:\n                Additional keyword arguments that will be passed to the\n                Gemini client.\n        \"\"\"\n        from google import genai\n\n        self.client = genai.Client(\n            api_key=api_key,\n            **kwargs,\n        )\n        self.model_name = model_name\n\n    async def count(\n        self,\n        messages: list[dict],\n        tools: list[dict] | None = None,\n        **config_kwargs: Any,\n    ) -> int:\n        \"\"\"Count the number of tokens of gemini models.\"\"\"\n\n        kwargs = {\n            \"model\": self.model_name,\n            \"contents\": messages,\n            \"config\": {\n                \"tools\": tools,\n                **config_kwargs,\n            },\n        }\n\n        res = self.client.models.count_tokens(**kwargs)\n\n        return res.total_tokens\n"
  },
  {
    "path": "src/agentscope/token/_huggingface_token_counter.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The huggingface token counter class.\"\"\"\nimport os\nfrom typing import Any\n\nfrom agentscope.token._token_base import TokenCounterBase\n\n\nclass HuggingFaceTokenCounter(TokenCounterBase):\n    \"\"\"The token counter for Huggingface models.\"\"\"\n\n    def __init__(\n        self,\n        pretrained_model_name_or_path: str,\n        use_mirror: bool = False,\n        use_fast: bool = False,\n        trust_remote_code: bool = False,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Initialize the huggingface token counter.\n\n        Args:\n            pretrained_model_name_or_path (`str`):\n                The name or path of the pretrained model, which will be used\n                to download the tokenizer from Huggingface Hub.\n            use_mirror (`bool`, defaults to `False`):\n                Whether to enable the HuggingFace mirror, which is useful for\n                users in China.\n            use_fast (`bool`, defaults to `False`):\n                The argument that will be passed to the tokenizer.\n            trust_remote_code (`bool`, defaults to `False`):\n                The argument that will be passed to the tokenizer.\n            **kwargs:\n                Additional keyword arguments that will be passed to the\n                tokenizer.\n        \"\"\"\n        if use_mirror:\n            mirror = \"https://hf-mirror.com\"\n            os.environ[\"HF_ENDPOINT\"] = mirror\n\n            # if the huggingface is already imported in other dependencies,\n            # we need to set the endpoint manually\n            import huggingface_hub.constants\n\n            huggingface_hub.constants.ENDPOINT = mirror\n            huggingface_hub.constants.HUGGINGFACE_CO_URL_TEMPLATE = (\n                mirror + \"/{repo_id}/resolve/{revision}/{filename}\"\n            )\n\n        from transformers import AutoTokenizer\n\n        self.tokenizer = AutoTokenizer.from_pretrained(\n            pretrained_model_name_or_path,\n            use_fast=use_fast,\n            trust_remote_code=trust_remote_code,\n            **kwargs,\n        )\n\n        if self.tokenizer.chat_template is None:\n            raise ValueError(\n                f\"The tokenizer for model {pretrained_model_name_or_path} in \"\n                f\"transformers does not have chat template.\",\n            )\n\n    async def count(\n        self,\n        messages: list[dict],\n        tools: list[dict] | None = None,\n        **kwargs: Any,\n    ) -> int:\n        \"\"\"Count the number of tokens with the tokenizer download from\n        HuggingFace hub.\n\n        Args:\n            messages (`list[dict]`):\n                A list of message dictionaries\n            tools (`list[dict] | None`, defaults to `None`):\n                The JSON schema of the tools, which will also be involved in\n                the token counting.\n            **kwargs (`Any`):\n                The additional keyword arguments that will be passed to the\n                tokenizer, e.g. `chat_template`, `padding`, etc.\n        \"\"\"\n\n        tokenized_msgs = self.tokenizer.apply_chat_template(\n            messages,\n            add_generation_prompt=False,\n            tokenize=True,\n            return_tensors=\"np\",\n            tools=tools,\n            **kwargs,\n        )[0]\n\n        return len(tokenized_msgs)\n"
  },
  {
    "path": "src/agentscope/token/_openai_token_counter.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The OpenAI token counting class. The token calculation of vision models\nfollows\nhttps://platform.openai.com/docs/guides/images-vision?api-mode=chat#calculating-costs\n\"\"\"\nimport base64\nimport io\nimport json\nimport math\nfrom http import HTTPStatus\nfrom typing import Any\n\nimport requests\n\nfrom ._token_base import TokenCounterBase\n\n\ndef _calculate_tokens_for_high_quality_image(\n    base_tokens: int,\n    tile_tokens: int,\n    width: int,\n    height: int,\n) -> int:\n    \"\"\"Calculate the number of tokens for a high-quality image, which follows\n    https://platform.openai.com/docs/guides/images-vision?api-mode=chat#calculating-costs\n    \"\"\"\n    # Step1: scale to fit within a 2048x2048 box\n    if width > 2048 or height > 2048:\n        ratio = min(2048 / width, 2048 / height)\n        width = int(width * ratio)\n        height = int(height * ratio)\n\n    # Step2: Scale to make the shortest side 768 pixels\n    shortest_side = min(width, height)\n    if shortest_side != 768:\n        ratio = 768 / shortest_side\n        width = int(width * ratio)\n        height = int(height * ratio)\n\n    # Step3: Calculate how many 512px tiles are needed\n    tiles_width = (width + 511) // 512\n    tiles_height = (height + 511) // 512\n    total_tiles = tiles_width * tiles_height\n\n    # Step4: Calculate the total tokens\n    total_tokens = (total_tiles * tile_tokens) + base_tokens\n\n    return total_tokens\n\n\ndef _get_size_of_image_url(url: str) -> tuple[int, int]:\n    \"\"\"Get the size of an image from the given URL.\n\n    Args:\n        url (`str`):\n            A web URL or base64 encoded image URL.\n\n    Returns:\n        `tuple[int, int]`:\n            A tuple containing the width and height of the image.\n    \"\"\"\n    if url.startswith(\"data:image/\"):\n        base64_data = url.split(\"base64,\")[1]\n        image_data = base64.b64decode(base64_data)\n\n    else:\n        response = None\n        for _ in range(3):\n            response = requests.get(url)\n            if response.status_code == HTTPStatus.OK:\n                break\n        response.raise_for_status()\n        image_data = response.content\n\n    from PIL import Image\n\n    image = Image.open(io.BytesIO(image_data))\n    width, height = image.size\n    return width, height\n\n\ndef _get_base_and_tile_tokens(model_name: str) -> tuple[int, int]:\n    \"\"\"Get the base and tile tokens for the given OpenAI model.\n\n    Args:\n        model_name (`str`):\n            The name of the model.\n\n    Returns:\n        `tuple[int, int]`:\n            A tuple containing the base tokens and tile tokens.\n    \"\"\"\n    if any(\n        model_name.startswith(_)\n        for _ in [\n            \"gpt-4o\",\n            \"gpt-4.1\",\n            \"gpt-4.5\",\n        ]\n    ):\n        return 85, 170\n\n    if any(\n        model_name.startswith(_)\n        for _ in [\n            \"o1\",\n            \"o1-pro\",\n            \"o3\",\n        ]\n    ):\n        return 75, 150\n\n    if model_name.startswith(\"4o-mini\"):\n        return 2833, 5667\n\n    raise ValueError(\n        f\"Unsupported OpenAI model {model_name} for token counting. \",\n    )\n\n\ndef _calculate_tokens_for_tools(\n    model_name: str,\n    tools: list[dict],\n    encoding: Any,\n) -> int:\n    \"\"\"Calculate the tokens for the given tools JSON schema, which follows the\n    OpenAI cookbook\n    https://github.com/openai/openai-cookbook/blob/6dfb7920b59a45291f7df4ea41338d1faf9ef1e8/examples/How_to_count_tokens_with_tiktoken.ipynb\n    \"\"\"\n    if not tools:\n        return 0\n\n    func_init = 10\n    prop_init = 3\n    prop_key = 3\n    enum_init = -3\n    enum_item = 3\n    func_end = 12\n\n    if model_name.startswith(\"gpt-4o\"):\n        func_init = 7\n\n    func_token_count = 0\n    for f in tools:\n        func_token_count += func_init\n        function = f[\"function\"]\n        f_name = function[\"name\"]\n        f_desc = function.get(\"description\", \"\").removesuffix(\".\")\n        func_token_count += len(encoding.encode(f\"{f_name}:{f_desc}\"))\n\n        properties = function[\"parameters\"][\"properties\"]\n\n        if len(properties) > 0:\n            func_token_count += prop_init\n            for key in properties.keys():\n                func_token_count += prop_key\n                p_name = key\n                p_type = properties[key][\"type\"]\n                p_desc = (\n                    properties[key].get(\"description\", \"\").removesuffix(\".\")\n                )\n\n                if \"enum\" in properties[key].keys():\n                    func_token_count += enum_init\n                    for item in properties[key][\"enum\"]:\n                        func_token_count += enum_item\n                        func_token_count += len(encoding.encode(item))\n\n                func_token_count += len(\n                    encoding.encode(f\"{p_name}:{p_type}:{p_desc}\"),\n                )\n    func_token_count += func_end\n\n    return func_token_count\n\n\ndef _count_content_tokens_for_openai_vision_model(\n    model_name: str,\n    content: list[dict],\n    encoding: Any,\n) -> int:\n    \"\"\"Yield the number of tokens for the content of an OpenAI vision model.\n    Implemented according to https://platform.openai.com/docs/guides/vision.\n\n    Args:\n        model_name (`str`):\n            The name of the model.\n        content (`list[dict]`):\n            A list of dictionaries.\n        encoding (`Any`):\n            The encoding object.\n\n    Example:\n        .. code-block:: python\n\n            _yield_tokens_for_openai_vision_model(\n                [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"xxx\",\n                    },\n                    {\n                        \"type\": \"image_url\",\n                        \"image_url\": {\n                            \"url\": \"xxx\",\n                            \"detail\": \"auto\",\n                        }\n                    },\n                    # ...\n                ]\n            )\n\n    Returns:\n        `Generator[int, None, None]`: Generate the number of tokens in a\n        generator.\n    \"\"\"\n\n    num_tokens = 0\n    for item in content:\n        assert isinstance(item, dict), (\n            \"The content field should be a list of dictionaries, but got \"\n            f\"{type(item)}.\"\n        )\n\n        typ = item.get(\"type\", None)\n        if typ == \"text\":\n            num_tokens += len(\n                encoding.encode(item[\"text\"]),\n            )\n\n        elif typ == \"image_url\":\n            width, height = _get_size_of_image_url(item[\"image_url\"][\"url\"])\n\n            # Different counting logic for different models\n            if any(\n                model_name.startswith(_)\n                for _ in [\n                    \"gpt-4.1-mini\",\n                    \"gpt-4.1-nano\",\n                    \"o4-mini\",\n                ]\n            ):\n                patches = min(\n                    math.ceil(width / 32) * math.ceil(height / 32),\n                    1536,\n                )\n                if model_name.startswith(\"gpt-4.1-mini\"):\n                    num_tokens += math.ceil(patches * 1.62)\n\n                elif model_name.startswith(\"gpt-4.1-nano\"):\n                    num_tokens += math.ceil(patches * 2.46)\n\n                else:\n                    num_tokens += math.ceil(patches * 1.72)\n\n            elif any(\n                model_name.startswith(_)\n                for _ in [\n                    \"gpt-4o\",\n                    \"gpt-4.1\",\n                    \"gpt-4o-mini\",\n                    \"o\",\n                ]\n            ):\n                base_tokens, tile_tokens = _get_base_and_tile_tokens(\n                    model_name,\n                )\n\n                # By default, we use high here to avoid undercounting tokens\n                detail = item.get(\"image_url\").get(\"detail\", \"high\")\n                if detail == \"low\":\n                    num_tokens += base_tokens\n\n                elif detail in [\"auto\", \"high\"]:\n                    num_tokens += _calculate_tokens_for_high_quality_image(\n                        base_tokens,\n                        tile_tokens,\n                        width,\n                        height,\n                    )\n\n                else:\n                    raise ValueError(\n                        f\"Unsupported image detail {detail}, expected \"\n                        f\"one of ['low', 'auto', 'high'].\",\n                    )\n\n        else:\n            raise ValueError(\n                \"The type field currently only supports 'text' \"\n                f\"and 'image_url', but got {typ}.\",\n            )\n\n    return num_tokens\n\n\nclass OpenAITokenCounter(TokenCounterBase):\n    \"\"\"The OpenAI token counting class.\"\"\"\n\n    def __init__(self, model_name: str) -> None:\n        \"\"\"Initialize the OpenAI token counter.\n\n        Args:\n            model_name (`str`):\n                The name of the OpenAI model to use for token counting.\n        \"\"\"\n        self.model_name = model_name\n\n    async def count(\n        self,\n        messages: list[dict[str, Any]],\n        tools: list[dict] = None,\n        **kwargs: Any,\n    ) -> int:\n        \"\"\"Count the token numbers of the given messages.\n\n        .. note:: OpenAI hasn't provided an official guide for counting tokens\n         with tools. If you have any ideas, please open an issue on\n         our GitHub repository.\n\n        Args:\n            messages (`list[dict[str, Any]]`):\n                A list of dictionaries, where `role` and `content` fields are\n                required.\n            tools (`list[dict]`, defaults to `None`):\n        \"\"\"\n        import tiktoken\n\n        try:\n            encoding = tiktoken.encoding_for_model(self.model_name)\n        except KeyError:\n            encoding = tiktoken.get_encoding(\"o200k_base\")\n\n        tokens_per_message = 3\n        tokens_per_name = 1\n\n        # every reply is primed with <|start|>assistant<|message|>\n        num_tokens = 3\n        for message in messages:\n            num_tokens += tokens_per_message\n            for key, value in message.items():\n                # Considering vision models\n                if key == \"content\" and isinstance(value, list):\n                    num_tokens += (\n                        _count_content_tokens_for_openai_vision_model(\n                            self.model_name,\n                            value,\n                            encoding,\n                        )\n                    )\n\n                elif isinstance(value, str):\n                    num_tokens += len(encoding.encode(value))\n\n                elif value is None:\n                    continue\n\n                elif key == \"tool_calls\":\n                    # TODO: This is only a temporary solution, since OpenAI\n                    # hasn't provided an official guide for counting tokens\n                    # with tool results.\n                    num_tokens += len(\n                        encoding.encode(\n                            json.dumps(value, ensure_ascii=False),\n                        ),\n                    )\n\n                else:\n                    raise TypeError(\n                        f\"Invalid type {type(value)} in the {key} field: \"\n                        f\"{value}\",\n                    )\n\n                if key == \"name\":\n                    num_tokens += tokens_per_name\n\n        if tools:\n            num_tokens += _calculate_tokens_for_tools(\n                self.model_name,\n                tools,\n                encoding,\n            )\n\n        return num_tokens\n"
  },
  {
    "path": "src/agentscope/token/_token_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The token base class in agentscope.\"\"\"\nfrom abc import abstractmethod\nfrom typing import Any\n\n\nclass TokenCounterBase:\n    \"\"\"The base class for token counting.\"\"\"\n\n    @abstractmethod\n    async def count(\n        self,\n        messages: list[dict],\n        **kwargs: Any,\n    ) -> int:\n        \"\"\"Count the number of tokens by the given model and messages.\"\"\"\n"
  },
  {
    "path": "src/agentscope/tool/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The tool module in agentscope.\"\"\"\n\nfrom ._response import ToolResponse\nfrom ._coding import (\n    execute_python_code,\n    execute_shell_command,\n)\nfrom ._text_file import (\n    view_text_file,\n    write_text_file,\n    insert_text_file,\n)\nfrom ._multi_modality import (\n    dashscope_text_to_image,\n    dashscope_text_to_audio,\n    dashscope_image_to_text,\n    openai_text_to_image,\n    openai_text_to_audio,\n    openai_edit_image,\n    openai_create_image_variation,\n    openai_image_to_text,\n    openai_audio_to_text,\n)\nfrom ._toolkit import Toolkit\n\n__all__ = [\n    \"Toolkit\",\n    \"ToolResponse\",\n    \"execute_python_code\",\n    \"execute_shell_command\",\n    \"view_text_file\",\n    \"write_text_file\",\n    \"insert_text_file\",\n    \"dashscope_text_to_image\",\n    \"dashscope_text_to_audio\",\n    \"dashscope_image_to_text\",\n    \"openai_text_to_image\",\n    \"openai_text_to_audio\",\n    \"openai_edit_image\",\n    \"openai_create_image_variation\",\n    \"openai_image_to_text\",\n    \"openai_audio_to_text\",\n]\n"
  },
  {
    "path": "src/agentscope/tool/_async_wrapper.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The functions that wrap object, sync generator, and async generator\ninto async generators.\n\nTODO: handle the exception raised when yielding from async generator\n into a normal ToolResponse instance.\n\"\"\"\nimport asyncio\nfrom typing import AsyncGenerator, Generator, Callable, Awaitable\n\nfrom ._response import ToolResponse\nfrom ..message import TextBlock\nfrom .._utils._common import _execute_async_or_sync_func\n\n\nasync def _postprocess_tool_response(\n    tool_response: ToolResponse,\n    postprocess_func: (\n        Callable[[ToolResponse], ToolResponse | None]\n        | Callable[[ToolResponse], Awaitable[ToolResponse | None]]\n    )\n    | None,\n) -> ToolResponse:\n    \"\"\"Post-process a ToolResponse object with the given function.\n\n    Supports both sync and async postprocess_func.\n    \"\"\"\n    if postprocess_func:\n        processed_response = await _execute_async_or_sync_func(\n            postprocess_func,\n            tool_response,\n        )\n        if processed_response:\n            return processed_response\n    return tool_response\n\n\nasync def _object_wrapper(\n    obj: ToolResponse,\n    postprocess_func: (\n        Callable[[ToolResponse], ToolResponse | None]\n        | Callable[[ToolResponse], Awaitable[ToolResponse | None]]\n    )\n    | None,\n) -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"Wrap a ToolResponse object to an async generator.\"\"\"\n    yield await _postprocess_tool_response(obj, postprocess_func)\n\n\nasync def _sync_generator_wrapper(\n    sync_generator: Generator[ToolResponse, None, None],\n    postprocess_func: (\n        Callable[[ToolResponse], ToolResponse | None]\n        | Callable[[ToolResponse], Awaitable[ToolResponse | None]]\n    )\n    | None,\n) -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"Wrap a sync generator to an async generator.\"\"\"\n    for chunk in sync_generator:\n        yield await _postprocess_tool_response(chunk, postprocess_func)\n\n\nasync def _async_generator_wrapper(\n    async_func: AsyncGenerator[ToolResponse, None],\n    postprocess_func: (\n        Callable[[ToolResponse], ToolResponse | None]\n        | Callable[[ToolResponse], Awaitable[ToolResponse | None]]\n    )\n    | None,\n) -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"When the function is interrupted during generating the tool\n    response, add an interrupted message to the response, and postpone\n    the CancelledError to the caller.\"\"\"\n\n    last_chunk = None\n    try:\n        async for chunk in async_func:\n            processed_chunk = await _postprocess_tool_response(\n                chunk,\n                postprocess_func,\n            )\n            yield processed_chunk\n            last_chunk = processed_chunk\n\n    except asyncio.CancelledError:\n        interrupted_info = TextBlock(\n            type=\"text\",\n            text=\"<system-info>\"\n            \"The tool call has been interrupted by the user.\"\n            \"</system-info>\",\n        )\n        if last_chunk:\n            last_chunk.content.append(interrupted_info)\n            last_chunk.is_interrupted = True\n            last_chunk.is_last = True\n            yield await _postprocess_tool_response(\n                last_chunk,\n                postprocess_func,\n            )\n\n        else:\n            yield await _postprocess_tool_response(\n                ToolResponse(\n                    content=[interrupted_info],\n                    is_interrupted=True,\n                    is_last=True,\n                ),\n                postprocess_func,\n            )\n"
  },
  {
    "path": "src/agentscope/tool/_coding/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The coding-related tools module in agentscope.\"\"\"\n\nfrom ._python import execute_python_code\nfrom ._shell import execute_shell_command\n\n__all__ = [\n    \"execute_python_code\",\n    \"execute_shell_command\",\n]\n"
  },
  {
    "path": "src/agentscope/tool/_coding/_python.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=unused-argument\n\"\"\"The Python code execution tool in agentscope.\"\"\"\n\nimport asyncio\nimport os\nimport sys\nimport tempfile\nfrom typing import Any\n\nimport shortuuid\n\nfrom ...message import TextBlock\nfrom .._response import ToolResponse\n\n\nasync def execute_python_code(\n    code: str,\n    timeout: float = 300,\n    **kwargs: Any,\n) -> ToolResponse:\n    \"\"\"Execute the given python code in a temp file and capture the return\n    code, standard output and error. Note you must `print` the output to get\n    the result, and the tmp file will be removed right after the execution.\n\n    Args:\n        code (`str`):\n            The Python code to be executed.\n        timeout (`float`, defaults to `300`):\n            The maximum time (in seconds) allowed for the code to run.\n\n    Returns:\n        `ToolResponse`:\n            The response containing the return code, standard output, and\n            standard error of the executed code.\n    \"\"\"\n\n    with tempfile.TemporaryDirectory() as temp_dir:\n        temp_file = os.path.join(temp_dir, f\"tmp_{shortuuid.uuid()}.py\")\n        with open(temp_file, \"w\", encoding=\"utf-8\") as f:\n            f.write(code)\n\n        env = os.environ.copy()\n        env[\"PYTHONUTF8\"] = \"1\"\n        env[\"PYTHONIOENCODING\"] = \"utf-8\"\n        proc = await asyncio.create_subprocess_exec(\n            sys.executable,\n            \"-u\",\n            temp_file,\n            stdout=asyncio.subprocess.PIPE,\n            stderr=asyncio.subprocess.PIPE,\n            env=env,\n        )\n\n        try:\n            await asyncio.wait_for(proc.wait(), timeout=timeout)\n            stdout, stderr = await proc.communicate()\n            stdout_str = stdout.decode(\"utf-8\")\n            stderr_str = stderr.decode(\"utf-8\")\n            returncode = proc.returncode\n\n        except asyncio.TimeoutError:\n            stderr_suffix = (\n                f\"TimeoutError: The code execution exceeded \"\n                f\"the timeout of {timeout} seconds.\"\n            )\n            returncode = -1\n            try:\n                proc.terminate()\n                stdout, stderr = await proc.communicate()\n                stdout_str = stdout.decode(\"utf-8\")\n                stderr_str = stderr.decode(\"utf-8\")\n                if stderr_str:\n                    stderr_str += f\"\\n{stderr_suffix}\"\n                else:\n                    stderr_str = stderr_suffix\n            except ProcessLookupError:\n                stdout_str = \"\"\n                stderr_str = stderr_suffix\n\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=f\"<returncode>{returncode}</returncode>\"\n                    f\"<stdout>{stdout_str}</stdout>\"\n                    f\"<stderr>{stderr_str}</stderr>\",\n                ),\n            ],\n        )\n"
  },
  {
    "path": "src/agentscope/tool/_coding/_shell.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=unused-argument\n\"\"\"The shell command tool in agentscope.\"\"\"\n\nimport asyncio\nfrom typing import Any\n\nfrom .._response import ToolResponse\nfrom ...message import TextBlock\n\n\nasync def execute_shell_command(\n    command: str,\n    timeout: int = 300,\n    **kwargs: Any,\n) -> ToolResponse:\n    \"\"\"Execute given command and return the return code, standard output and\n    error within <returncode></returncode>, <stdout></stdout> and\n    <stderr></stderr> tags.\n\n    Args:\n        command (`str`):\n            The shell command to execute.\n        timeout (`float`, defaults to `300`):\n            The maximum time (in seconds) allowed for the command to run.\n\n    Returns:\n        `ToolResponse`:\n            The tool response containing the return code, standard output, and\n            standard error of the executed command.\n    \"\"\"\n\n    proc = await asyncio.create_subprocess_shell(\n        command,\n        stdout=asyncio.subprocess.PIPE,\n        stderr=asyncio.subprocess.PIPE,\n        bufsize=0,\n    )\n\n    try:\n        await asyncio.wait_for(proc.wait(), timeout=timeout)\n        stdout, stderr = await proc.communicate()\n        stdout_str = stdout.decode(\"utf-8\")\n        stderr_str = stderr.decode(\"utf-8\")\n        returncode = proc.returncode\n\n    except asyncio.TimeoutError:\n        stderr_suffix = (\n            f\"TimeoutError: The command execution exceeded \"\n            f\"the timeout of {timeout} seconds.\"\n        )\n        returncode = -1\n        try:\n            proc.terminate()\n            stdout, stderr = await proc.communicate()\n            stdout_str = stdout.decode(\"utf-8\")\n            stderr_str = stderr.decode(\"utf-8\")\n            if stderr_str:\n                stderr_str += f\"\\n{stderr_suffix}\"\n            else:\n                stderr_str = stderr_suffix\n        except ProcessLookupError:\n            stdout_str = \"\"\n            stderr_str = stderr_suffix\n\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=(\n                    f\"<returncode>{returncode}</returncode>\"\n                    f\"<stdout>{stdout_str}</stdout>\"\n                    f\"<stderr>{stderr_str}</stderr>\"\n                ),\n            ),\n        ],\n    )\n"
  },
  {
    "path": "src/agentscope/tool/_multi_modality/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The multi-modal-related tools module in agentscope.\"\"\"\nfrom ._dashscope_tools import (\n    dashscope_image_to_text,\n    dashscope_text_to_audio,\n    dashscope_text_to_image,\n)\nfrom ._openai_tools import (\n    openai_text_to_image,\n    openai_edit_image,\n    openai_text_to_audio,\n    openai_create_image_variation,\n    openai_image_to_text,\n    openai_audio_to_text,\n)\n\n__all__ = [\n    \"dashscope_image_to_text\",\n    \"dashscope_text_to_audio\",\n    \"dashscope_text_to_image\",\n    \"openai_text_to_image\",\n    \"openai_text_to_audio\",\n    \"openai_edit_image\",\n    \"openai_create_image_variation\",\n    \"openai_image_to_text\",\n    \"openai_audio_to_text\",\n]\n"
  },
  {
    "path": "src/agentscope/tool/_multi_modality/_dashscope_tools.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Use DashScope API to generate images,\nconvert text to audio, and convert images to text.\nPlease refer to the `official documentation <https://dashscope.aliyun.com/>`_\n for more details.\n\"\"\"\nimport base64\nfrom typing import Literal, Sequence\n\nimport os\n\n\nfrom ..._utils._common import _get_bytes_from_web_url\nfrom ...message import ImageBlock, TextBlock, AudioBlock\nfrom ...tool import ToolResponse\n\n\ndef dashscope_text_to_image(\n    prompt: str,\n    api_key: str,\n    n: int = 1,\n    size: Literal[\"1024*1024\", \"720*1280\", \"1280*720\"] = \"1024*1024\",\n    model: str = \"wanx-v1\",\n    use_base64: bool = False,\n) -> ToolResponse:\n    \"\"\"Generate image(s) based on the given prompt, and return image url(s)\n    or base64 data.\n\n    Args:\n        prompt (`str`):\n            The text prompt to generate image.\n        api_key (`str`):\n            The api key for the dashscope api.\n        n (`int`, defaults to `1`):\n            The number of images to generate.\n        size (`Literal[\"1024*1024\", \"720*1280\", \"1280*720\"]`, defaults to \\\n         `\"1024*1024\"`):\n            Size of the image.\n        model (`str`, defaults to '\"wanx-v1\"'):\n            The model to use, such as \"wanx-v1\", \"qwen-image\",\n            \"wan2.2-t2i-flash\", etc.\n        use_base64 (`bool`, defaults to 'False'):\n            Whether to use base64 data for images.\n\n    Returns:\n        `ToolResponse`:\n            A ToolResponse containing the generated content\n            (ImageBlock/TextBlock/AudioBlock) or error information if the\n            operation failed.\n    \"\"\"\n    try:\n        import dashscope\n\n        response = dashscope.ImageSynthesis.call(\n            model=model,\n            prompt=prompt,\n            api_key=api_key,\n            n=n,\n            size=size,\n        )\n        images = response.output[\"results\"]\n        urls = [_[\"url\"] for _ in images]\n\n        image_blocks: list = []\n\n        if urls is not None:\n            for url in urls:\n                if use_base64:\n                    extension = url.split(\".\")[-1].lower()\n\n                    image_data = _get_bytes_from_web_url(url)\n                    image_blocks.append(\n                        ImageBlock(\n                            type=\"image\",\n                            source={\n                                \"type\": \"base64\",\n                                \"media_type\": f\"image/{extension}\",\n                                \"data\": image_data,\n                            },\n                        ),\n                    )\n                else:\n                    image_blocks.append(\n                        ImageBlock(\n                            type=\"image\",\n                            source={\n                                \"type\": \"url\",\n                                \"url\": url,\n                            },\n                        ),\n                    )\n\n            return ToolResponse(\n                content=image_blocks,\n            )\n\n        else:\n            return ToolResponse(\n                [\n                    TextBlock(\n                        type=\"text\",\n                        text=\"Error: Failed to generate images\",\n                    ),\n                ],\n            )\n    except Exception as e:\n        return ToolResponse(\n            [\n                TextBlock(\n                    type=\"text\",\n                    text=f\"Failed to generate images: {str(e)}\",\n                ),\n            ],\n        )\n\n\ndef dashscope_image_to_text(\n    image_urls: str | Sequence[str],\n    api_key: str,\n    prompt: str = \"Describe the image\",\n    model: str = \"qwen-vl-plus\",\n) -> ToolResponse:\n    \"\"\"Generate text based on the given images.\n\n    Args:\n        image_urls (`str | Sequence[str]`):\n            The url of single or multiple images.\n        api_key (`str`):\n            The api key for the dashscope api.\n        prompt (`str`, defaults to 'Describe the image' ):\n            The text prompt.\n        model (`str`, defaults to 'qwen-vl-plus'):\n            The model to use in DashScope MultiModal API.\n\n    Returns:\n        `ToolResponse`:\n            A ToolResponse containing the generated content\n            (ImageBlock/TextBlock/AudioBlock) or error information if the\n            operation failed.\n    \"\"\"\n\n    if isinstance(image_urls, str):\n        image_urls = [image_urls]\n\n    # Check if the local url is valid\n    img_abs_urls = []\n    for url in image_urls:\n        if os.path.exists(url):\n            if os.path.isfile(url):\n                img_abs_urls.append(os.path.abspath(url))\n            else:\n                return ToolResponse(\n                    [\n                        TextBlock(\n                            type=\"text\",\n                            text=f'Error: The input image url \"{url}\" is '\n                            f\"not a file.\",\n                        ),\n                    ],\n                )\n        else:\n            # Maybe a web url or an invalid url, we leave it to the API\n            # to handle\n            img_abs_urls.append(url)\n\n    # Convert image paths according to the model requirements\n    contents = []\n    for url in img_abs_urls:\n        contents.append(\n            {\n                \"image\": url,\n            },\n        )\n\n    contents.append({\"text\": prompt})\n\n    # currently only support one round of conversation\n    # if multiple rounds of conversation are needed,\n    # it would be better to implement an Agent class\n    sys_message = {\n        \"role\": \"system\",\n        \"content\": [{\"text\": \"You are a helpful assistant.\"}],\n    }\n    user_message = {\n        \"role\": \"user\",\n        \"content\": contents,\n    }\n    messages = [sys_message, user_message]\n    try:\n        import dashscope\n\n        response = dashscope.MultiModalConversation.call(\n            model=model,\n            messages=messages,\n            api_key=api_key,\n        )\n        content = response.output[\"choices\"][0][\"message\"][\"content\"]\n        if isinstance(content, list):\n            content = content[0][\"text\"]\n        if content is not None:\n            return ToolResponse(\n                [\n                    TextBlock(\n                        type=\"text\",\n                        text=content,\n                    ),\n                ],\n            )\n        else:\n            return ToolResponse(\n                [\n                    TextBlock(\n                        type=\"text\",\n                        text=\"Error: Failed to generate text\",\n                    ),\n                ],\n            )\n    except Exception as e:\n        return ToolResponse(\n            [\n                TextBlock(\n                    type=\"text\",\n                    text=f\"Failed to generate text: {str(e)}\",\n                ),\n            ],\n        )\n\n\ndef dashscope_text_to_audio(\n    text: str,\n    api_key: str,\n    model: str = \"sambert-zhichu-v1\",\n    sample_rate: int = 48000,\n) -> ToolResponse:\n    \"\"\"Convert the given text to audio.\n\n    Args:\n        text (`str`):\n            The text to be converted into audio.\n        api_key (`str`):\n            The api key for the dashscope API.\n        model (`str`, defaults to 'sambert-zhichu-v1'):\n            The model to use. Full model list can be found in the\n            `official document\n            <https://help.aliyun.com/zh/model-studio/sambert-python-sdk>`_.\n        sample_rate (`int`, defaults to 48000):\n            Sample rate of the audio.\n\n    Returns:\n        `ToolResponse`:\n            A ToolResponse containing the generated content\n            (ImageBlock/TextBlock/AudioBlock) or error information if the\n            operation failed.\n\n    \"\"\"\n    try:\n        import dashscope\n\n        dashscope.api_key = api_key\n\n        res = dashscope.audio.tts.SpeechSynthesizer.call(\n            model=model,\n            text=text,\n            sample_rate=sample_rate,\n            format=\"wav\",\n        )\n\n        audio_data = res.get_audio_data()\n\n        if audio_data is not None:\n            audio_base64 = base64.b64encode(audio_data).decode(\"utf-8\")\n\n            return ToolResponse(\n                [\n                    AudioBlock(\n                        type=\"audio\",\n                        source={\n                            \"type\": \"base64\",\n                            \"media_type\": \"audio/wav\",\n                            \"data\": audio_base64,\n                        },\n                    ),\n                ],\n            )\n        else:\n            return ToolResponse(\n                [\n                    TextBlock(\n                        type=\"text\",\n                        text=\"Error: Failed to generate audio\",\n                    ),\n                ],\n            )\n    except Exception as e:\n        return ToolResponse(\n            [\n                TextBlock(\n                    type=\"text\",\n                    text=f\"Failed to generate audio: {str(e)}\",\n                ),\n            ],\n        )\n"
  },
  {
    "path": "src/agentscope/tool/_multi_modality/_openai_tools.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nWrap OpenAI API calls as tools. Refer the official\n`OpenAI API documentation <https://platform.openai.com/docs/overview>`_ for\nmore details.\n\"\"\"\nimport base64\nfrom io import BytesIO\nimport os\nfrom typing import Literal, IO\nimport requests\n\nfrom .. import ToolResponse\nfrom ...formatter._openai_formatter import _to_openai_image_url\nfrom ...message import (\n    ImageBlock,\n    TextBlock,\n    Base64Source,\n    URLSource,\n    AudioBlock,\n)\n\n\ndef _parse_url(url: str) -> BytesIO | IO[bytes]:\n    \"\"\"\n    If url is a local file path, return a BytesIO of the file content.\n    If url is a web URL, fetch the content and return as BytesIO.\n    \"\"\"\n    if url.startswith((\"http://\", \"https://\")):\n        response = requests.get(url)\n        response.raise_for_status()  # Raise an exception for HTTP errors\n        return BytesIO(response.content)\n    else:\n        if not os.path.exists(url):\n            raise FileNotFoundError(f\"File not found: {url}\")\n        return open(os.path.abspath(url), \"rb\")\n\n\ndef openai_text_to_image(\n    prompt: str,\n    api_key: str,\n    n: int = 1,\n    model: Literal[\"dall-e-2\", \"dall-e-3\", \"gpt-image-1\"] = \"dall-e-2\",\n    size: Literal[\n        \"256x256\",\n        \"512x512\",\n        \"1024x1024\",\n        \"1792x1024\",\n        \"1024x1792\",\n    ] = \"256x256\",\n    quality: Literal[\n        \"auto\",\n        \"standard\",\n        \"hd\",\n        \"high\",\n        \"medium\",\n        \"low\",\n    ] = \"auto\",\n    style: Literal[\"vivid\", \"natural\"] = \"vivid\",\n    response_format: Literal[\"url\", \"b64_json\"] = \"url\",\n) -> ToolResponse:\n    \"\"\"\n    Generate image(s) based on the given prompt, and return image URL(s) or\n    base64 data.\n\n    Args:\n        prompt (`str`):\n            The text prompt to generate images.\n        api_key (`str`):\n            The API key for the OpenAI API.\n        n (`int`, defaults to `1`):\n            The number of images to generate.\n        model (`Literal[\"dall-e-2\", \"dall-e-3\"]`, defaults to `\"dall-e-2\"`):\n            The model to use for image generation.\n        size (`Literal[\"256x256\", \"512x512\", \"1024x1024\", \"1792x1024\", \\\n        \"1024x1792\"]`, defaults to `\"256x256\"`):\n            The size of the generated images.\n            Must be one of 1024x1024, 1536x1024 (landscape), 1024x1536 (\n            portrait), or auto (default value) for gpt-image-1,\n            one of 256x256, 512x512, or 1024x1024 for dall-e-2,\n            and one of 1024x1024, 1792x1024, or 1024x1792 for dall-e-3.\n        quality (`Literal[\"auto\", \"standard\", \"hd\", \"high\", \"medium\", \\\n        \"low\"]`,  defaults to `\"auto\"`):\n            The quality of the image that will be generated.\n\n            - `auto` (default value) will automatically select the best\n              quality for the given model.\n            - `high`, `medium` and `low` are supported for gpt-image-1.\n            - `hd` and `standard` are supported for dall-e-3.\n            - `standard` is the only option for dall-e-2.\n        style (`Literal[\"vivid\", \"natural\"]`, defaults to `\"vivid\"`):\n            The style of the generated images.\n            This parameter is only supported for dall-e-3.\n            Must be one of `vivid` or `natural`.\n\n            - `Vivid` causes the model to lean towards generating hyper-real\n              and dramatic images.\n            - `Natural` causes the model to produce more natural,\n              less hyper-real looking images.\n        response_format (`Literal[\"url\", \"b64_json\"]`, defaults to `\"url\"`):\n            The format in which generated images with dall-e-2 and dall-e-3\n            are returned.\n\n            - Must be one of \"url\" or \"b64_json\".\n            - URLs are only valid for 60 minutes after the image has been\n              generated.\n            - This parameter isn't supported for gpt-image-1 which will always\n              return base64-encoded images.\n    Returns:\n        `ToolResponse`:\n            A ToolResponse containing the generated content\n            (ImageBlock/TextBlock/AudioBlock) or error information if the\n            operation failed.\n    \"\"\"\n\n    kwargs = {\n        \"model\": model,\n        \"prompt\": prompt,\n        \"n\": n,\n        \"size\": size,\n    }\n    if model == \"dall-e-3\":\n        kwargs[\"style\"] = style\n    if model != \"dall-e-2\":\n        kwargs[\"quality\"] = quality\n    if model != \"gpt-image-1\":\n        kwargs[\"response_format\"] = response_format\n    if model == \"gpt-image-1\":\n        response_format = \"b64_json\"\n\n    try:\n        import openai\n\n        client = openai.OpenAI(\n            api_key=api_key,\n        )\n        response = client.images.generate(\n            **kwargs,\n        )\n        image_blocks: list = []\n        if response_format == \"url\":\n            image_urls = [_.url for _ in response.data]\n            for image_url in image_urls:\n                image_blocks.append(\n                    ImageBlock(\n                        type=\"image\",\n                        source=URLSource(\n                            type=\"url\",\n                            url=image_url,\n                        ),\n                    ),\n                )\n        else:\n            image_datas = [_.b64_json for _ in response.data]\n            for image_data in image_datas:\n                image_blocks.append(\n                    ImageBlock(\n                        type=\"image\",\n                        source=Base64Source(\n                            type=\"base64\",\n                            media_type=\"image/png\",\n                            data=image_data,\n                        ),\n                    ),\n                )\n        return ToolResponse(\n            content=image_blocks,\n        )\n    except Exception as e:\n        return ToolResponse(\n            [\n                TextBlock(\n                    type=\"text\",\n                    text=f\"Failed to generate image: {str(e)}\",\n                ),\n            ],\n        )\n\n\ndef openai_edit_image(\n    image_url: str,\n    prompt: str,\n    api_key: str,\n    model: Literal[\"dall-e-2\", \"gpt-image-1\"] = \"dall-e-2\",\n    mask_url: str | None = None,\n    n: int = 1,\n    size: Literal[\n        \"256x256\",\n        \"512x512\",\n        \"1024x1024\",\n    ] = \"256x256\",\n    response_format: Literal[\"url\", \"b64_json\"] = \"url\",\n) -> ToolResponse:\n    \"\"\"\n    Edit an image based on the provided mask and prompt, and return the edited\n    image URL(s) or base64 data.\n\n    Args:\n        image_url (`str`):\n            The file path or URL to the image that needs editing.\n        prompt (`str`):\n            The text prompt describing the edits to be made to the image.\n        api_key (`str`):\n            The API key for the OpenAI API.\n        model (`Literal[\"dall-e-2\", \"gpt-image-1\"]`, defaults to `\"dall-e-2\"`):\n            The model to use for image generation.\n        mask_url (`str | None`, defaults to `None`):\n            The file path or URL to the mask image that specifies the regions\n            to be edited.\n        n (`int`, defaults to `1`):\n            The number of edited images to generate.\n        size (`Literal[\"256x256\", \"512x512\", \"1024x1024\"]`, defaults to \\\n        `\"256x256\"`):\n            The size of the edited images.\n        response_format (`Literal[\"url\", \"b64_json\"]`, defaults to `\"url\"`):\n            The format in which generated images are returned.\n\n            - Must be one of \"url\" or \"b64_json\".\n            - URLs are only valid for 60 minutes after generation.\n            - This parameter isn't supported for gpt-image-1 which will\n              always return base64-encoded images.\n\n    Returns:\n        `ToolResponse`:\n            A ToolResponse containing the generated content\n            (ImageBlock/TextBlock/AudioBlock) or error information if the\n            operation failed.\n\n    \"\"\"\n\n    try:\n        import openai\n\n        client = openai.OpenAI(\n            api_key=api_key,\n        )\n\n        def prepare_image(url_or_path: str) -> BytesIO:\n            from PIL import Image\n\n            if url_or_path.startswith((\"http://\", \"https://\")):\n                response = requests.get(url_or_path)\n                response.raise_for_status()\n                img = Image.open(BytesIO(response.content))\n            else:\n                img = Image.open(url_or_path)\n\n            if img.mode != \"RGBA\":\n                img = img.convert(\"RGBA\")\n            img_buffer = BytesIO()\n            img.save(img_buffer, format=\"PNG\")\n            img_buffer.seek(0)\n            img_buffer.name = \"image.png\"\n            return img_buffer\n\n        image_file = prepare_image(image_url)\n\n        kwargs = {\n            \"model\": model,\n            \"image\": image_file,\n            \"prompt\": prompt,\n            \"n\": n,\n            \"size\": size,\n        }\n\n        if mask_url:\n            kwargs[\"mask\"] = prepare_image(mask_url)\n\n        if model == \"dall-e-2\":\n            kwargs[\"response_format\"] = response_format\n        else:\n            response_format = \"b64_json\"\n\n        response = client.images.edit(**kwargs)\n\n        if response_format == \"url\":\n            urls = [_.url for _ in response.data]\n            image_blocks: list = []\n            for url in urls:\n                image_blocks.append(\n                    ImageBlock(\n                        type=\"image\",\n                        source=URLSource(\n                            type=\"url\",\n                            url=url,\n                        ),\n                    ),\n                )\n            return ToolResponse(\n                content=image_blocks,\n            )\n        elif response_format == \"b64_json\":\n            image_datas = [_.b64_json for _ in response.data]\n            image_blocks = []\n            for image_data in image_datas:\n                image_blocks.append(\n                    ImageBlock(\n                        type=\"image\",\n                        source=Base64Source(\n                            type=\"base64\",\n                            media_type=\"image/png\",\n                            data=image_data,\n                        ),\n                    ),\n                )\n            return ToolResponse(\n                content=image_blocks,\n            )\n    except Exception as e:\n        return ToolResponse(\n            [\n                TextBlock(\n                    type=\"text\",\n                    text=f\"Failed to generate image: {str(e)}\",\n                ),\n            ],\n        )\n\n\ndef openai_create_image_variation(\n    image_url: str,\n    api_key: str,\n    n: int = 1,\n    model: Literal[\"dall-e-2\"] = \"dall-e-2\",\n    size: Literal[\n        \"256x256\",\n        \"512x512\",\n        \"1024x1024\",\n    ] = \"256x256\",\n    response_format: Literal[\"url\", \"b64_json\"] = \"url\",\n) -> ToolResponse:\n    \"\"\"\n    Create variations of an image and return the image URL(s) or base64 data.\n\n    Args:\n        image_url (`str`):\n            The file path or URL to the image from which variations will be\n            generated.\n        api_key (`str`):\n            The API key for the OpenAI API.\n        n (`int`, defaults to `1`):\n            The number of image variations to generate.\n        model (` Literal[\"dall-e-2\"]`, default to `dall-e-2`):\n            The model to use for image variation.\n        size (`Literal[\"256x256\", \"512x512\", \"1024x1024\"]`, defaults to \\\n        `\"256x256\"`):\n            The size of the generated image variations.\n        response_format (`Literal[\"url\", \"b64_json\"]`, defaults to `\"url\"`):\n            The format in which generated images are returned.\n\n            - Must be one of url or b64_json.\n            - URLs are only valid for 60 minutes after the image has been\n              generated.\n\n    Returns:\n        `ToolResponse`:\n            A ToolResponse containing the generated content\n            (ImageBlock/TextBlock/AudioBlock) or error information if the\n            operation failed.\n    \"\"\"\n\n    # _parse_url handles both local and web URLs and returns BytesIO\n    image = _parse_url(image_url)\n    try:\n        import openai\n\n        client = openai.OpenAI(\n            api_key=api_key,\n        )\n        response = client.images.create_variation(\n            model=model,\n            image=image,\n            n=n,\n            size=size,\n        )\n        image_blocks: list = []\n        if response_format == \"url\":\n            urls = [_.url for _ in response.data]\n            for url in urls:\n                image_blocks.append(\n                    ImageBlock(\n                        type=\"image\",\n                        source=URLSource(\n                            type=\"url\",\n                            url=url,\n                        ),\n                    ),\n                )\n        else:\n            image_datas = [_.b64_json for _ in response.data]\n            for image_data in image_datas:\n                image_blocks.append(\n                    ImageBlock(\n                        type=\"image\",\n                        source=Base64Source(\n                            type=\"base64\",\n                            media_type=\"image/png\",\n                            data=image_data,\n                        ),\n                    ),\n                )\n        return ToolResponse(\n            content=image_blocks,\n        )\n    except Exception as e:\n        return ToolResponse(\n            [\n                TextBlock(\n                    type=\"text\",\n                    text=f\"Failed to generate image: {str(e)}\",\n                ),\n            ],\n        )\n\n\ndef openai_image_to_text(\n    image_urls: str | list[str],\n    api_key: str,\n    prompt: str = \"Describe the image\",\n    model: str = \"gpt-4o\",\n) -> ToolResponse:\n    \"\"\"\n    Generate descriptive text for given image(s) using a specified model, and\n    return the generated text.\n\n    Args:\n        image_urls (`str | list[str]`):\n            The URL or list of URLs pointing to the images that need to be\n            described.\n        api_key (`str`):\n            The API key for the OpenAI API.\n        prompt (`str`, defaults to `\"Describe the image\"`):\n            The prompt that instructs the model on how to describe\n            the image(s).\n        model (`str`, defaults to `\"gpt-4o\"`):\n            The model to use for generating the text descriptions.\n\n    Returns:\n        `ToolResponse`:\n            A ToolResponse containing the generated content\n            (ImageBlock/TextBlock/AudioBlock) or error information if the\n            operation failed.\n\n    \"\"\"\n    if isinstance(image_urls, str):\n        image_urls = [image_urls]\n\n    content = []\n    for url in image_urls:\n        content.append(\n            {\n                \"type\": \"image_url\",\n                \"image_url\": {\n                    \"url\": _to_openai_image_url(url),\n                },\n            },\n        )\n    content.append(\n        {\n            \"type\": \"text\",\n            \"text\": prompt,\n        },\n    )\n    messages = [\n        {\n            \"role\": \"user\",\n            \"content\": content,\n        },\n    ]\n\n    try:\n        import openai\n\n        client = openai.OpenAI(\n            api_key=api_key,\n        )\n        response = client.chat.completions.create(\n            messages=messages,\n            model=model,\n        )\n        return ToolResponse(\n            [\n                TextBlock(\n                    type=\"text\",\n                    text=response.choices[0].message.content,\n                ),\n            ],\n        )\n    except Exception as e:\n        return ToolResponse(\n            [\n                TextBlock(\n                    type=\"text\",\n                    text=f\"Failed to generate text: {str(e)}\",\n                ),\n            ],\n        )\n\n\ndef openai_text_to_audio(\n    text: str,\n    api_key: str,\n    model: Literal[\"tts-1\", \"tts-1-hd\", \"gpt-4o-mini-tts\"] = \"tts-1\",\n    voice: Literal[\n        \"alloy\",\n        \"ash\",\n        \"ballad\",\n        \"coral\",\n        \"echo\",\n        \"fable\",\n        \"nova\",\n        \"onyx\",\n        \"sage\",\n        \"shimmer\",\n    ] = \"alloy\",\n    speed: float = 1.0,\n    res_format: Literal[\n        \"mp3\",\n        \"opus\",\n        \"aac\",\n        \"flac\",\n        \"wav\",\n        \"pcm\",\n    ] = \"mp3\",\n) -> ToolResponse:\n    \"\"\"\n    Convert text to an audio file using a specified model and voice.\n\n    Args:\n        text (`str`):\n            The text to convert to audio.\n        api_key (`str`):\n            The API key for the OpenAI API.\n        model (`Literal[\"tts-1\", \"tts-1-hd\"]`, defaults to `\"tts-1\"`):\n            The model to use for text-to-speech conversion.\n        voice (`Literal[\"alloy\", \"echo\", \"fable\", \"onyx\", \"nova\", \\\n        \"shimmer\"]`, defaults to `\"alloy\"`):\n            The voice to use for the audio output.\n        speed (`float`, defaults to `1.0`):\n            The speed of the audio playback. A value of 1.0 is normal speed.\n        res_format (`Literal[\"mp3\", \"wav\", \"opus\", \"aac\", \"flac\", \\\n        \"wav\", \"pcm\"]`, defaults to `\"mp3\"`):\n            The format of the audio file.\n\n    Returns:\n        `ToolResponse`:\n            A ToolResponse containing the generated content\n            (ImageBlock/TextBlock/AudioBlock) or error information if the\n            operation failed.\n    \"\"\"\n\n    try:\n        import openai\n\n        client = openai.OpenAI(\n            api_key=api_key,\n        )\n        response = client.audio.speech.create(\n            model=model,\n            voice=voice,\n            speed=speed,\n            input=text,\n            response_format=res_format,\n        )\n        audio_bytes = response.content\n        audio_base64 = base64.b64encode(audio_bytes).decode(\"utf-8\")\n\n        return ToolResponse(\n            [\n                AudioBlock(\n                    type=\"audio\",\n                    source=Base64Source(\n                        type=\"base64\",\n                        media_type=f\"audio/{res_format}\",\n                        data=audio_base64,\n                    ),\n                ),\n            ],\n        )\n    except Exception as e:\n        return ToolResponse(\n            [\n                TextBlock(\n                    type=\"text\",\n                    text=f\"Error: Failed to generate audio. {str(e)}\",\n                ),\n            ],\n        )\n\n\ndef openai_audio_to_text(\n    audio_file_url: str,\n    api_key: str,\n    language: str = \"en\",\n    temperature: float = 0.2,\n) -> ToolResponse:\n    \"\"\"\n    Convert an audio file to text using OpenAI's transcription service.\n\n    Args:\n        audio_file_url (`str`):\n            The file path or URL to the audio file that needs to be\n            transcribed.\n        api_key (`str`):\n            The API key for the OpenAI API.\n        language (`str`, defaults to `\"en\"`):\n            The language of the input audio in\n            `ISO-639-1 format \\\n            <https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes>`_\n            (e.g., \"en\", \"zh\", \"fr\"). Improves accuracy and latency.\n        temperature (`float`, defaults to `0.2`):\n            The temperature for the transcription, which affects the\n            randomness of the output.\n\n    Returns:\n        `ToolResponse`:\n            A ToolResponse containing the generated content\n            (ImageBlock/TextBlock/AudioBlock) or error information if the\n            operation failed.\n    \"\"\"\n\n    try:\n        import openai\n\n        client = openai.OpenAI(\n            api_key=api_key,\n        )\n\n        if audio_file_url.startswith((\"http://\", \"https://\")):\n            response = requests.get(audio_file_url)\n            response.raise_for_status()\n            audio_buffer = BytesIO(response.content)\n            import urllib.parse\n            from pathlib import Path\n\n            parsed_url = urllib.parse.urlparse(audio_file_url)\n            filename = Path(parsed_url.path).name or \"audio.mp3\"\n            audio_buffer.name = filename\n            audio_file = audio_buffer\n            transcription = client.audio.transcriptions.create(\n                model=\"whisper-1\",\n                file=audio_file,\n                language=language,\n                temperature=temperature,\n            )\n        else:\n            if not os.path.exists(audio_file_url):\n                raise FileNotFoundError(f\"File not found: {audio_file_url}\")\n            with open(audio_file_url, \"rb\") as audio_file:\n                transcription = client.audio.transcriptions.create(\n                    model=\"whisper-1\",\n                    file=audio_file,\n                    language=language,\n                    temperature=temperature,\n                )\n        return ToolResponse(\n            [\n                TextBlock(\n                    type=\"text\",\n                    text=transcription.text,\n                ),\n            ],\n        )\n    except Exception as e:\n        return ToolResponse(\n            [\n                TextBlock(\n                    type=\"text\",\n                    text=f\"Error: Failed to transcribe audio: {str(e)}\",\n                ),\n            ],\n        )\n"
  },
  {
    "path": "src/agentscope/tool/_response.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The tool response class.\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import Optional, List\n\nfrom .._utils._common import _get_timestamp\nfrom ..message import AudioBlock, ImageBlock, TextBlock, VideoBlock\n\n\n@dataclass\nclass ToolResponse:\n    \"\"\"The result chunk of a tool call.\"\"\"\n\n    content: List[TextBlock | ImageBlock | AudioBlock | VideoBlock]\n    \"\"\"The execution output of the tool function.\"\"\"\n\n    metadata: Optional[dict] = None\n    \"\"\"The metadata to be accessed within the agent, so that we don't need to\n    parse the tool result block.\"\"\"\n\n    stream: bool = False\n    \"\"\"Whether the tool output is streamed.\"\"\"\n\n    is_last: bool = True\n    \"\"\"Whether this is the last response in a stream tool execution.\"\"\"\n\n    is_interrupted: bool = False\n    \"\"\"Whether the tool execution is interrupted.\"\"\"\n\n    id: str = field(default_factory=lambda: _get_timestamp(True))\n    \"\"\"The identity of the tool response.\"\"\"\n"
  },
  {
    "path": "src/agentscope/tool/_text_file/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The text file tool module in agentscope.\"\"\"\nfrom ._view_text_file import view_text_file\nfrom ._write_text_file import (\n    insert_text_file,\n    write_text_file,\n)\n\n__all__ = [\n    \"insert_text_file\",\n    \"write_text_file\",\n    \"view_text_file\",\n]\n"
  },
  {
    "path": "src/agentscope/tool/_text_file/_utils.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The utility functions for text file tools in agentscope.\"\"\"\nfrom ...exception import ToolInvalidArgumentsError\n\n\ndef _calculate_view_ranges(\n    old_n_lines: int,\n    new_n_lines: int,\n    start: int,\n    end: int,\n    extra_view_n_lines: int = 5,\n) -> tuple[int, int]:\n    \"\"\"Calculate after writing the new content, the view ranges of the file.\n\n    Args:\n        old_n_lines (`int`):\n            The number of lines before writing the new content.\n        new_n_lines (`int`):\n            The number of lines after writing the new content.\n        start (`int`):\n            The start line of the writing range.\n        end (`int`):\n            The end line of the writing range.\n        extra_view_n_lines (`int`, optional):\n            The number of extra lines to view before and after the range.\n    \"\"\"\n\n    view_start = max(1, start - extra_view_n_lines)\n\n    delta_lines = new_n_lines - old_n_lines\n    view_end = min(end + delta_lines + extra_view_n_lines, new_n_lines)\n\n    return view_start, view_end\n\n\ndef _assert_ranges(\n    ranges: list[int],\n) -> None:\n    \"\"\"Check if the ranges are valid.\n\n    Raises:\n        ToolInvalidArgumentsError: If the ranges are invalid.\n    \"\"\"\n    if (\n        isinstance(ranges, list)\n        and len(ranges) == 2\n        and all(isinstance(i, int) for i in ranges)\n    ):\n        start, end = ranges\n        if start > end:\n            raise ToolInvalidArgumentsError(\n                f\"InvalidArgumentError: The start line is greater than the \"\n                f\"end line in the given range {ranges}.\",\n            )\n    else:\n        raise ToolInvalidArgumentsError(\n            f\"InvalidArgumentError: Invalid range format. Expected a list of \"\n            f\"two integers, but got {ranges}.\",\n        )\n\n\ndef _view_text_file(\n    file_path: str,\n    ranges: list[int] | None = None,\n) -> str:\n    \"\"\"Return the file content in the specified range with line numbers.\"\"\"\n    with open(file_path, \"r\", encoding=\"utf-8\") as file:\n        lines = file.readlines()\n\n    if ranges:\n        _assert_ranges(ranges)\n        start, end = ranges\n\n        if start > len(lines):\n            raise ToolInvalidArgumentsError(\n                f\"InvalidArgumentError: The range '{ranges}' is out of bounds \"\n                f\"for the file '{file_path}', which has only {len(lines)} \"\n                f\"lines.\",\n            )\n\n        view_content = [\n            f\"{index + start}: {line}\"\n            for index, line in enumerate(lines[start - 1 : end])\n        ]\n\n        return \"\".join(view_content)\n\n    return \"\".join(f\"{index + 1}: {line}\" for index, line in enumerate(lines))\n"
  },
  {
    "path": "src/agentscope/tool/_text_file/_view_text_file.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n# pylint: disable=line-too-long\n\"\"\"The view text file tool in agentscope.\"\"\"\nimport os\n\nfrom ._write_text_file import _view_text_file\nfrom .._response import ToolResponse\nfrom ...exception import ToolInvalidArgumentsError\nfrom ...message import TextBlock\n\n\nasync def view_text_file(\n    file_path: str,\n    ranges: list[int] | None = None,\n) -> ToolResponse:\n    \"\"\"View the file content in the specified range with line numbers. If `ranges` is not provided, the entire file will be returned.\n\n    Args:\n        file_path (`str`):\n            The target file path.\n        ranges:\n            The range of lines to be viewed (e.g. lines 1 to 100: [1, 100]), inclusive. If not provided, the entire file will be returned. To view the last 100 lines, use [-100, -1].\n\n    Returns:\n        `ToolResponse`:\n            The tool response containing the file content or an error message.\n    \"\"\"\n    file_path = os.path.expanduser(file_path)\n    if not os.path.exists(file_path):\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=f\"Error: The file {file_path} does not exist.\",\n                ),\n            ],\n        )\n    if not os.path.isfile(file_path):\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=f\"Error: The path {file_path} is not a file.\",\n                ),\n            ],\n        )\n\n    try:\n        content = _view_text_file(file_path, ranges)\n    except ToolInvalidArgumentsError as e:\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=e.message,\n                ),\n            ],\n        )\n\n    if ranges is None:\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=f\"\"\"The content of {file_path}:\n```\n{content}```\"\"\",\n                ),\n            ],\n        )\n    else:\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=f\"\"\"The content of {file_path} in {ranges} lines:\n```\n{content}```\"\"\",\n                ),\n            ],\n        )\n"
  },
  {
    "path": "src/agentscope/tool/_text_file/_write_text_file.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n# pylint: disable=line-too-long\n\"\"\"The text file tools in agentscope.\"\"\"\nimport os\n\nfrom ._utils import _calculate_view_ranges, _view_text_file\nfrom .._response import ToolResponse\nfrom ...message import TextBlock\n\n\nasync def insert_text_file(\n    file_path: str,\n    content: str,\n    line_number: int,\n) -> ToolResponse:\n    \"\"\"Insert the content at the specified line number in a text file.\n\n    Args:\n        file_path (`str`):\n            The target file path.\n        content (`str`):\n            The content to be inserted.\n        line_number (`int`):\n            The line number at which the content should be inserted, starting\n            from 1. If exceeds the number of lines in the file, it will be\n            appended to the end of the file.\n\n    Returns:\n        `ToolResponse`:\n            The tool response containing the result of the insertion operation.\n    \"\"\"\n    if line_number <= 0:\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=f\"InvalidArgumentsError: \"\n                    f\"The line number {line_number} is invalid. \",\n                ),\n            ],\n        )\n\n    file_path = os.path.expanduser(file_path)\n    if not os.path.exists(file_path):\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=f\"InvalidArgumentsError: The target file \"\n                    f\"{file_path} does not exist. \",\n                ),\n            ],\n        )\n\n    with open(file_path, \"r\", encoding=\"utf-8\") as file:\n        original_lines = file.readlines()\n\n    if line_number == len(original_lines) + 1:\n        new_lines = original_lines + [\"\\n\" + content]\n    elif line_number < len(original_lines) + 1:\n        new_lines = (\n            original_lines[: line_number - 1]\n            + [content + \"\\n\"]\n            + original_lines[line_number - 1 :]\n        )\n    else:\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=\"InvalidArgumentsError: The given line_number \"\n                    f\"({line_number}) is not in the valid range \"\n                    f\"[1, {len(original_lines) + 1}].\",\n                ),\n            ],\n        )\n\n    with open(file_path, \"w\", encoding=\"utf-8\") as file:\n        file.writelines(new_lines)\n\n    with open(file_path, \"r\", encoding=\"utf-8\") as file:\n        new_lines = file.readlines()\n\n    start, end = _calculate_view_ranges(\n        len(original_lines),\n        len(new_lines),\n        line_number,\n        line_number,\n        extra_view_n_lines=5,\n    )\n\n    show_content = _view_text_file(file_path, [start, end])\n\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=f\"Insert content into {file_path} at line \"\n                f\"{line_number} successfully. The new content \"\n                f\"between lines {start}-{end} is:\\n\"\n                f\"```\\n{show_content}```\",\n            ),\n        ],\n    )\n\n\nasync def write_text_file(\n    file_path: str,\n    content: str,\n    ranges: None | list[int] = None,\n) -> ToolResponse:\n    \"\"\"Create/Replace/Overwrite content in a text file. When `ranges` is provided, the content will be replaced in the specified range. Otherwise, the entire file (if exists) will be overwritten.\n\n    Args:\n        file_path (`str`):\n            The target file path.\n        content (`str`):\n            The content to be written.\n        ranges (`list[int] | None`, defaults to `None`):\n            The range of lines to be replaced. If `None`, the entire file will\n            be overwritten.\n\n    Returns:\n        `ToolResponse`:\n            The tool response containing the result of the writing operation.\n    \"\"\"\n    file_path = os.path.expanduser(file_path)\n    if not os.path.exists(file_path):\n        with open(file_path, \"w\", encoding=\"utf-8\") as file:\n            file.write(content)\n\n        if ranges:\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Create and write {file_path} successfully. \"\n                        f\"The ranges {ranges} is ignored because the \"\n                        f\"file does not exist.\",\n                    ),\n                ],\n            )\n\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=f\"Create and write {file_path} successfully.\",\n                ),\n            ],\n        )\n\n    with open(file_path, \"r\", encoding=\"utf-8\") as file:\n        original_lines = file.readlines()\n\n    if ranges is not None:\n        if (\n            isinstance(ranges, list)\n            and len(ranges) == 2\n            and all(isinstance(i, int) for i in ranges)\n        ):\n            # Replace content in the specified range\n            start, end = ranges\n            if start > len(original_lines):\n                return ToolResponse(\n                    content=[\n                        TextBlock(\n                            type=\"text\",\n                            text=f\"Error: The start line {start} is invalid. \"\n                            f\"The file only has {len(original_lines)} \"\n                            f\"lines.\",\n                        ),\n                    ],\n                )\n\n            new_content = (\n                original_lines[: start - 1]\n                + [\n                    content,\n                ]\n                + original_lines[end:]\n            )\n\n            with open(file_path, \"w\", encoding=\"utf-8\") as file:\n                file.write(\"\".join(new_content))\n\n            # The written content may contain multiple \"\\n\", to avoid mis\n            # counting the lines, we read the file again to get the new content\n            with open(file_path, \"r\", encoding=\"utf-8\") as file:\n                new_lines = file.readlines()\n\n            view_start, view_end = _calculate_view_ranges(\n                len(original_lines),\n                len(new_lines),\n                start,\n                end,\n            )\n\n            content = \"\".join(\n                [\n                    f\"{index + view_start}: {line}\"\n                    for index, line in enumerate(\n                        new_lines[view_start - 1 : view_end],\n                    )\n                ],\n            )\n\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"\"\"Write {file_path} successfully. The new content snippet:\n```\n{content}```\"\"\",\n                    ),\n                ],\n            )\n\n        else:\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Error: Invalid range format. Expected a list \"\n                        f\"of two integers, but got {ranges}.\",\n                    ),\n                ],\n            )\n\n    with open(file_path, \"w\", encoding=\"utf-8\") as file:\n        file.write(content)\n\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=f\"Overwrite {file_path} successfully.\",\n            ),\n        ],\n    )\n"
  },
  {
    "path": "src/agentscope/tool/_toolkit.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The toolkit class for tool calls in agentscope.\n\nTODO: We should consider to split this `Toolkit` class in the future.\n\"\"\"\n# pylint: disable=too-many-lines\nimport asyncio\nimport inspect\nimport os\nfrom copy import deepcopy\nfrom functools import partial, wraps\nfrom typing import (\n    AsyncGenerator,\n    Literal,\n    Any,\n    Type,\n    Generator,\n    Callable,\n    Awaitable,\n    Coroutine,\n)\n\nimport mcp\nimport shortuuid\nfrom pydantic import (\n    BaseModel,\n    Field,\n    create_model,\n)\n\nfrom ._async_wrapper import (\n    _async_generator_wrapper,\n    _object_wrapper,\n    _sync_generator_wrapper,\n)\nfrom ._response import ToolResponse\nfrom ._types import ToolGroup, AgentSkill, RegisteredToolFunction\nfrom .._utils._common import _parse_tool_function\nfrom ..mcp import (\n    MCPToolFunction,\n    MCPClientBase,\n    StatefulClientBase,\n)\nfrom ..message import (\n    ToolUseBlock,\n    TextBlock,\n)\nfrom ..module import StateModule\nfrom ..types import (\n    JSONSerializableObject,\n    ToolFunction,\n)\nfrom ..tracing._trace import trace_toolkit\nfrom .._logging import logger\n\n\ndef _apply_middlewares(\n    func: Callable[\n        ...,\n        Coroutine[Any, Any, AsyncGenerator[ToolResponse, None]],\n    ],\n) -> Callable[..., AsyncGenerator[ToolResponse, None]]:\n    \"\"\"Decorator that applies registered middlewares at runtime.\n\n    This decorator reads the middleware list from the instance and constructs\n    the middleware chain dynamically during each invocation.\n\n    .. note:: Middlewares must be async generator functions that yield\n     `ToolResponse` objects.\n    \"\"\"\n\n    @wraps(func)\n    async def wrapper(\n        self: \"Toolkit\",\n        tool_call: ToolUseBlock,\n    ) -> AsyncGenerator[ToolResponse, None]:\n        \"\"\"Wrapper that applies middleware chain.\"\"\"\n        middlewares = getattr(self, \"_middlewares\", [])\n\n        if not middlewares:\n            # No middlewares, call the original function directly\n            async for chunk in await func(self, tool_call):\n                yield chunk\n            return\n\n        # Build the middleware chain from innermost to outermost\n        async def base_handler(\n            **kwargs: Any,\n        ) -> AsyncGenerator[ToolResponse, None]:\n            \"\"\"Base handler that calls the original function.\"\"\"\n            return await func(self, **kwargs)\n\n        # Wrap with each middleware in reverse order\n        current_handler = base_handler\n        for middleware in reversed(middlewares):\n\n            def make_handler(mw: Callable, handler: Callable) -> Callable:\n                \"\"\"Create wrapped handler for middleware.\"\"\"\n\n                async def wrapped(\n                    **kwargs: Any,\n                ) -> AsyncGenerator[ToolResponse, None]:\n                    \"\"\"Handler that applies middleware.\"\"\"\n                    return mw(kwargs, handler)\n\n                return wrapped\n\n            current_handler = make_handler(middleware, current_handler)\n\n        # Execute the middleware chain\n        async for chunk in await current_handler(tool_call=tool_call):\n            yield chunk\n\n    return wrapper\n\n\nclass Toolkit(StateModule):\n    \"\"\"Toolkit is the core module to register, manage and delete tool\n    functions, MCP clients, Agent skills in AgentScope.\n\n    About tool functions:\n\n    - Register and parse JSON schemas from their docstrings automatically.\n    - Group-wise tools management, and agentic tools activation/deactivation.\n    - Extend the tool function JSON schema dynamically with Pydantic BaseModel.\n    - Tool function execution with unified streaming interface.\n\n    About MCP clients:\n\n    - Register tool functions from MCP clients directly.\n    - Client-level tool functions removal.\n\n    About Agent skills:\n\n    - Register agent skills from the given directory.\n    - Provide prompt for the registered skills to the agent.\n    \"\"\"\n\n    _DEFAULT_AGENT_SKILL_INSTRUCTION = (\n        \"# Agent Skills\\n\"\n        \"The agent skills are a collection of folds of instructions, scripts, \"\n        \"and resources that you can load dynamically to improve performance \"\n        \"on specialized tasks. Each agent skill has a `SKILL.md` file in its \"\n        \"folder that describes how to use the skill. If you want to use a \"\n        \"skill, you MUST read its `SKILL.md` file carefully.\"\n    )\n\n    _DEFAULT_AGENT_SKILL_TEMPLATE = \"\"\"## {name}\n{description}\nCheck \"{dir}/SKILL.md\" for how to use this skill\"\"\"\n\n    def __init__(\n        self,\n        agent_skill_instruction: str | None = None,\n        agent_skill_template: str | None = None,\n    ) -> None:\n        \"\"\"Initialize the toolkit.\n\n        Args:\n            agent_skill_instruction (`str | None`, optional):\n                The instruction for agent skills in the system prompt. If not\n                provided, a default instruction will be used.\n            agent_skill_template (`str | None`, optional):\n                The template to present one agent skill in the system prompt,\n                which should contain `{name}`, `{description}`, and `{dir}`\n                placeholders. If not provided, a default template will be used.\n        \"\"\"\n        super().__init__()\n\n        self.tools: dict[str, RegisteredToolFunction] = {}\n        self.groups: dict[str, ToolGroup] = {}\n        self.skills: dict[str, AgentSkill] = {}\n        self._middlewares: list = []  # Store registered middlewares\n\n        self._agent_skill_instruction = (\n            agent_skill_instruction or self._DEFAULT_AGENT_SKILL_INSTRUCTION\n        )\n        self._agent_skill_template = (\n            agent_skill_template or self._DEFAULT_AGENT_SKILL_TEMPLATE\n        )\n\n    def create_tool_group(\n        self,\n        group_name: str,\n        description: str,\n        active: bool = False,\n        notes: str | None = None,\n    ) -> None:\n        \"\"\"Create a tool group to organize tool functions\n\n        Args:\n            group_name (`str`):\n                The name of the tool group.\n            description (`str`):\n                The description of the tool group.\n            active (`bool`, defaults to `False`):\n                If the group is active, meaning the tool functions in this\n                group are included in the JSON schema.\n            notes (`str | None`, optional):\n                The notes used to remind the agent how to use the tool\n                functions properly, which can be combined into the system\n                prompt.\n        \"\"\"\n        if group_name in self.groups or group_name == \"basic\":\n            raise ValueError(\n                f\"Tool group '{group_name}' is already registered in the \"\n                \"toolkit.\",\n            )\n\n        self.groups[group_name] = ToolGroup(\n            name=group_name,\n            description=description,\n            notes=notes,\n            active=active,\n        )\n\n    def update_tool_groups(self, group_names: list[str], active: bool) -> None:\n        \"\"\"Update the activation status of the given tool groups.\n\n        Args:\n            group_names (`list[str]`):\n                The list of tool group names to be updated.\n            active (`bool`):\n                If the tool groups should be activated or deactivated.\n        \"\"\"\n\n        for group_name in group_names:\n            if group_name == \"basic\":\n                logger.warning(\n                    \"The 'basic' tool group is always active, skipping it.\",\n                )\n\n            if group_name in self.groups:\n                self.groups[group_name].active = active\n\n    def remove_tool_groups(self, group_names: str | list[str]) -> None:\n        \"\"\"Remove tool functions from the toolkit by their group names.\n\n        Args:\n            group_names (`str | list[str]`):\n                The group names to be removed from the toolkit.\n        \"\"\"\n        if isinstance(group_names, str):\n            group_names = [group_names]\n\n        if not isinstance(group_names, list) or not all(\n            isinstance(_, str) for _ in group_names\n        ):\n            raise TypeError(\n                f\"The group_names must be a list of strings, \"\n                f\"but got {type(group_names)}.\",\n            )\n\n        if \"basic\" in group_names:\n            raise ValueError(\n                \"Cannot remove the default 'basic' tool group.\",\n            )\n\n        for group_name in group_names:\n            self.groups.pop(group_name, None)\n\n        # Remove the tool functions in the given groups\n        tool_names = deepcopy(list(self.tools.keys()))\n        for tool_name in tool_names:\n            if self.tools[tool_name].group in group_names:\n                self.tools.pop(tool_name)\n\n    # pylint: disable=too-many-branches, too-many-statements\n    def register_tool_function(\n        self,\n        tool_func: ToolFunction,\n        group_name: str | Literal[\"basic\"] = \"basic\",\n        preset_kwargs: dict[str, JSONSerializableObject] | None = None,\n        func_name: str | None = None,\n        func_description: str | None = None,\n        json_schema: dict | None = None,\n        include_long_description: bool = True,\n        include_var_positional: bool = False,\n        include_var_keyword: bool = False,\n        postprocess_func: (\n            Callable[\n                [ToolUseBlock, ToolResponse],\n                ToolResponse | None,\n            ]\n            | Callable[\n                [ToolUseBlock, ToolResponse],\n                Awaitable[ToolResponse | None],\n            ]\n        )\n        | None = None,\n        namesake_strategy: Literal[\n            \"override\",\n            \"skip\",\n            \"raise\",\n            \"rename\",\n        ] = \"raise\",\n    ) -> None:\n        \"\"\"Register a tool function to the toolkit.\n\n        Args:\n            tool_func (`ToolFunction`):\n                The tool function, which can be async or sync, streaming or\n                not-streaming, but the response must be a `ToolResponse`\n                object.\n            group_name (`str | Literal[\"basic\"]`, defaults to `\"basic\"`):\n                The belonging group of the tool function. Tools in \"basic\"\n                group is always included in the JSON schema, while the others\n                are only included when their group is active.\n            preset_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n            optional):\n                Preset arguments by the user, which will not be included in\n                the JSON schema, nor exposed to the agent.\n            func_name (`str | None`, optional):\n                The custom function name, which should be consistent with the\n                name in function_description and json_schema (if provided).\n                By default, the function name will be extracted from the\n                function automatically.\n            func_description (`str | None`, optional):\n                The function description. If not provided, the description\n                will be extracted from the docstring automatically.\n            json_schema (`dict | None`, optional):\n                Manually provided JSON schema for the tool function, which\n                should be `{\"type\": \"function\", \"function\": {\"name\":\n                \"function_name\": \"xx\", \"description\": \"xx\",\n                \"parameters\": {...}}}`\n            include_long_description (`bool`, defaults to `True`):\n                When extracting function description from the docstring, if\n                the long description will be included.\n            include_var_positional (`bool`, defaults to `False`):\n                Whether to include the variable positional arguments (`*args`)\n                in the function schema.\n            include_var_keyword (`bool`, defaults to `False`):\n                Whether to include the variable keyword arguments (`**kwargs`)\n                in the function schema.\n            postprocess_func (`(Callable[[ToolUseBlock, ToolResponse], \\\n            ToolResponse | None] | Callable[[ToolUseBlock, ToolResponse], \\\n            Awaitable[ToolResponse | None]]) | None`, optional):\n                A post-processing function that will be called after the tool\n                function is executed, taking the tool call block and tool\n                response as arguments. The function can be either sync or\n                async. If it returns `None`, the tool result will be\n                returned as is. If it returns a `ToolResponse`,\n                the returned block will be used as the final tool result.\n            namesake_strategy (`Literal['raise', 'override', 'skip', \\\n            'rename']`, defaults to `'raise'`):\n                The strategy to handle the tool function name conflict:\n                - 'raise': raise a ValueError (default behavior).\n                - 'override': override the existing tool function with the new\n                  one.\n                - 'skip': skip the registration of the new tool function.\n                - 'rename': rename the new tool function by appending a random\n                  suffix to make it unique.\n        \"\"\"\n        # Arguments checking\n        if group_name not in self.groups and group_name != \"basic\":\n            raise ValueError(\n                f\"Tool group '{group_name}' not found.\",\n            )\n\n        # Check the manually provided JSON schema if provided\n        if json_schema:\n            assert (\n                isinstance(json_schema, dict)\n                and \"type\" in json_schema\n                and json_schema[\"type\"] == \"function\"\n                and \"function\" in json_schema\n                and isinstance(json_schema[\"function\"], dict)\n            ), \"Invalid JSON schema for the tool function.\"\n\n        # Handle MCP tool function and regular function respectively\n        mcp_name = None\n        if isinstance(tool_func, MCPToolFunction):\n            input_func_name = tool_func.name\n            original_func = tool_func.__call__\n            json_schema = json_schema or tool_func.json_schema\n            mcp_name = tool_func.mcp_name\n\n        elif isinstance(tool_func, partial):\n            # partial function\n            kwargs = tool_func.keywords\n            # Turn args into keyword arguments\n            if tool_func.args:\n                param_names = list(\n                    inspect.signature(tool_func.func).parameters.keys(),\n                )\n                for i, arg in enumerate(tool_func.args):\n                    if i < len(param_names):\n                        kwargs[param_names[i]] = arg\n\n            preset_kwargs = {\n                **kwargs,\n                **(preset_kwargs or {}),\n            }\n\n            input_func_name = tool_func.func.__name__\n            original_func = tool_func.func\n            json_schema = json_schema or _parse_tool_function(\n                tool_func.func,\n                include_long_description=include_long_description,\n                include_var_positional=include_var_positional,\n                include_var_keyword=include_var_keyword,\n            )\n\n        else:\n            # normal function\n            input_func_name = tool_func.__name__\n            original_func = tool_func\n            json_schema = json_schema or _parse_tool_function(\n                tool_func,\n                include_long_description=include_long_description,\n                include_var_positional=include_var_positional,\n                include_var_keyword=include_var_keyword,\n            )\n\n        # Record the original function name if the func_name is given\n        original_name = input_func_name if func_name else None\n\n        # Use the given function name if provided\n        func_name = func_name or input_func_name\n\n        # Always set the function name in json_schema\n        json_schema[\"function\"][\"name\"] = func_name\n\n        # Override the description if provided\n        if func_description:\n            json_schema[\"function\"][\"description\"] = func_description\n\n        # Remove the preset kwargs from the JSON schema\n        for arg_name in preset_kwargs or {}:\n            if arg_name in json_schema[\"function\"][\"parameters\"][\"properties\"]:\n                json_schema[\"function\"][\"parameters\"][\"properties\"].pop(\n                    arg_name,\n                )\n\n        if \"required\" in json_schema[\"function\"][\"parameters\"]:\n            for arg_name in preset_kwargs or {}:\n                if (\n                    arg_name\n                    in json_schema[\"function\"][\"parameters\"][\"required\"]\n                ):\n                    json_schema[\"function\"][\"parameters\"][\"required\"].remove(\n                        arg_name,\n                    )\n\n            # Remove the required field if it is empty\n            if len(json_schema[\"function\"][\"parameters\"][\"required\"]) == 0:\n                json_schema[\"function\"][\"parameters\"].pop(\"required\", None)\n\n        func_obj = RegisteredToolFunction(\n            name=func_name,\n            group=group_name,\n            source=\"function\",\n            original_func=original_func,\n            json_schema=json_schema,\n            preset_kwargs=preset_kwargs or {},\n            original_name=original_name,\n            extended_model=None,\n            mcp_name=mcp_name,\n            postprocess_func=postprocess_func,\n        )\n\n        if func_name in self.tools:\n            if namesake_strategy == \"raise\":\n                raise ValueError(\n                    f\"A function with name '{func_name}' is already \"\n                    f\"registered in the toolkit.\",\n                )\n\n            if namesake_strategy == \"skip\":\n                logger.warning(\n                    \"A function with name '%s' is already \"\n                    \"registered in the toolkit. Skipping registration.\",\n                    func_name,\n                )\n\n            elif namesake_strategy == \"override\":\n                logger.warning(\n                    \"A function with name '%s' is already registered \"\n                    \"in the toolkit. Overriding with the new function.\",\n                    func_name,\n                )\n                self.tools[func_name] = func_obj\n\n            elif namesake_strategy == \"rename\":\n                new_func_name = func_name\n                for _ in range(100):\n                    suffix = shortuuid.uuid()[:5]\n                    new_func_name = f\"{func_name}_{suffix}\"\n                    if new_func_name not in self.tools:\n                        break\n\n                # Raise error if failed to find a unique name\n                if new_func_name in self.tools:\n                    raise RuntimeError(\n                        f\"Failed to register tool function '{func_name}' with \"\n                        \"a unique name after 100 attempts.\",\n                    )\n                logger.warning(\n                    \"A function with name '%s' is already \"\n                    \"registered in the toolkit. Renaming the new function to \"\n                    \"'%s'.\",\n                    func_name,\n                    new_func_name,\n                )\n\n                # Replace the function name with the new one\n                func_obj.original_name = original_name or func_name\n                func_obj.name = new_func_name\n                func_obj.json_schema[\"function\"][\"name\"] = new_func_name\n\n                self.tools[new_func_name] = func_obj\n\n            else:\n                raise ValueError(\n                    f\"Invalid namesake_strategy: {namesake_strategy}. \"\n                    \"Supported strategies are 'raise', 'override', 'skip', \"\n                    \"and 'rename'.\",\n                )\n\n        else:\n            self.tools[func_name] = func_obj\n\n    def remove_tool_function(\n        self,\n        tool_name: str,\n        allow_not_exist: bool = True,\n    ) -> None:\n        \"\"\"Remove tool function from the toolkit by its name.\n\n        Args:\n            tool_name (`str`):\n                The name of the tool function to be removed.\n            allow_not_exist (`bool`):\n                Allow the tool function to not exist when removing.\n        \"\"\"\n\n        if tool_name not in self.tools and not allow_not_exist:\n            raise ValueError(\n                f\"Tool function '{tool_name}' does not exist in the \"\n                \"toolkit.\",\n            )\n\n        self.tools.pop(tool_name, None)\n\n    def get_json_schemas(\n        self,\n    ) -> list[dict]:\n        \"\"\"Get the JSON schemas from the tool functions that belong to the\n        active groups.\n\n        .. note:: The preset keyword arguments is removed from the JSON\n         schema, and the extended model is applied if it is set.\n\n        Example:\n            .. code-block:: JSON\n                :caption: Example of tool function JSON schemas\n\n                [\n                    {\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"google_search\",\n                            \"description\": \"Search on Google.\",\n                            \"parameters\": {\n                                \"type\": \"object\",\n                                \"properties\": {\n                                    \"query\": {\n                                        \"type\": \"string\",\n                                        \"description\": \"The search query.\"\n                                    }\n                                },\n                                \"required\": [\"query\"]\n                            }\n                        }\n                    },\n                    ...\n                ]\n\n        Returns:\n            `list[dict]`:\n                A list of function JSON schemas.\n        \"\"\"\n        # If meta tool is set here, update its extended model here\n        if \"reset_equipped_tools\" in self.tools:\n            fields = {}\n            for group_name, group in self.groups.items():\n                if group_name == \"basic\":\n                    continue\n                fields[group_name] = (\n                    bool,\n                    Field(\n                        default=False,\n                        description=group.description,\n                    ),\n                )\n            extended_model = create_model(\"_DynamicModel\", **fields)\n            self.set_extended_model(\n                \"reset_equipped_tools\",\n                extended_model,\n            )\n\n        return [\n            tool.extended_json_schema\n            for tool in self.tools.values()\n            if tool.group == \"basic\" or self.groups[tool.group].active\n        ]\n\n    def set_extended_model(\n        self,\n        func_name: str,\n        model: Type[BaseModel] | None,\n    ) -> None:\n        \"\"\"Set the extended model for a tool function, so that the original\n        JSON schema will be extended.\n\n        Args:\n            func_name (`str`):\n                The name of the tool function.\n            model (`Union[Type[BaseModel], None]`):\n                The extended model to be set.\n        \"\"\"\n        if model is not None and not issubclass(model, BaseModel):\n            raise TypeError(\n                \"The extended model must be a child class of pydantic \"\n                f\"BaseModel, but got {type(model)}.\",\n            )\n\n        if func_name in self.tools:\n            self.tools[func_name].extended_model = model\n\n        else:\n            raise ValueError(\n                f\"Tool function '{func_name}' not found in the toolkit.\",\n            )\n\n    async def remove_mcp_clients(\n        self,\n        client_names: list[str],\n    ) -> None:\n        \"\"\"Remove tool functions from the MCP clients by their names.\n\n        Args:\n            client_names (`list[str]`):\n                The names of the MCP client, which used to initialize the\n                client instance.\n        \"\"\"\n        if isinstance(client_names, str):\n            client_names = [client_names]\n\n        if isinstance(client_names, list) and not all(\n            isinstance(_, str) for _ in client_names\n        ):\n            raise TypeError(\n                f\"The client_names must be a list of strings, \"\n                f\"but got {type(client_names)}.\",\n            )\n\n        to_removed = []\n        func_names = deepcopy(list(self.tools.keys()))\n        for func_name in func_names:\n            if self.tools[func_name].mcp_name in client_names:\n                self.tools.pop(func_name)\n                to_removed.append(func_name)\n\n        logger.info(\n            \"Removed %d tool functions from %d MCP: %s\",\n            len(to_removed),\n            len(client_names),\n            \", \".join(to_removed),\n        )\n\n    @trace_toolkit\n    @_apply_middlewares\n    async def call_tool_function(\n        self,\n        tool_call: ToolUseBlock,\n    ) -> AsyncGenerator[ToolResponse, None]:\n        \"\"\"Execute the tool function by the `ToolUseBlock` and return the\n        tool response chunk in unified streaming mode, i.e. an async\n        generator of `ToolResponse` objects.\n\n        .. note:: The tool response chunk is **accumulated**.\n\n        Args:\n            tool_call (`ToolUseBlock`):\n                A tool call block.\n\n        Yields:\n            `ToolResponse`:\n                The tool response chunk, in accumulative manner.\n        \"\"\"\n\n        # Check\n        if tool_call[\"name\"] not in self.tools:\n            return _object_wrapper(\n                ToolResponse(\n                    content=[\n                        TextBlock(\n                            type=\"text\",\n                            text=\"FunctionNotFoundError: Cannot find the \"\n                            f\"function named {tool_call['name']}\",\n                        ),\n                    ],\n                ),\n                None,\n            )\n\n        # Obtain the tool function\n        tool_func = self.tools[tool_call[\"name\"]]\n\n        # Check if the tool function is in an inactive group\n        if (\n            tool_func.group != \"basic\"\n            and not self.groups[tool_func.group].active\n        ):\n            return _object_wrapper(\n                ToolResponse(\n                    content=[\n                        TextBlock(\n                            type=\"text\",\n                            text=\"FunctionInactiveError: The function \"\n                            f\"'{tool_call['name']}' is in the inactive \"\n                            f\"group '{tool_func.group}'. Activate the tool \"\n                            \"group by calling 'reset_equipped_tools' \"\n                            \"first to use this tool.\",\n                        ),\n                    ],\n                ),\n                None,\n            )\n\n        # Prepare keyword arguments\n        kwargs = {\n            **tool_func.preset_kwargs,\n            **(tool_call.get(\"input\", {}) or {}),\n        }\n\n        # Prepare postprocess function\n        if tool_func.postprocess_func:\n            # Type: partial wraps the postprocess_func with tool_call bound,\n            # reducing it from (ToolUseBlock, ToolResponse) to (ToolResponse)\n            partial_postprocess_func: (\n                Callable[[ToolResponse], ToolResponse | None]\n                | Callable[[ToolResponse], Awaitable[ToolResponse | None]]\n            ) | None = partial(\n                tool_func.postprocess_func,\n                tool_call,\n            )\n        else:\n            partial_postprocess_func = None\n\n        # Async function\n        try:\n            if inspect.iscoroutinefunction(tool_func.original_func):\n                try:\n                    res = await tool_func.original_func(**kwargs)\n                except asyncio.CancelledError:\n                    res = ToolResponse(\n                        content=[\n                            TextBlock(\n                                type=\"text\",\n                                text=\"<system-info>\"\n                                \"The tool call has been interrupted \"\n                                \"by the user.\"\n                                \"</system-info>\",\n                            ),\n                        ],\n                        stream=True,\n                        is_last=True,\n                        is_interrupted=True,\n                    )\n\n            else:\n                # When `tool_func.original_func` is Async generator function or\n                # Sync function\n                res = tool_func.original_func(**kwargs)\n\n        except mcp.shared.exceptions.McpError as e:\n            res = ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Error occurred when calling MCP tool: {e}\",\n                    ),\n                ],\n            )\n\n        except Exception as e:\n            res = ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Error: {e}\",\n                    ),\n                ],\n            )\n\n        # Handle different return type\n\n        # If return an async generator\n        if isinstance(res, AsyncGenerator):\n            return _async_generator_wrapper(res, partial_postprocess_func)\n\n        # If return a sync generator\n        if isinstance(res, Generator):\n            return _sync_generator_wrapper(res, partial_postprocess_func)\n\n        if isinstance(res, ToolResponse):\n            return _object_wrapper(res, partial_postprocess_func)\n\n        raise TypeError(\n            \"The tool function must return a ToolResponse object, or an \"\n            \"AsyncGenerator/Generator of ToolResponse objects, \"\n            f\"but got {type(res)}.\",\n        )\n\n    async def register_mcp_client(\n        self,\n        mcp_client: MCPClientBase,\n        group_name: str = \"basic\",\n        enable_funcs: list[str] | None = None,\n        disable_funcs: list[str] | None = None,\n        preset_kwargs_mapping: dict[str, dict[str, Any]] | None = None,\n        postprocess_func: (\n            Callable[\n                [ToolUseBlock, ToolResponse],\n                ToolResponse | None,\n            ]\n            | Callable[\n                [ToolUseBlock, ToolResponse],\n                Awaitable[ToolResponse | None],\n            ]\n        )\n        | None = None,\n        namesake_strategy: Literal[\n            \"override\",\n            \"skip\",\n            \"raise\",\n            \"rename\",\n        ] = \"raise\",\n    ) -> None:\n        \"\"\"Register tool functions from an MCP client.\n\n        Args:\n            mcp_client (`MCPClientBase`):\n                The MCP client instance to connect to the MCP server.\n            group_name (`str`, defaults to `\"basic\"`):\n                The group name that the tool functions will be added to.\n            enable_funcs (`list[str] | None`, optional):\n                The functions to be added into the toolkit. If `None`, all\n                tool functions within the MCP servers will be added.\n            disable_funcs (`list[str] | None`, optional):\n                The functions that will be filtered out. If `None`, no\n                tool functions will be filtered out.\n            preset_kwargs_mapping: (`Optional[dict[str, dict[str, Any]]]`, \\\n            defaults to `None`):\n                The preset keyword arguments mapping, whose keys are the tool\n                function names and values are the preset keyword arguments.\n            postprocess_func (`(Callable[[ToolUseBlock, ToolResponse], \\\n            ToolResponse | None] | Callable[[ToolUseBlock, ToolResponse], \\\n            Awaitable[ToolResponse | None]]) | None`, optional):\n                A post-processing function that will be called after the tool\n                function is executed, taking the tool call block and tool\n                response as arguments. The function can be either sync or\n                async. If it returns `None`, the tool result will be\n                returned as is. If it returns a `ToolResponse`,\n                the returned block will be used as the final tool result.\n            namesake_strategy (`Literal['raise', 'override', 'skip', \\\n            'rename']`, defaults to `'raise'`):\n                The strategy to handle the tool function name conflict:\n                - 'raise': raise a ValueError (default behavior).\n                - 'override': override the existing tool function with the new\n                  one.\n                - 'skip': skip the registration of the new tool function.\n                - 'rename': rename the new tool function by appending a random\n                  suffix to make it unique.\n        \"\"\"\n        if (\n            isinstance(mcp_client, StatefulClientBase)\n            and not mcp_client.is_connected\n        ):\n            raise RuntimeError(\n                \"The MCP client is not connected to the server. Use the \"\n                \"`connect()` method first.\",\n            )\n\n        # Check arguments for enable_funcs and disabled_funcs\n        if enable_funcs is not None and disable_funcs is not None:\n            assert isinstance(enable_funcs, list) and all(\n                isinstance(_, str) for _ in enable_funcs\n            ), (\n                \"Enable functions should be a list of strings, but got \"\n                f\"{enable_funcs}.\"\n            )\n\n            assert isinstance(disable_funcs, list) and all(\n                isinstance(_, str) for _ in disable_funcs\n            ), (\n                \"Disable functions should be a list of strings, but got \"\n                f\"{disable_funcs}.\"\n            )\n            intersection = set(enable_funcs).intersection(\n                set(disable_funcs),\n            )\n            assert len(intersection) == 0, (\n                f\"The functions in enable_funcs and disable_funcs \"\n                f\"should not overlap, but got {intersection}.\"\n            )\n\n        if not (\n            preset_kwargs_mapping is None\n            or isinstance(preset_kwargs_mapping, dict)\n        ):\n            raise TypeError(\n                f\"The preset_kwargs_mapping must be a dictionary or None, \"\n                f\"but got {type(preset_kwargs_mapping)}.\",\n            )\n\n        tool_names = []\n        for mcp_tool in await mcp_client.list_tools():\n            # Skip the functions that are not in the enable_funcs if\n            # enable_funcs is not None\n            if enable_funcs is not None and mcp_tool.name not in enable_funcs:\n                continue\n\n            # Skip the disabled functions\n            if disable_funcs is not None and mcp_tool.name in disable_funcs:\n                continue\n\n            tool_names.append(mcp_tool.name)\n\n            # Obtain callable function object\n            func_obj = await mcp_client.get_callable_function(\n                func_name=mcp_tool.name,\n                wrap_tool_result=True,\n            )\n\n            # Prepare preset kwargs\n            preset_kwargs = None\n            if preset_kwargs_mapping is not None:\n                preset_kwargs = preset_kwargs_mapping.get(mcp_tool.name, {})\n\n            self.register_tool_function(\n                tool_func=func_obj,\n                group_name=group_name,\n                preset_kwargs=preset_kwargs,\n                postprocess_func=postprocess_func,\n                namesake_strategy=namesake_strategy,\n            )\n\n        logger.info(\n            \"Registered %d tool functions from MCP: %s.\",\n            len(tool_names),\n            \", \".join(tool_names),\n        )\n\n    def state_dict(self) -> dict[str, Any]:\n        \"\"\"Get the state dictionary of the toolkit.\n\n        Returns:\n            `dict[str, Any]`:\n                A dictionary containing the active tool group names.\n        \"\"\"\n        return {\n            \"active_groups\": [\n                name for name, group in self.groups.items() if group.active\n            ],\n        }\n\n    def load_state_dict(\n        self,\n        state_dict: dict[str, Any],\n        strict: bool = True,\n    ) -> None:\n        \"\"\"Load the state dictionary into the toolkit.\n\n        Args:\n            state_dict (`dict`):\n                The state dictionary to load, which should have \"active_groups\"\n                key and its value must be a list of group names.\n            strict (`bool`, defaults to `True`):\n                If `True`, raises an error if any key in the module is not\n                found in the state_dict. If `False`, skips missing keys.\n        \"\"\"\n        if (\n            not isinstance(state_dict, dict)\n            or \"active_groups\" not in state_dict\n            or not isinstance(state_dict[\"active_groups\"], list)\n        ):\n            raise ValueError(\n                \"The state_dict for toolkit must be a dictionary with \"\n                \"active_groups key and its value must be a list, \"\n                f\"but got {type(state_dict)}.\",\n            )\n\n        if strict and list(state_dict.keys()) != [\"active_groups\"]:\n            raise ValueError(\n                \"Get additional keys in the state_dict: \"\n                f'{list(state_dict.keys())}, but only \"active_groups\" '\n                \"is expected.\",\n            )\n\n        for group_name, group in self.groups.items():\n            if group_name in state_dict[\"active_groups\"]:\n                group.active = True\n            else:\n                group.active = False\n\n    def get_activated_notes(self) -> str:\n        \"\"\"Get the notes from the active tool groups, which can be used to\n        construct the system prompt for the agent.\n\n        Returns:\n            `str`:\n                The combined notes from the active tool groups.\n        \"\"\"\n        collected_notes = []\n        for group_name, group in self.groups.items():\n            if group.active and group.notes:\n                collected_notes.append(\n                    \"\\n\".join(\n                        [f\"## About Tool Group '{group_name}'\", group.notes],\n                    ),\n                )\n        return \"\\n\".join(collected_notes)\n\n    def reset_equipped_tools(self, **kwargs: Any) -> ToolResponse:\n        \"\"\"This function allows you to activate or deactivate tool groups\n        dynamically based on your current task requirements.\n        **Important: Each call sets the absolute final state of ALL tool\n        groups, not incremental changes**. Any group not explicitly set to True\n        will be deactivated, regardless of its previous state.\n\n        **Best practice**: Actively manage your tool groups——activate only\n        what you need for the current task, and promptly deactivate groups as\n        soon as they are no longer needed to conserve context space.\n\n        The function will return the usage instructions for the activated tool\n        groups, which you **MUST pay attention to and follow**. You can also\n        reuse this function to check the notes of the tool groups.\"\"\"\n\n        # Deactivate all tool groups first\n        self.update_tool_groups(list(self.groups.keys()), active=False)\n\n        to_activate = []\n        for key, value in kwargs.items():\n            if not isinstance(value, bool):\n                return ToolResponse(\n                    content=[\n                        TextBlock(\n                            type=\"text\",\n                            text=f\"Invalid arguments: the argument {key} \"\n                            f\"should be a bool value, but got {type(value)}.\",\n                        ),\n                    ],\n                )\n\n            if value:\n                to_activate.append(key)\n\n        self.update_tool_groups(to_activate, active=True)\n\n        notes = self.get_activated_notes()\n\n        text_response = \"\"\n        if to_activate:\n            text_response += (\n                \"Now tool groups \"\n                + \", \".join([f\"'{_}'\" for _ in to_activate])\n                + \" are activated.\"\n            )\n\n        if notes:\n            text_response += (\n                f\" You MUST follow these notes to use these tools:\\n\"\n                f\"<notes>{notes}</notes>\"\n            )\n\n        if not text_response:\n            text_response = \"All tool groups are now deactivated currently.\"\n\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=text_response,\n                ),\n            ],\n        )\n\n    def clear(self) -> None:\n        \"\"\"Clear the toolkit, removing all tool functions and groups.\"\"\"\n        self.tools.clear()\n        self.groups.clear()\n\n    def _validate_tool_function(self, func_name: str) -> None:\n        \"\"\"Check if the tool function already registered in the toolkit. If\n        so, raise a ValueError.\"\"\"\n        if func_name in self.tools:\n            raise ValueError(\n                f\"A function with name '{func_name}' is already registered \"\n                \"in the toolkit.\",\n            )\n\n    def register_agent_skill(\n        self,\n        skill_dir: str,\n    ) -> None:\n        \"\"\"Register agent skills from a given directory. This function will\n        scan the directory, read metadata from the SKILL.md file, and add\n        it to the skill related prompt. Developers can obtain the\n        skills-related prompt by calling `toolkit.get_agent_skill_prompt()`.\n\n        .. note:: This directory\n         - Must include a SKILL.md file at the top level\n         - The SKILL.md must have a YAML Front Matter including `name` and\n            `description` fields\n         - All files must specify a common root directory in their paths\n\n        Args:\n            skill_dir (`str`):\n                The path to the skill directory.\n        \"\"\"\n        import frontmatter\n\n        # Check the skill directory\n        if not os.path.isdir(skill_dir):\n            raise ValueError(\n                f\"The skill directory '{skill_dir}' does not exist or is \"\n                \"not a directory.\",\n            )\n\n        # Check SKILL.md file\n        path_skill_md = os.path.join(skill_dir, \"SKILL.md\")\n        if not os.path.isfile(path_skill_md):\n            raise ValueError(\n                f\"The skill directory '{skill_dir}' must include a \"\n                \"SKILL.md file at the top level.\",\n            )\n\n        # Check YAML Front Matter\n        with open(path_skill_md, \"r\", encoding=\"utf-8\") as f:\n            post = frontmatter.load(f)\n\n        name = post.get(\"name\", None)\n        description = post.get(\"description\", None)\n\n        if not name or not description:\n            raise ValueError(\n                f\"The SKILL.md file in '{skill_dir}' must have a YAML Front \"\n                \"Matter including `name` and `description` fields.\",\n            )\n\n        name, description = str(name), str(description)\n        if name in self.skills:\n            raise ValueError(\n                f\"An agent skill with name '{name}' is already registered \"\n                \"in the toolkit.\",\n            )\n\n        self.skills[name] = AgentSkill(\n            name=name,\n            description=description,\n            dir=skill_dir,\n        )\n\n        logger.info(\n            \"Registered agent skill '%s' from directory '%s'.\",\n            name,\n            skill_dir,\n        )\n\n    def remove_agent_skill(self, name: str) -> None:\n        \"\"\"Remove an agent skill by its name.\n\n        Args:\n            name (`str`):\n                The name of the agent skill to be removed.\n        \"\"\"\n        if name in self.skills:\n            self.skills.pop(name)\n        else:\n            logger.warning(\n                \"Agent skill '%s' not found in the toolkit, skipping removal.\",\n                name,\n            )\n\n    def get_agent_skill_prompt(self) -> str | None:\n        \"\"\"Get the prompt for all registered agent skills, which can be\n        attached to the system prompt for the agent.\n\n        The prompt is consisted of an overall instruction and the detailed\n        descriptions of each skill, including its name, description, and\n        directory.\n\n        .. note:: If no skill is registered, None will be returned.\n\n        Returns:\n            `str | None`:\n                The combined prompt for all registered agent skills, or None\n                if no skill is registered.\n        \"\"\"\n        if len(self.skills) == 0:\n            return None\n\n        skill_descriptions = [\n            self._agent_skill_instruction,\n        ] + [\n            self._agent_skill_template.format(\n                name=_[\"name\"],\n                description=_[\"description\"],\n                dir=_[\"dir\"],\n            )\n            for _ in self.skills.values()\n        ]\n        return \"\\n\".join(skill_descriptions)\n\n    def register_middleware(\n        self,\n        middleware: Callable[\n            ...,\n            Coroutine[Any, Any, AsyncGenerator[ToolResponse, None]]\n            | AsyncGenerator[ToolResponse, None],\n        ],\n    ) -> None:\n        \"\"\"Register an onion-style middleware for the `call_tool_function`,\n        which will wrap around the `call_tool_function` method, allowing\n        pre-processing, post-processing, or even skipping the execution of\n        the tool function.\n\n        The middleware follows an onion model, where each registered\n        middleware wraps around the previous one, forming layers. The\n        middleware can:\n\n        - Perform pre-processing before calling the tool function\n        - Intercept and modify each ToolResponse chunk\n        - Perform post-processing after the tool function completes\n        - Skip the tool function execution entirely\n\n        The middleware function should accept a ``kwargs`` dict as the first\n        parameter and ``next_handler`` as the second parameter. The ``kwargs``\n        dict currently contains:\n\n        - ``tool_call`` (`ToolUseBlock`): The tool call request\n\n        When calling ``next_handler``, pass ``**kwargs`` to unpack the dict.\n\n        Example:\n            .. code-block:: python\n\n                # Simple direct consumption style (recommended)\n                async def my_middleware(\n                    kwargs: dict,\n                    next_handler: Callable,\n                ) -> AsyncGenerator[ToolResponse, None]:\n                    # Access the tool call\n                    tool_call = kwargs[\"tool_call\"]\n\n                    # Pre-processing\n                    print(f\"Calling tool: {tool_call['name']}\")\n\n                    # Call next handler with **kwargs\n                    async for response in await next_handler(**kwargs):\n                        # Intercept and modify response if needed\n                        yield response\n\n                    # Post-processing after tool completes\n                    print(f\"Tool {tool_call['name']} completed\")\n\n                toolkit.register_middleware(my_middleware)\n\n            .. code-block:: python\n\n                # Alternative: Skip execution based on conditions\n                async def my_middleware(\n                    kwargs: dict,\n                    next_handler: Callable,\n                ) -> AsyncGenerator[ToolResponse, None]:\n                    tool_call = kwargs[\"tool_call\"]\n\n                    # Pre-processing\n                    if not is_authorized(tool_call):\n                        # Skip execution and return error directly\n                        yield ToolResponse(\n                            content=[\n                                TextBlock(\n                                    type=\"text\",\n                                    text=\"Unauthorized\",\n                                ),\n                            ],\n                        )\n                        return\n\n                    # Call next handler with **kwargs\n                    async for response in await next_handler(**kwargs):\n                        yield response\n\n                toolkit.register_middleware(my_middleware)\n\n        Args:\n            middleware (`Callable[..., Coroutine[Any, Any, \\\nAsyncGenerator[ToolResponse, None]] | AsyncGenerator[ToolResponse, None]]`):\n                The middleware function that accepts ``kwargs`` (dict) and\n                ``next_handler`` (Callable), and returns a coroutine that\n                yields AsyncGenerator of ToolResponse objects. The ``kwargs``\n                dict currently includes ``tool_call`` (ToolUseBlock), and may\n                include additional context in future versions.\n\n        .. note:: The middleware chain is applied inside the\n        `call_tool_function` via the `@apply_middlewares` decorator. This\n        ensures that the `@trace_toolkit` decorator remains at the outermost\n        layer for complete observability.\n        \"\"\"\n        # Simply append the middleware to the list\n        # The @apply_middlewares decorator will handle the execution\n        self._middlewares.append(middleware)\n"
  },
  {
    "path": "src/agentscope/tool/_types.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The types for the tool module in AgentScope.\"\"\"\nfrom copy import deepcopy\nfrom dataclasses import dataclass, field\nfrom typing import TypedDict, Literal, Type, Callable, Awaitable\n\nfrom pydantic import BaseModel\n\nfrom . import ToolResponse\nfrom .._utils._common import _remove_title_field\nfrom ..message import ToolUseBlock\nfrom ..types import ToolFunction, JSONSerializableObject\n\n\n@dataclass\nclass RegisteredToolFunction:\n    \"\"\"The registered tool function class.\"\"\"\n\n    name: str\n    \"\"\"The name of the tool function.\"\"\"\n    group: str | Literal[\"basic\"]\n    \"\"\"The belonging group of the tool function\"\"\"\n    source: Literal[\"function\", \"mcp_server\", \"function_group\"]\n    \"\"\"\"The type of the tool function, can be `function` or `mcp_server`.\"\"\"\n    original_func: ToolFunction\n    \"\"\"The original function\"\"\"\n    json_schema: dict\n    \"\"\"The JSON schema of the tool function, which is used to validate the \"\"\"\n    preset_kwargs: dict[str, JSONSerializableObject] = field(\n        default_factory=dict,\n    )\n    \"\"\"The preset keyword arguments, which won't be presented in the JSON\n    schema and exposed to the user.\"\"\"\n    original_name: str | None = None\n    \"\"\"The original name of the tool function when it has been renamed.\"\"\"\n    extended_model: Type[BaseModel] | None = None\n    \"\"\"The base model used to extend the JSON schema of the original tool\n    function, so that we can dynamically adjust the tool function.\"\"\"\n    mcp_name: str | None = None\n    \"\"\"The name of the MCP, if the tool function comes from an MCP server.\"\"\"\n    postprocess_func: (\n        Callable[\n            [ToolUseBlock, ToolResponse],\n            ToolResponse | None,\n        ]\n        | Callable[\n            [ToolUseBlock, ToolResponse],\n            Awaitable[ToolResponse | None],\n        ]\n    ) | None = None\n    \"\"\"The post-processing function that will be called after the tool\n    function is executed, taking the tool call block and tool\n    response as arguments. The function can be either sync or async. If it\n    returns `None`, the tool result will be returned as is. If it returns a\n    `ToolResponse`, the returned block will be used as the final tool\n    response.\"\"\"\n\n    @property\n    def extended_json_schema(self) -> dict:\n        \"\"\"Get the JSON schema of the tool function, if an extended model is\n        set, the merged JSON schema will be returned.\"\"\"\n        if self.extended_model is None:\n            return self.json_schema\n\n        # Merge the extended model with the original JSON schema\n        extended_schema = self.extended_model.model_json_schema()\n\n        merged_schema = deepcopy(self.json_schema)\n\n        _remove_title_field(  # pylint: disable=protected-access\n            extended_schema,\n        )\n\n        # Merge properties from extended schema\n        for key, value in extended_schema[\"properties\"].items():\n            if key in self.json_schema[\"function\"][\"parameters\"][\"properties\"]:\n                raise ValueError(\n                    f\"The field `{key}` already exists in the original \"\n                    f\"function schema of `{self.name}`. Try to use a \"\n                    \"different name.\",\n                )\n\n            merged_schema[\"function\"][\"parameters\"][\"properties\"][key] = value\n\n            if key in extended_schema.get(\"required\", []):\n                if \"required\" not in merged_schema[\"function\"][\"parameters\"]:\n                    merged_schema[\"function\"][\"parameters\"][\"required\"] = []\n                merged_schema[\"function\"][\"parameters\"][\"required\"].append(key)\n\n        # Merge $defs from extended schema to support nested models\n        if \"$defs\" in extended_schema:\n            merged_params = merged_schema[\"function\"][\"parameters\"]\n            if \"$defs\" not in merged_params:\n                merged_params[\"$defs\"] = {}\n\n            # Check for conflicts and merge $defs\n            for def_key, def_value in extended_schema[\"$defs\"].items():\n                def_value_copy = deepcopy(def_value)\n                _remove_title_field(\n                    def_value_copy,\n                )  # pylint: disable=protected-access\n\n                if def_key in merged_params[\"$defs\"]:\n                    # Check if the two definitions are from the same BaseModel\n                    # by comparing their content\n                    # Create copies and remove title fields for comparison\n\n                    existing_def_copy = deepcopy(\n                        merged_params[\"$defs\"][def_key],\n                    )\n                    _remove_title_field(\n                        existing_def_copy,\n                    )  # pylint: disable=protected-access\n\n                    if existing_def_copy != def_value_copy:\n                        # The definitions are different, raise an error\n                        raise ValueError(\n                            f\"The $defs key `{def_key}` conflicts with \"\n                            f\"existing definition in function schema of \"\n                            f\"`{self.name}`.\",\n                        )\n                    # The definitions are the same (from the same BaseModel),\n                    # skip merging this key\n                    continue\n\n                merged_params[\"$defs\"][def_key] = def_value_copy\n\n        return merged_schema\n\n\n@dataclass\nclass ToolGroup:\n    \"\"\"The tool group class\"\"\"\n\n    name: str\n    \"\"\"The group name, which will be used in the reset function as the group\n    identifier.\"\"\"\n    active: bool\n    \"\"\"If the tool group is active, meaning the tool functions in this group\n    is included in the JSON schema\"\"\"\n    description: str\n    \"\"\"The description of the tool group to tell the agent what the tool\n    group is about.\"\"\"\n    notes: str | None = None\n    \"\"\"The using notes of the tool group, to remind the agent how to use\"\"\"\n\n\nclass AgentSkill(TypedDict):\n    \"\"\"The agent skill typed dict class\"\"\"\n\n    name: str\n    \"\"\"The name of the skill.\"\"\"\n    description: str\n    \"\"\"The description of the skill.\"\"\"\n    dir: str\n    \"\"\"The directory of the agent skill.\"\"\"\n"
  },
  {
    "path": "src/agentscope/tracing/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The tracing interface class in agentscope.\"\"\"\n\nfrom ._setup import setup_tracing\nfrom ._trace import (\n    trace,\n    trace_llm,\n    trace_reply,\n    trace_format,\n    trace_toolkit,\n    trace_embedding,\n)\n\n__all__ = [\n    \"setup_tracing\",\n    \"trace\",\n    \"trace_llm\",\n    \"trace_reply\",\n    \"trace_format\",\n    \"trace_toolkit\",\n    \"trace_embedding\",\n]\n"
  },
  {
    "path": "src/agentscope/tracing/_attributes.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The tracing types class in agentscope.\"\"\"\nfrom opentelemetry.semconv._incubating.attributes import (\n    gen_ai_attributes as GenAIAttributes,\n)\n\n\nclass SpanAttributes:\n    \"\"\"The span attributes.\"\"\"\n\n    # GenAI Common Attributes\n    GEN_AI_CONVERSATION_ID = GenAIAttributes.GEN_AI_CONVERSATION_ID\n    \"\"\"The gen ai conversation ID.\"\"\"\n\n    GEN_AI_OPERATION_NAME = GenAIAttributes.GEN_AI_OPERATION_NAME\n    \"\"\"The gen ai operation name.\"\"\"\n\n    GEN_AI_PROVIDER_NAME = GenAIAttributes.GEN_AI_PROVIDER_NAME\n    \"\"\"The gen ai provider name.\"\"\"\n\n    # GenAI Request Attributes\n    GEN_AI_REQUEST_MODEL = GenAIAttributes.GEN_AI_REQUEST_MODEL\n    \"\"\"The gen ai request model.\"\"\"\n\n    GEN_AI_REQUEST_TEMPERATURE = GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE\n    \"\"\"The gen ai request temperature.\"\"\"\n\n    GEN_AI_REQUEST_TOP_P = GenAIAttributes.GEN_AI_REQUEST_TOP_P\n    \"\"\"The gen ai request top_p.\"\"\"\n\n    GEN_AI_REQUEST_TOP_K = GenAIAttributes.GEN_AI_REQUEST_TOP_K\n    \"\"\"The gen ai request top_k.\"\"\"\n\n    GEN_AI_REQUEST_MAX_TOKENS = GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS\n    \"\"\"The gen ai request max_tokens.\"\"\"\n\n    GEN_AI_REQUEST_PRESENCE_PENALTY = (\n        GenAIAttributes.GEN_AI_REQUEST_PRESENCE_PENALTY\n    )\n    \"\"\"The gen ai request presence_penalty.\"\"\"\n\n    GEN_AI_REQUEST_FREQUENCY_PENALTY = (\n        GenAIAttributes.GEN_AI_REQUEST_FREQUENCY_PENALTY\n    )\n    \"\"\"The gen ai request frequency_penalty.\"\"\"\n\n    GEN_AI_REQUEST_STOP_SEQUENCES = (\n        GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES\n    )\n    \"\"\"The gen ai request stop_sequences.\"\"\"\n\n    GEN_AI_REQUEST_SEED = GenAIAttributes.GEN_AI_REQUEST_SEED\n    \"\"\"The gen ai request seed.\"\"\"\n\n    # GenAI Response Attributes\n    GEN_AI_RESPONSE_ID = GenAIAttributes.GEN_AI_RESPONSE_ID\n    \"\"\"The gen ai response ID.\"\"\"\n\n    GEN_AI_RESPONSE_FINISH_REASONS = (\n        GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS\n    )\n    \"\"\"The gen ai response finish reasons.\"\"\"\n\n    # GenAI Usage Attributes\n    GEN_AI_USAGE_INPUT_TOKENS = GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS\n    \"\"\"The gen ai usage input tokens.\"\"\"\n\n    GEN_AI_USAGE_OUTPUT_TOKENS = GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS\n    \"\"\"The gen ai usage output tokens.\"\"\"\n\n    # GenAI Message Attributes\n    GEN_AI_INPUT_MESSAGES = GenAIAttributes.GEN_AI_INPUT_MESSAGES\n    \"\"\"The gen ai input messages.\"\"\"\n\n    GEN_AI_OUTPUT_MESSAGES = GenAIAttributes.GEN_AI_OUTPUT_MESSAGES\n    \"\"\"The gen ai output messages.\"\"\"\n\n    # GenAI Agent Attributes\n    GEN_AI_AGENT_ID = GenAIAttributes.GEN_AI_AGENT_ID\n    \"\"\"The gen ai agent ID.\"\"\"\n\n    GEN_AI_AGENT_NAME = GenAIAttributes.GEN_AI_AGENT_NAME\n    \"\"\"The gen ai agent name.\"\"\"\n\n    GEN_AI_AGENT_DESCRIPTION = GenAIAttributes.GEN_AI_AGENT_DESCRIPTION\n    \"\"\"The gen ai agent description.\"\"\"\n\n    GEN_AI_SYSTEM_INSTRUCTIONS = GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS\n    \"\"\"The gen ai system instructions.\"\"\"\n\n    # GenAI Tool Attributes\n    GEN_AI_TOOL_CALL_ID = GenAIAttributes.GEN_AI_TOOL_CALL_ID\n    \"\"\"The gen ai tool call ID.\"\"\"\n\n    GEN_AI_TOOL_NAME = GenAIAttributes.GEN_AI_TOOL_NAME\n    \"\"\"The gen ai tool name.\"\"\"\n\n    GEN_AI_TOOL_DESCRIPTION = GenAIAttributes.GEN_AI_TOOL_DESCRIPTION\n    \"\"\"The gen ai tool description.\"\"\"\n\n    GEN_AI_TOOL_CALL_ARGUMENTS = \"gen_ai.tool.call.arguments\"\n    \"\"\"The gen ai tool call arguments.\"\"\"\n\n    GEN_AI_TOOL_CALL_RESULT = \"gen_ai.tool.call.result\"\n    \"\"\"The gen ai tool call result.\"\"\"\n\n    GEN_AI_TOOL_DEFINITIONS = \"gen_ai.tool.definitions\"\n    \"\"\"The gen ai tool definitions.\"\"\"\n\n    # GenAI Embedding Attributes\n    GEN_AI_EMBEDDINGS_DIMENSION_COUNT = \"gen_ai.embeddings.dimension.count\"\n    \"\"\"The gen ai embeddings dimension count.\"\"\"\n\n    # AgentScope Extended Attributes\n    AGENTSCOPE_FORMAT_TARGET = \"agentscope.format.target\"\n    \"\"\"The agentscope format target.\"\"\"\n\n    AGENTSCOPE_FORMAT_COUNT = \"agentscope.format.count\"\n    \"\"\"The count of formatted messages in the result.\"\"\"\n\n    AGENTSCOPE_FUNCTION_NAME = \"agentscope.function.name\"\n    \"\"\"The agentscope function name.\"\"\"\n\n    AGENTSCOPE_FUNCTION_INPUT = \"agentscope.function.input\"\n    \"\"\"The agentscope function input.\"\"\"\n\n    AGENTSCOPE_FUNCTION_OUTPUT = \"agentscope.function.output\"\n    \"\"\"The agentscope function output.\"\"\"\n\n\nclass OperationNameValues:\n    \"\"\"The operation name values.\"\"\"\n\n    FORMATTER = \"format\"\n    \"\"\"The formatter operation name.\"\"\"\n\n    INVOKE_GENERIC_FUNCTION = \"invoke_generic_function\"\n    \"\"\"The invoke generic function operation name.\"\"\"\n\n    CHAT = GenAIAttributes.GenAiOperationNameValues.CHAT.value\n    \"\"\"The chat operation name.\"\"\"\n\n    INVOKE_AGENT = GenAIAttributes.GenAiOperationNameValues.INVOKE_AGENT.value\n    \"\"\"The invoke agent operation name.\"\"\"\n\n    EXECUTE_TOOL = GenAIAttributes.GenAiOperationNameValues.EXECUTE_TOOL.value\n    \"\"\"The execute tool operation name.\"\"\"\n\n    EMBEDDINGS = GenAIAttributes.GenAiOperationNameValues.EMBEDDINGS.value\n    \"\"\"The embeddings operation name.\"\"\"\n\n\nclass ProviderNameValues:\n    \"\"\"The provider name values.\"\"\"\n\n    DASHSCOPE = \"dashscope\"\n    \"\"\"The dashscope provider name.\"\"\"\n\n    OLLAMA = \"ollama\"\n    \"\"\"The ollama provider name.\"\"\"\n\n    DEEPSEEK = GenAIAttributes.GenAiProviderNameValues.DEEPSEEK.value\n    \"\"\"The deepseek provider name.\"\"\"\n\n    OPENAI = GenAIAttributes.GenAiProviderNameValues.OPENAI.value\n    \"\"\"The openai provider name.\"\"\"\n\n    ANTHROPIC = GenAIAttributes.GenAiProviderNameValues.ANTHROPIC.value\n    \"\"\"The anthropic provider name.\"\"\"\n\n    GCP_GEMINI = GenAIAttributes.GenAiProviderNameValues.GCP_GEMINI.value\n    \"\"\"The gcp gemini provider name.\"\"\"\n\n    MOONSHOT = \"moonshot\"\n    \"\"\"The moonshot provider name.\"\"\"\n\n    AZURE_AI_OPENAI = (\n        GenAIAttributes.GenAiProviderNameValues.AZURE_AI_OPENAI.value\n    )\n    \"\"\"The azure openai provider name.\"\"\"\n\n    AWS_BEDROCK = GenAIAttributes.GenAiProviderNameValues.AWS_BEDROCK.value\n    \"\"\"The aws bedrock provider name.\"\"\"\n"
  },
  {
    "path": "src/agentscope/tracing/_converter.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Convert ContentBlock to OpenTelemetry GenAI part format.\"\"\"\n\nfrom typing import Any, Dict\n\nfrom ..message import ContentBlock\n\nfrom ._utils import _serialize_to_str\n\n\ndef _convert_media_block(\n    source: Dict[str, Any],\n    modality: str,\n) -> Dict[str, Any] | None:\n    \"\"\"Convert media block (image/audio/video) to OpenTelemetry format.\n\n    Args:\n        source (`Dict[str, Any]`):\n            Source Dictionary with type, url/data, and media_type.\n        modality (`str`):\n            Media modality: \"image\", \"audio\", or \"video\".\n\n    Returns:\n        `Dict[str, Any] | None`:\n            Converted part Dictionary or None if source type is invalid.\n    \"\"\"\n    source_type = source.get(\"type\")\n\n    if source_type == \"url\":\n        url = source.get(\"url\", \"\")\n        return {\n            \"type\": \"uri\",\n            \"uri\": url,\n            \"modality\": modality,\n        }\n\n    if source_type == \"base64\":\n        data = source.get(\"data\", \"\")\n        media_type = source.get(\"media_type\")\n        if not media_type:\n            default_media_types = {\n                \"image\": \"image/jpeg\",\n                \"audio\": \"audio/wav\",\n                \"video\": \"video/mp4\",\n            }\n            media_type = default_media_types.get(modality, \"unknown\")\n        return {\n            \"type\": \"blob\",\n            \"content\": data,\n            \"media_type\": media_type,\n            \"modality\": modality,\n        }\n\n    return None\n\n\ndef _convert_block_to_part(block: ContentBlock) -> Dict[str, Any] | None:\n    \"\"\"Convert content block to OpenTelemetry GenAI part format.\n\n    Converts text, thinking, tool_use, tool_result, image, audio, video\n    blocks to standardized parts.\n\n    Args:\n        block (`ContentBlock`):\n            The content block object to convert. Supported block types:\n            - text: Text content block\n            - thinking: Reasoning/thinking content block\n            - tool_use: Tool call block with id, name, and input\n            - tool_result: Tool result block with id and output\n            - image: Image block with source (url or base64)\n            - audio: Audio block with source (url or base64)\n            - video: Video block with source (url or base64)\n\n    Returns:\n        `Dict[str, Any] | None`:\n            Standardized part Dictionary in OpenTelemetry GenAI format,\n            or None if the block type is invalid or cannot be converted.\n    \"\"\"\n    block_type = block.get(\"type\")\n    part: Dict[str, Any] | None = None\n\n    # Handle simple text-based blocks\n    if block_type == \"text\":\n        part = {\n            \"type\": \"text\",\n            \"content\": block.get(\"text\", \"\"),\n        }\n    elif block_type == \"thinking\":\n        part = {\n            \"type\": \"reasoning\",\n            \"content\": block.get(\"thinking\", \"\"),\n        }\n    # Handle tool blocks\n    elif block_type == \"tool_use\":\n        part = {\n            \"type\": \"tool_call\",\n            \"id\": block.get(\"id\", \"\"),\n            \"name\": block.get(\"name\", \"\"),\n            \"arguments\": block.get(\"input\", {}),\n        }\n    elif block_type == \"tool_result\":\n        output = block.get(\"output\", \"\")\n        if isinstance(output, (list, Dict)):\n            result = _serialize_to_str(output)\n        else:\n            result = str(output)\n\n        part = {\n            \"type\": \"tool_call_response\",\n            \"id\": block.get(\"id\", \"\"),\n            \"response\": result,\n        }\n    # Handle media blocks (image, audio, video)\n    elif block_type in (\"image\", \"audio\", \"video\"):\n        source = block.get(\"source\", {})\n        # Type assertion for mypy\n        if isinstance(source, dict):\n            source_dict: Dict[str, Any] = source\n\n            part = _convert_media_block(\n                source_dict,\n                modality=block_type,\n            )\n\n    return part\n"
  },
  {
    "path": "src/agentscope/tracing/_extractor.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Extract attributes from AgentScope components for OpenTelemetry tracing.\"\"\"\nimport inspect\nfrom typing import Any, Dict, Tuple, TYPE_CHECKING\n\nfrom .. import _config\nfrom ..embedding import EmbeddingModelBase\nfrom ..message import Msg, ToolUseBlock\nfrom ..model import ChatModelBase\n\nfrom ._attributes import (\n    SpanAttributes,\n    OperationNameValues,\n    ProviderNameValues,\n)\nfrom ._converter import _convert_block_to_part\nfrom ._utils import _serialize_to_str\n\nif TYPE_CHECKING:\n    from ..agent import AgentBase\n    from ..formatter import FormatterBase\n    from ..tool import (\n        Toolkit,\n    )\nelse:\n    AgentBase = \"AgentBase\"\n    FormatterBase = \"FormatterBase\"\n    Toolkit = \"Toolkit\"\n\n_CLASS_NAME_MAP = {\n    \"dashscope\": ProviderNameValues.DASHSCOPE,\n    \"openai\": ProviderNameValues.OPENAI,\n    \"anthropic\": ProviderNameValues.ANTHROPIC,\n    \"gemini\": ProviderNameValues.GCP_GEMINI,\n    \"ollama\": ProviderNameValues.OLLAMA,\n    \"deepseek\": ProviderNameValues.DEEPSEEK,\n    \"trinity\": ProviderNameValues.OPENAI,\n}\n\n# Map base URL fragments to provider names for OpenAI-compatible APIs\n_BASE_URL_PROVIDER_MAP = [\n    (\"api.openai.com\", ProviderNameValues.OPENAI),\n    (\"dashscope\", ProviderNameValues.DASHSCOPE),\n    (\"deepseek\", ProviderNameValues.DEEPSEEK),\n    (\"moonshot\", ProviderNameValues.MOONSHOT),\n    (\"generativelanguage.googleapis.com\", ProviderNameValues.GCP_GEMINI),\n    (\"openai.azure.com\", ProviderNameValues.AZURE_AI_OPENAI),\n    (\"amazonaws.com\", ProviderNameValues.AWS_BEDROCK),\n]\n\n\ndef _get_common_attributes() -> Dict[str, str]:\n    \"\"\"Get common attributes for all spans.\n\n    Returns:\n        `Dict[str, str]`:\n        Common span attributes including conversation ID\n    \"\"\"\n    return {\n        SpanAttributes.GEN_AI_CONVERSATION_ID: _serialize_to_str(\n            _config.run_id,\n        ),\n    }\n\n\ndef _get_format_target(instance: Any) -> str:\n    \"\"\"Get format target for the given instance.\n\n    Maps AgentScope formatter class names to format target names.\n\n    Args:\n        instance (`Any`):\n            The formatter instance to get the format target for.\n\n    Returns:\n        `str`:\n            Format target name (e.g., \"openai\", \"dashscope\", \"anthropic\")\n    \"\"\"\n    classname = instance.__class__.__name__\n    prefix_key = (\n        classname.removesuffix(\"ChatFormatter\")\n        .removesuffix(\"MultiAgentFormatter\")\n        .lower()\n    )\n    return _CLASS_NAME_MAP.get(prefix_key, \"unknown\")\n\n\ndef _get_provider_name(instance: ChatModelBase) -> str:\n    \"\"\"Get provider name from ChatModelBase instance.\n\n    Maps ChatModelBase class names to provider names, with special handling\n    for OpenAI-compatible APIs that may use different base URLs.\n    This follows the implementation pattern from agentscope-java PR #73.\n\n    Args:\n        instance (`ChatModelBase`):\n            The chat model instance to get the provider name for.\n\n    Returns:\n        `str`:\n            Provider name (e.g., \"openai\", \"dashscope\", \"anthropic\")\n    \"\"\"\n    classname = instance.__class__.__name__\n\n    # Special handling for OpenAIChatModel - check base_url\n    if classname == \"OpenAIChatModel\":\n        # Try to get base_url from the client\n        base_url = None\n        if hasattr(instance, \"client\") and hasattr(\n            instance.client,\n            \"base_url\",\n        ):\n            base_url = str(instance.client.base_url)\n\n        # If base_url is None or empty, return default OpenAI\n        if not base_url:\n            return ProviderNameValues.OPENAI\n\n        # Check base_url fragments to identify provider\n        for url_fragment, provider_name in _BASE_URL_PROVIDER_MAP:\n            if url_fragment in base_url:\n                return provider_name\n\n        # If no match found, return openai as default\n        return ProviderNameValues.OPENAI\n\n    # For other model types, use direct mapping\n    prefix_key = (\n        classname.removesuffix(\"ChatModel\")\n        .removesuffix(\"MultiAgentModel\")\n        .lower()\n    )\n    return _CLASS_NAME_MAP.get(prefix_key, \"unknown\")\n\n\ndef _get_tool_definitions(\n    tools: list[dict[str, Any]] | None,\n    tool_choice: str | None,\n) -> str | None:\n    \"\"\"Extract and serialize tool definitions for tracing.\n\n    Converts AgentScope/OpenAI nested tool format to OpenTelemetry GenAI\n    flat format for tracing.\n\n    Args:\n        tools (`list[dict[str, Any]] | None`, optional):\n            List of tool definitions in OpenAI format with nested\n            structure: ``[{\"type\": \"function\", \"function\": {...}}]``\n        tool_choice (`str | None`, optional):\n            Tool choice mode. Can be \"auto\", \"none\", \"any\", \"required\",\n            or a specific tool name. If \"none\", returns None to indicate\n            tools should not be traced.\n\n    Returns:\n        `str | None`:\n            Serialized tool definitions in flat format:\n            ``[{\"type\": \"function\", \"name\": ..., \"parameters\": ...}]``\n            or None if tools should not be traced (e.g., tools is None/empty\n            or tool_choice is \"none\").\n    \"\"\"\n    # No tools provided\n    if tools is None or not isinstance(tools, list) or len(tools) == 0:\n        return None\n\n    # Tool choice is explicitly \"none\" (model should not use tools)\n    if tool_choice == \"none\":\n        return None\n\n    try:\n        # Convert nested format to flat format for OpenTelemetry GenAI\n        # TODO: Currently only supports \"function\" type tools. If other tool\n        # types are added in the future (e.g., \"retrieval\", \"code_interpreter\",\n        # \"browser\"), this conversion logic needs to be updated to handle them.\n        flat_tools = []\n        for tool in tools:\n            if not isinstance(tool, dict) or \"function\" not in tool:\n                continue\n\n            func_def = tool[\"function\"]\n            flat_tool = {\n                \"type\": tool.get(\"type\", \"function\"),\n                \"name\": func_def.get(\"name\"),\n                \"description\": func_def.get(\"description\"),\n                \"parameters\": func_def.get(\"parameters\"),\n            }\n            # Remove None values\n            flat_tool = {k: v for k, v in flat_tool.items() if v is not None}\n            flat_tools.append(flat_tool)\n\n        if flat_tools:\n            return _serialize_to_str(flat_tools)\n        return None\n\n    except Exception:\n        return None\n\n\ndef _get_llm_request_attributes(\n    instance: ChatModelBase,\n    args: Tuple[Any, ...],\n    kwargs: Dict[str, Any],\n) -> Dict[str, Any]:\n    \"\"\"Get LLM request attributes for OpenTelemetry tracing.\n\n    Extracts request parameters from LLM model calls into GenAI attributes.\n\n    Args:\n        instance (`ChatModelBase`):\n            The chat model instance making the request.\n        args (`Tuple[Any, ...]`):\n            Positional arguments passed to the model call.\n        kwargs (`Dict[str, Any]`):\n            Keyword arguments including generation parameters such as\n            temperature, top_p, top_k, max_tokens, presence_penalty,\n            frequency_penalty, stop_sequences, seed, tools, and tool_choice.\n\n    Returns:\n        `Dict[str, Any]`:\n            OpenTelemetry GenAI attributes with string values, including\n            operation name, provider name, model name, generation parameters,\n            tool definitions, and custom AgentScope function input.\n    \"\"\"\n\n    attributes = {\n        # required attributes\n        SpanAttributes.GEN_AI_OPERATION_NAME: OperationNameValues.CHAT,\n        SpanAttributes.GEN_AI_PROVIDER_NAME: _get_provider_name(instance),\n        # conditionally required attributes\n        SpanAttributes.GEN_AI_REQUEST_MODEL: getattr(\n            instance,\n            \"model_name\",\n            \"unknown_model\",\n        ),\n        # recommended attributes\n        SpanAttributes.GEN_AI_REQUEST_TEMPERATURE: kwargs.get(\"temperature\"),\n        SpanAttributes.GEN_AI_REQUEST_TOP_P: kwargs.get(\"p\")\n        or kwargs.get(\"top_p\"),\n        SpanAttributes.GEN_AI_REQUEST_TOP_K: kwargs.get(\"top_k\"),\n        SpanAttributes.GEN_AI_REQUEST_MAX_TOKENS: kwargs.get(\"max_tokens\"),\n        SpanAttributes.GEN_AI_REQUEST_PRESENCE_PENALTY: kwargs.get(\n            \"presence_penalty\",\n        ),\n        SpanAttributes.GEN_AI_REQUEST_FREQUENCY_PENALTY: kwargs.get(\n            \"frequency_penalty\",\n        ),\n        SpanAttributes.GEN_AI_REQUEST_STOP_SEQUENCES: kwargs.get(\n            \"stop_sequences\",\n        ),\n        SpanAttributes.GEN_AI_REQUEST_SEED: kwargs.get(\"seed\"),\n        # custom attributes\n        SpanAttributes.AGENTSCOPE_FUNCTION_INPUT: _serialize_to_str(\n            {\n                \"args\": args,\n                \"kwargs\": kwargs,\n            },\n        ),\n    }\n\n    # Extract tool definitions if provided\n    tool_definitions = _get_tool_definitions(\n        tools=kwargs.get(\"tools\"),\n        tool_choice=kwargs.get(\"tool_choice\"),\n    )\n    if tool_definitions:\n        attributes[SpanAttributes.GEN_AI_TOOL_DEFINITIONS] = tool_definitions\n\n    return {k: v for k, v in attributes.items() if v is not None}\n\n\ndef _get_llm_span_name(attributes: Dict[str, str]) -> str:\n    \"\"\"Generate span name for LLM operations.\n\n    Args:\n        attributes (`Dict[str, str]`):\n            LLM request attributes dictionary containing operation name and\n            model name.\n\n    Returns:\n        `str`:\n            Formatted span name in the format \"{operation} {model}\",\n            e.g., \"chat gpt-4\" or \"chat qwen-plus\".\n    \"\"\"\n    return (\n        f\"{attributes[SpanAttributes.GEN_AI_OPERATION_NAME]} \"\n        f\"{attributes[SpanAttributes.GEN_AI_REQUEST_MODEL]}\"\n    )\n\n\ndef _get_llm_output_messages(\n    chat_response: Any,\n) -> list[dict[str, Any]]:\n    \"\"\"Extract and format LLM output messages for tracing.\n\n    Converts ChatResponse objects to standardized message format compatible\n    with OpenTelemetry GenAI specification.\n\n    Args:\n        chat_response (`Any`):\n            Chat response object with content blocks. Should be a ChatResponse\n            instance containing content blocks (text, tool_use, etc.).\n\n    Returns:\n        `list[dict[str, Any]]`:\n            List containing a single formatted message dictionary with role,\n            parts, and finish_reason. Returns the original response if it's\n            not a ChatResponse instance, or an error message format if\n            conversion fails.\n    \"\"\"\n    try:\n        from agentscope.model import ChatResponse\n\n        if not isinstance(chat_response, ChatResponse):\n            return chat_response\n\n        parts = []\n        finish_reason = \"stop\"  # Default finish reason\n\n        for block in chat_response.content:\n            part = _convert_block_to_part(block)\n            if part:\n                parts.append(part)\n\n        output_message = {\n            \"role\": \"assistant\",\n            \"parts\": parts,\n            \"finish_reason\": finish_reason,\n        }\n\n        return [output_message]\n\n    except Exception:\n        return [\n            {\n                \"role\": \"assistant\",\n                \"parts\": [\n                    {\n                        \"type\": \"text\",\n                        \"content\": \"<error processing response>\",\n                    },\n                ],\n                \"finish_reason\": \"error\",\n            },\n        ]\n\n\ndef _get_llm_response_attributes(\n    chat_response: Any,\n) -> Dict[str, Any]:\n    \"\"\"Get LLM response attributes for OpenTelemetry tracing.\n\n    Extracts response metadata and formats into GenAI attributes.\n\n    Args:\n        chat_response (`Any`):\n            Chat response object with data and usage info. Should have\n            attributes like id, usage (with input_tokens and output_tokens),\n            and content blocks.\n\n    Returns:\n        `Dict[str, Any]`:\n            OpenTelemetry GenAI response attributes including response ID,\n            finish reasons, token usage (input/output tokens), output messages,\n            and custom AgentScope function output.\n    \"\"\"\n    attributes = {\n        SpanAttributes.GEN_AI_RESPONSE_ID: getattr(\n            chat_response,\n            \"id\",\n            \"unknown_id\",\n        ),\n        # FIXME: finish reason should be capture in chat response\n        SpanAttributes.GEN_AI_RESPONSE_FINISH_REASONS: '[\"stop\"]',\n    }\n    if hasattr(chat_response, \"usage\") and chat_response.usage:\n        attributes[\n            SpanAttributes.GEN_AI_USAGE_INPUT_TOKENS\n        ] = chat_response.usage.input_tokens\n        attributes[\n            SpanAttributes.GEN_AI_USAGE_OUTPUT_TOKENS\n        ] = chat_response.usage.output_tokens\n\n    output_messages = _get_llm_output_messages(chat_response)\n    if output_messages:\n        attributes[SpanAttributes.GEN_AI_OUTPUT_MESSAGES] = _serialize_to_str(\n            output_messages,\n        )\n\n    attributes[SpanAttributes.AGENTSCOPE_FUNCTION_OUTPUT] = _serialize_to_str(\n        chat_response,\n    )\n    return attributes\n\n\ndef _get_agent_messages(\n    msg: Msg | list[Msg],\n) -> list[dict[str, Any]]:\n    \"\"\"Convert AgentScope message(s) to standardized parts format.\n\n    Transforms Msg objects into OpenTelemetry GenAI format.\n\n    Args:\n        msg (`Msg | list[Msg]`):\n            AgentScope message object or list of message objects with\n            content blocks.\n\n    Returns:\n        `list[dict[str, Any]]`:\n            List of formatted message dictionaries with role, parts, name,\n            and finish_reason.\n    \"\"\"\n    try:\n        if isinstance(msg, Msg):\n            msg = [msg]\n\n        formatted_msgs = []\n        for m in msg:\n            parts = []\n            for block in m.get_content_blocks():\n                part = _convert_block_to_part(block)\n                if part:\n                    parts.append(part)\n            formatted_msg = {\n                \"role\": m.role,\n                \"parts\": parts,\n                \"name\": m.name,\n                \"finish_reason\": \"stop\",\n            }\n            formatted_msgs.append(formatted_msg)\n\n        return formatted_msgs\n    except Exception:\n        return [\n            {\n                \"role\": msg.role,\n                \"parts\": [\n                    {\n                        \"type\": \"text\",\n                        \"content\": str(msg.content) if msg.content else \"\",\n                    },\n                ],\n                \"name\": msg.name,\n                \"finish_reason\": \"stop\",\n            },\n        ]\n\n\ndef _get_agent_request_attributes(\n    instance: \"AgentBase\",\n    args: Tuple[Any, ...],\n    kwargs: Dict[str, Any],\n) -> Dict[str, str]:\n    \"\"\"Get agent request attributes for OpenTelemetry tracing.\n\n    Extracts agent metadata and input data into GenAI attributes.\n\n    Args:\n        instance (`AgentBase`):\n            The agent instance making the request.\n        args (`Tuple[Any, ...]`):\n            Positional arguments passed to the agent's reply method.\n        kwargs (`Dict[str, Any]`):\n            Keyword arguments passed to the agent's reply method.\n\n    Returns:\n        `Dict[str, str]`:\n            OpenTelemetry GenAI attributes including operation name, agent ID,\n            agent name, agent description, input messages (if provided), and\n            custom AgentScope function input.\n    \"\"\"\n    attributes = {\n        SpanAttributes.GEN_AI_OPERATION_NAME: (\n            OperationNameValues.INVOKE_AGENT\n        ),\n        SpanAttributes.GEN_AI_AGENT_ID: getattr(instance, \"id\", \"unknown\"),\n        SpanAttributes.GEN_AI_AGENT_NAME: getattr(\n            instance,\n            \"name\",\n            \"unknown_agent\",\n        ),\n        SpanAttributes.GEN_AI_AGENT_DESCRIPTION: inspect.getdoc(\n            instance.__class__,\n        )\n        or \"No description available\",\n    }\n\n    msg = None\n    if args and len(args) > 0:\n        msg = args[0]\n    elif \"msg\" in kwargs:\n        msg = kwargs[\"msg\"]\n    if msg:\n        input_messages = _get_agent_messages(msg)\n        attributes[SpanAttributes.GEN_AI_INPUT_MESSAGES] = _serialize_to_str(\n            input_messages,\n        )\n\n    # custom attributes\n    attributes[SpanAttributes.AGENTSCOPE_FUNCTION_INPUT] = _serialize_to_str(\n        {\n            \"args\": args,\n            \"kwargs\": kwargs,\n        },\n    )\n    return attributes\n\n\ndef _get_agent_span_name(attributes: Dict[str, str]) -> str:\n    \"\"\"Generate span name for agent operations.\n\n    Args:\n        attributes (`Dict[str, str]`):\n            Agent request attributes dictionary containing operation name and\n            agent name.\n\n    Returns:\n        `str`:\n            Formatted span name in the format \"{operation} {agent_name}\",\n            e.g., \"invoke_agent MyAgent\".\n    \"\"\"\n    return (\n        f\"{attributes[SpanAttributes.GEN_AI_OPERATION_NAME]} \"\n        f\"{attributes[SpanAttributes.GEN_AI_AGENT_NAME]}\"\n    )\n\n\ndef _get_agent_response_attributes(\n    agent_response: Any,\n) -> Dict[str, str]:\n    \"\"\"Get agent response attributes for OpenTelemetry tracing.\n\n    Args:\n        agent_response (`Any`):\n            Response object returned by agent. Should be a Msg object with\n            content blocks.\n\n    Returns:\n        `Dict[str, str]`:\n            OpenTelemetry GenAI response attributes including output messages\n            and custom AgentScope function output.\n    \"\"\"\n    attributes = {\n        SpanAttributes.GEN_AI_OUTPUT_MESSAGES: _serialize_to_str(\n            _get_agent_messages(agent_response),\n        ),\n        SpanAttributes.AGENTSCOPE_FUNCTION_OUTPUT: _serialize_to_str(\n            agent_response,\n        ),\n    }\n    return attributes\n\n\ndef _get_tool_request_attributes(\n    instance: \"Toolkit\",\n    tool_call: ToolUseBlock,\n) -> Dict[str, str]:\n    \"\"\"Get tool request attributes for OpenTelemetry tracing.\n\n    Extracts tool execution metadata into GenAI attributes.\n\n    Args:\n        instance (`Toolkit`):\n            Toolkit instance with tool definitions. Used to extract tool\n            description from the tool's JSON schema.\n        tool_call (`ToolUseBlock`):\n            Tool use block with call information including id, name, and input\n            arguments.\n\n    Returns:\n        `Dict[str, str]`:\n            OpenTelemetry GenAI tool attributes including operation name, tool\n            call ID, tool name, tool description (if available), tool call\n            arguments, and custom AgentScope function input.\n    \"\"\"\n    attributes = {\n        SpanAttributes.GEN_AI_OPERATION_NAME: (\n            OperationNameValues.EXECUTE_TOOL\n        ),\n    }\n\n    if tool_call:\n        tool_name = tool_call.get(\"name\")\n        attributes[SpanAttributes.GEN_AI_TOOL_CALL_ID] = tool_call.get(\"id\")\n        attributes[SpanAttributes.GEN_AI_TOOL_NAME] = tool_name\n        attributes[\n            SpanAttributes.GEN_AI_TOOL_CALL_ARGUMENTS\n        ] = _serialize_to_str(tool_call.get(\"input\"))\n\n        if tool_name:\n            if tool := getattr(instance, \"tools\", {}).get(tool_name):\n                if tool_func := getattr(tool, \"json_schema\", {}).get(\n                    \"function\",\n                    {},\n                ):\n                    attributes[\n                        SpanAttributes.GEN_AI_TOOL_DESCRIPTION\n                    ] = tool_func.get(\"description\", \"unknown_description\")\n\n        # custom attributes\n        attributes[\n            SpanAttributes.AGENTSCOPE_FUNCTION_INPUT\n        ] = _serialize_to_str(\n            {\n                \"tool_call\": tool_call,\n            },\n        )\n    return attributes\n\n\ndef _get_tool_span_name(attributes: Dict[str, str]) -> str:\n    \"\"\"Generate span name for tool operations.\n\n    Args:\n        attributes (`Dict[str, str]`):\n            Tool request attributes dictionary containing operation name and\n            tool name.\n\n    Returns:\n        `str`:\n            Formatted span name in the format \"{operation} {tool_name}\",\n            e.g., \"execute_tool search\".\n    \"\"\"\n    return (\n        f\"{attributes[SpanAttributes.GEN_AI_OPERATION_NAME]} \"\n        f\"{attributes[SpanAttributes.GEN_AI_TOOL_NAME]}\"\n    )\n\n\ndef _get_tool_response_attributes(\n    tool_response: Any,\n) -> Dict[str, str]:\n    \"\"\"Get tool response attributes for OpenTelemetry tracing.\n\n    Args:\n        tool_response (`Any`):\n            Response object from tool execution. Can be any serializable object\n            returned by the tool function.\n\n    Returns:\n        `Dict[str, str]`:\n            OpenTelemetry GenAI response attributes including tool call result\n            and custom AgentScope function output.\n    \"\"\"\n    attributes = {\n        SpanAttributes.GEN_AI_TOOL_CALL_RESULT: _serialize_to_str(\n            tool_response,\n        ),\n    }\n\n    attributes[SpanAttributes.AGENTSCOPE_FUNCTION_OUTPUT] = _serialize_to_str(\n        tool_response,\n    )\n    return attributes\n\n\ndef _get_formatter_request_attributes(\n    instance: \"FormatterBase\",\n    args: Tuple[Any, ...],\n    kwargs: Dict[str, Any],\n) -> Dict[str, str]:\n    \"\"\"Get formatter request attributes for OpenTelemetry tracing.\n\n    Extracts formatter metadata into GenAI attributes.\n\n    Args:\n        instance (`FormatterBase`):\n            The formatter instance being used to format messages.\n        args (`Tuple[Any, ...]`):\n            Positional arguments passed to the formatter's format method.\n        kwargs (`Dict[str, Any]`):\n            Keyword arguments passed to the formatter's format method.\n\n    Returns:\n        `Dict[str, str]`:\n            OpenTelemetry GenAI formatter attributes including operation\n            name, format target (provider name), and custom AgentScope\n            function input.\n    \"\"\"\n    attributes = {\n        SpanAttributes.GEN_AI_OPERATION_NAME: (OperationNameValues.FORMATTER),\n        SpanAttributes.AGENTSCOPE_FORMAT_TARGET: _get_format_target(instance),\n        SpanAttributes.AGENTSCOPE_FUNCTION_INPUT: _serialize_to_str(\n            {\n                \"args\": args,\n                \"kwargs\": kwargs,\n            },\n        ),\n    }\n    return attributes\n\n\ndef _get_formatter_span_name(attributes: Dict[str, str]) -> str:\n    \"\"\"Generate span name for formatter operations.\n\n    Args:\n        attributes (`Dict[str, str]`):\n            Formatter request attributes dictionary containing operation name\n            and format target (provider name).\n\n    Returns:\n        `str`:\n            Formatted span name in the format \"{operation} {provider}\",\n            e.g., \"formatter openai\".\n    \"\"\"\n    return (\n        f\"{attributes[SpanAttributes.GEN_AI_OPERATION_NAME]} \"\n        f\"{attributes[SpanAttributes.AGENTSCOPE_FORMAT_TARGET]}\"\n    )\n\n\ndef _get_formatter_response_attributes(\n    response: Any,\n) -> Dict[str, Any]:\n    \"\"\"Get formatter response attributes for OpenTelemetry tracing.\n\n    Args:\n        response (`Any`):\n            Response object from formatter. Typically a list of dictionaries\n            representing formatted messages.\n\n    Returns:\n        `Dict[str, Any]`:\n            OpenTelemetry GenAI response attributes including custom AgentScope\n            function output and format count (if response is a list).\n    \"\"\"\n    attributes = {\n        SpanAttributes.AGENTSCOPE_FUNCTION_OUTPUT: _serialize_to_str(response),\n    }\n    if isinstance(response, list):\n        attributes[SpanAttributes.AGENTSCOPE_FORMAT_COUNT] = len(response)\n\n    return attributes\n\n\ndef _get_generic_function_request_attributes(\n    function_name: str,\n    args: Tuple[Any, ...],\n    kwargs: Dict[str, Any],\n) -> Dict[str, str]:\n    \"\"\"Get generic function request attributes for tracing.\n\n    Extracts metadata from function calls into GenAI attributes.\n\n    Args:\n        function_name (`str`):\n            Name of the function being called.\n        args (`Tuple[Any, ...]`):\n            Positional arguments passed to the function.\n        kwargs (`Dict[str, Any]`):\n            Keyword arguments passed to the function.\n\n    Returns:\n        `Dict[str, str]`:\n            OpenTelemetry GenAI function attributes including operation name,\n            function name, and custom AgentScope function input.\n    \"\"\"\n    attributes = {\n        SpanAttributes.GEN_AI_OPERATION_NAME: (\n            OperationNameValues.INVOKE_GENERIC_FUNCTION\n        ),\n        SpanAttributes.AGENTSCOPE_FUNCTION_NAME: function_name,\n        SpanAttributes.AGENTSCOPE_FUNCTION_INPUT: _serialize_to_str(\n            {\n                \"args\": args,\n                \"kwargs\": kwargs,\n            },\n        ),\n    }\n    return attributes\n\n\ndef _get_generic_function_span_name(attributes: Dict[str, str]) -> str:\n    \"\"\"Generate span name for generic function operations.\n\n    Args:\n        attributes (`Dict[str, str]`):\n            Generic function request attributes dictionary containing operation\n            name and function name.\n\n    Returns:\n        `str`:\n            Formatted span name in the format \"{operation} {function_name}\",\n            e.g., \"invoke_generic_function my_function\".\n    \"\"\"\n    return (\n        f\"{attributes[SpanAttributes.GEN_AI_OPERATION_NAME]} \"\n        f\"{attributes[SpanAttributes.AGENTSCOPE_FUNCTION_NAME]}\"\n    )\n\n\ndef _get_generic_function_response_attributes(\n    response: Any,\n) -> Dict[str, str]:\n    \"\"\"Get generic function response attributes for tracing.\n\n    Args:\n        response (`Any`):\n            Response object returned by the generic function. Can be any\n            serializable object.\n\n    Returns:\n        `Dict[str, str]`:\n            OpenTelemetry GenAI response attributes including custom AgentScope\n            function output.\n    \"\"\"\n    attributes = {\n        SpanAttributes.AGENTSCOPE_FUNCTION_OUTPUT: _serialize_to_str(response),\n    }\n    return attributes\n\n\ndef _get_embedding_request_attributes(\n    instance: \"EmbeddingModelBase\",\n    args: Tuple[Any, ...],\n    kwargs: Dict[str, Any],\n) -> Dict[str, Any]:\n    \"\"\"Get embedding request attributes for OpenTelemetry tracing.\n\n    Extracts embedding model metadata into GenAI attributes.\n\n    Args:\n        instance (`EmbeddingModelBase`):\n            The embedding model instance making the request.\n        args (`Tuple[Any, ...]`):\n            Positional arguments passed to the embedding model call.\n        kwargs (`Dict[str, Any]`):\n            Keyword arguments including dimensions and other embedding\n            parameters.\n\n    Returns:\n        `Dict[str, Any]`:\n            OpenTelemetry GenAI attributes including operation name,\n            model name, embedding dimensions count, and custom\n            AgentScope function input.\n    \"\"\"\n    attributes = {\n        SpanAttributes.GEN_AI_OPERATION_NAME: OperationNameValues.EMBEDDINGS,\n        SpanAttributes.GEN_AI_REQUEST_MODEL: getattr(\n            instance,\n            \"model_name\",\n            \"unknown_model\",\n        ),\n        SpanAttributes.GEN_AI_EMBEDDINGS_DIMENSION_COUNT: kwargs.get(\n            \"dimensions\",\n        ),\n        SpanAttributes.AGENTSCOPE_FUNCTION_INPUT: _serialize_to_str(\n            {\n                \"args\": args,\n                \"kwargs\": kwargs,\n            },\n        ),\n    }\n    return {k: v for k, v in attributes.items() if v is not None}\n\n\ndef _get_embedding_span_name(attributes: Dict[str, str]) -> str:\n    \"\"\"Generate span name for embedding operations.\n\n    Args:\n        attributes (`Dict[str, str]`):\n            Embedding request attributes dictionary containing operation name\n            and model name.\n\n    Returns:\n        `str`:\n            Formatted span name in the format \"{operation} {model}\",\n            e.g., \"embeddings text-embedding-ada-002\".\n    \"\"\"\n    return (\n        f\"{attributes[SpanAttributes.GEN_AI_OPERATION_NAME]} \"\n        f\"{attributes[SpanAttributes.GEN_AI_REQUEST_MODEL]}\"\n    )\n\n\ndef _get_embedding_response_attributes(\n    response: Any,\n) -> Dict[str, str]:\n    \"\"\"Get embedding response attributes for OpenTelemetry tracing.\n\n    Args:\n        response (`Any`):\n            Response object from embedding model. Typically a list of embedding\n            vectors or a similar structure.\n\n    Returns:\n        `Dict[str, str]`:\n            OpenTelemetry GenAI response attributes including custom AgentScope\n            function output.\n    \"\"\"\n    attributes = {\n        SpanAttributes.AGENTSCOPE_FUNCTION_OUTPUT: _serialize_to_str(response),\n    }\n    return {k: v for k, v in attributes.items() if v is not None}\n"
  },
  {
    "path": "src/agentscope/tracing/_setup.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The tracing interface class in agentscope.\"\"\"\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from opentelemetry.trace import Tracer\nelse:\n    Tracer = \"Tracer\"\n\n\ndef setup_tracing(endpoint: str) -> None:\n    \"\"\"Set up the AgentScope tracing by configuring the endpoint URL.\n\n    Args:\n        endpoint (`str`):\n            The endpoint URL for the tracing exporter.\n    \"\"\"\n    # Lazy import\n    from opentelemetry import trace\n    from opentelemetry.sdk.trace import TracerProvider\n    from opentelemetry.sdk.trace.export import BatchSpanProcessor\n    from opentelemetry.exporter.otlp.proto.http.trace_exporter import (\n        OTLPSpanExporter,\n    )\n\n    # Prepare a span_processor\n    exporter = OTLPSpanExporter(endpoint=endpoint)\n    span_processor = BatchSpanProcessor(exporter)\n\n    tracer_provider: TracerProvider = trace.get_tracer_provider()\n    if isinstance(tracer_provider, TracerProvider):\n        # The provider is set outside, just add the span processor\n        tracer_provider.add_span_processor(span_processor)\n\n    else:\n        tracer_provider = TracerProvider()\n        tracer_provider.add_span_processor(span_processor)\n        trace.set_tracer_provider(tracer_provider)\n\n\ndef _get_tracer() -> Tracer:\n    \"\"\"Get the tracer\n    Returns:\n        `Tracer`: The tracer with the name \"agentscope\" and version.\n    \"\"\"\n    from opentelemetry import trace\n    from .._version import __version__\n\n    return trace.get_tracer(\"agentscope\", __version__)\n"
  },
  {
    "path": "src/agentscope/tracing/_trace.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The tracing decorators for agent, formatter, toolkit, chat and embedding\nmodels.\"\"\"\nimport inspect\nfrom functools import wraps\nfrom typing import (\n    Generator,\n    AsyncGenerator,\n    Callable,\n    Any,\n    Coroutine,\n    TypeVar,\n    TYPE_CHECKING,\n)\n\nimport aioitertools\n\nfrom .. import _config\nfrom ..embedding import EmbeddingModelBase, EmbeddingResponse\nfrom .._logging import logger\nfrom ..message import Msg, ToolUseBlock\nfrom ..model import ChatModelBase, ChatResponse\n\nfrom ._attributes import SpanAttributes, OperationNameValues\nfrom ._extractor import (\n    _get_common_attributes,\n    _get_agent_request_attributes,\n    _get_agent_span_name,\n    _get_agent_response_attributes,\n    _get_llm_request_attributes,\n    _get_llm_span_name,\n    _get_llm_response_attributes,\n    _get_tool_request_attributes,\n    _get_tool_span_name,\n    _get_tool_response_attributes,\n    _get_formatter_request_attributes,\n    _get_formatter_span_name,\n    _get_formatter_response_attributes,\n    _get_generic_function_request_attributes,\n    _get_generic_function_span_name,\n    _get_generic_function_response_attributes,\n    _get_embedding_request_attributes,\n    _get_embedding_span_name,\n    _get_embedding_response_attributes,\n)\nfrom ._setup import _get_tracer\n\nif TYPE_CHECKING:\n    from opentelemetry.trace import Span\n\n    from ..agent import AgentBase\n    from ..formatter import FormatterBase\n    from ..tool import (\n        Toolkit,\n        ToolResponse,\n    )\n\nelse:\n    AgentBase = \"AgentBase\"\n    FormatterBase = \"FormatterBase\"\n    Span = \"Span\"\n    Toolkit = \"Toolkit\"\n    ToolResponse = \"ToolResponse\"\n\n\nT = TypeVar(\"T\")\n\n\ndef _check_tracing_enabled() -> bool:\n    \"\"\"Check if the OpenTelemetry tracer is initialized in AgentScope with an\n    endpoint.\n\n    TODO: We expect an OpenTelemetry official interface to check if the\n     tracer is initialized. Leaving this function here as a temporary\n     solution.\n    \"\"\"\n    return _config.trace_enabled\n\n\ndef _set_span_success_status(span: Span) -> None:\n    \"\"\"Set the status of the span.\n    Args:\n        span (`Span`):\n            The OpenTelemetry span to be used for tracing.\n    \"\"\"\n    from opentelemetry import trace as trace_api\n\n    span.set_status(trace_api.StatusCode.OK)\n    span.end()\n\n\ndef _set_span_error_status(span: Span, e: Exception) -> None:\n    \"\"\"Set the status of the span.\n    Args:\n        span (`Span`):\n            The OpenTelemetry span to be used for tracing.\n        e (`Exception`):\n            The exception to be recorded.\n    \"\"\"\n    from opentelemetry import trace as trace_api\n\n    span.set_status(trace_api.StatusCode.ERROR, str(e))\n    span.record_exception(e)\n    span.end()\n\n\ndef _trace_sync_generator_wrapper(\n    res: Generator[T, None, None],\n    span: Span,\n) -> Generator[T, None, None]:\n    \"\"\"Trace the sync generator output with OpenTelemetry.\"\"\"\n\n    has_error = False\n\n    try:\n        last_chunk = None\n        for chunk in res:\n            last_chunk = chunk\n            yield chunk\n    except Exception as e:\n        has_error = True\n        _set_span_error_status(span, e)\n        raise e from None\n\n    finally:\n        if not has_error:\n            # Set the last chunk as output\n            span.set_attributes(\n                _get_generic_function_response_attributes(last_chunk),\n            )\n            _set_span_success_status(span)\n\n\nasync def _trace_async_generator_wrapper(\n    res: AsyncGenerator[T, None],\n    span: Span,\n) -> AsyncGenerator[T, None]:\n    \"\"\"Trace the async generator output with OpenTelemetry.\n\n    Args:\n        res (`AsyncGenerator[T, None]`):\n            The generator or async generator to be traced.\n        span (`Span`):\n            The OpenTelemetry span to be used for tracing.\n\n    Yields:\n        `T`:\n            The output of the async generator.\n    \"\"\"\n    has_error = False\n\n    try:\n        last_chunk = None\n        async for chunk in aioitertools.iter(res):\n            last_chunk = chunk\n            yield chunk\n\n    except Exception as e:\n        has_error = True\n        _set_span_error_status(span, e)\n        raise e from None\n\n    finally:\n        if not has_error:\n            # Set the last chunk as output\n\n            if (\n                getattr(span, \"attributes\", {}).get(\n                    SpanAttributes.GEN_AI_OPERATION_NAME,\n                )\n                == OperationNameValues.CHAT\n            ):\n                response_attributes = _get_llm_response_attributes(last_chunk)\n            elif (\n                getattr(span, \"attributes\", {}).get(\n                    SpanAttributes.GEN_AI_OPERATION_NAME,\n                )\n                == OperationNameValues.EXECUTE_TOOL\n            ):\n                response_attributes = _get_tool_response_attributes(last_chunk)\n            else:\n                response_attributes = (\n                    _get_generic_function_response_attributes(\n                        last_chunk,\n                    )\n                )\n\n            span.set_attributes(response_attributes)\n            _set_span_success_status(span)\n\n\ndef trace(\n    name: str | None = None,\n) -> Callable:\n    \"\"\"A generic tracing decorator for synchronous and asynchronous functions.\n\n    Args:\n        name (`str | None`, optional):\n            The name of the span to be created. If not provided,\n            the name of the function will be used.\n\n    Returns:\n        `Callable`:\n            Returns a decorator that wraps the given function with\n            OpenTelemetry tracing.\n    \"\"\"\n\n    def decorator(\n        func: Callable,\n    ) -> Callable:\n        \"\"\"A decorator that wraps the given function with OpenTelemetry tracing\n\n        Args:\n            func (`Callable`):\n                The function to be traced, which can be sync or async function,\n                and returns an object or a generator.\n\n        Returns:\n            `Callable`:\n                A wrapper function that traces the function call and handles\n                input/output and exceptions.\n        \"\"\"\n        # Async function\n        if inspect.iscoroutinefunction(func):\n\n            @wraps(func)\n            async def wrapper(\n                *args: Any,\n                **kwargs: Any,\n            ) -> Any:\n                \"\"\"The wrapper function for tracing the sync function call.\"\"\"\n                if not _check_tracing_enabled():\n                    return await func(*args, **kwargs)\n\n                tracer = _get_tracer()\n\n                function_name = name if name else func.__name__\n                request_attributes = _get_generic_function_request_attributes(\n                    function_name,\n                    args,\n                    kwargs,\n                )\n\n                span_name = _get_generic_function_span_name(request_attributes)\n                with tracer.start_as_current_span(\n                    name=span_name,\n                    attributes=request_attributes,\n                    end_on_exit=False,\n                ) as span:\n                    try:\n                        res = await func(*args, **kwargs)\n\n                        # If generator or async generator\n                        if isinstance(res, AsyncGenerator):\n                            return _trace_async_generator_wrapper(res, span)\n                        if isinstance(res, Generator):\n                            return _trace_sync_generator_wrapper(res, span)\n\n                        # non-generator result\n                        span.set_attributes(\n                            _get_generic_function_response_attributes(res),\n                        )\n                        _set_span_success_status(span)\n                        return res\n\n                    except Exception as e:\n                        _set_span_error_status(span, e)\n                        raise e from None\n\n            return wrapper\n\n        # Sync function\n        @wraps(func)\n        def sync_wrapper(\n            *args: Any,\n            **kwargs: Any,\n        ) -> Any:\n            \"\"\"The wrapper function for tracing the sync function call.\"\"\"\n            if not _check_tracing_enabled():\n                return func(*args, **kwargs)\n\n            tracer = _get_tracer()\n\n            function_name = name if name else func.__name__\n            request_attributes = _get_generic_function_request_attributes(\n                function_name,\n                args,\n                kwargs,\n            )\n\n            span_name = _get_generic_function_span_name(request_attributes)\n            with tracer.start_as_current_span(\n                name=span_name,\n                attributes=request_attributes,\n                end_on_exit=False,\n            ) as span:\n                try:\n                    res = func(*args, **kwargs)\n\n                    # If generator or async generator\n                    if isinstance(res, AsyncGenerator):\n                        return _trace_async_generator_wrapper(res, span)\n                    if isinstance(res, Generator):\n                        return _trace_sync_generator_wrapper(res, span)\n\n                    # non-generator result\n                    span.set_attributes(\n                        _get_generic_function_response_attributes(res),\n                    )\n                    _set_span_success_status(span)\n                    return res\n\n                except Exception as e:\n                    _set_span_error_status(span, e)\n                    raise e from None\n\n        return sync_wrapper\n\n    return decorator\n\n\ndef trace_toolkit(\n    func: Callable[..., AsyncGenerator[ToolResponse, None]],\n) -> Callable[..., Coroutine[Any, Any, AsyncGenerator[ToolResponse, None]]]:\n    \"\"\"Trace the toolkit `call_tool_function` method with OpenTelemetry.\"\"\"\n\n    @wraps(func)\n    async def wrapper(\n        self: Toolkit,\n        tool_call: ToolUseBlock,\n    ) -> AsyncGenerator[ToolResponse, None]:\n        \"\"\"The wrapper function for tracing the toolkit call_tool_function\n        method.\"\"\"\n        if not _check_tracing_enabled():\n            return func(self, tool_call=tool_call)\n\n        tracer = _get_tracer()\n\n        request_attributes = _get_tool_request_attributes(self, tool_call)\n        span_name = _get_tool_span_name(request_attributes)\n        function_name = f\"{self.__class__.__name__}.{func.__name__}\"\n        with tracer.start_as_current_span(\n            name=span_name,\n            attributes={\n                **request_attributes,\n                **_get_common_attributes(),\n                SpanAttributes.AGENTSCOPE_FUNCTION_NAME: function_name,\n            },\n            end_on_exit=False,\n        ) as span:\n            try:\n                # Call the toolkit function (returns AsyncGenerator)\n                res = func(self, tool_call=tool_call)\n\n                # The result must be an AsyncGenerator\n                # Return the wrapped generator\n                return _trace_async_generator_wrapper(res, span)\n\n            except Exception as e:\n                _set_span_error_status(span, e)\n                span.end()\n                raise e from None\n\n    return wrapper\n\n\ndef trace_reply(\n    func: Callable[..., Coroutine[Any, Any, Msg]],\n) -> Callable[..., Coroutine[Any, Any, Msg]]:\n    \"\"\"Trace the agent reply call with OpenTelemetry.\n\n    Args:\n        func (`Callable[..., Coroutine[Any, Any, Msg]]`):\n            The agent async reply function to be traced.\n\n    Returns:\n        `Callable[..., Coroutine[Any, Any, Msg]]`:\n            A wrapper function that traces the agent reply call and handles\n            input/output and exceptions.\n    \"\"\"\n\n    @wraps(func)\n    async def wrapper(\n        self: \"AgentBase\",\n        *args: Any,\n        **kwargs: Any,\n    ) -> Msg:\n        \"\"\"The wrapper function for tracing the agent reply function call.\"\"\"\n        if not _check_tracing_enabled():\n            return await func(self, *args, **kwargs)\n\n        from ..agent import AgentBase\n\n        if not isinstance(self, AgentBase):\n            logger.warning(\n                \"Skipping tracing for %s as the first argument\"\n                \"is not an instance of AgentBase, but %s\",\n                func.__name__,\n                type(self),\n            )\n            return await func(self, *args, **kwargs)\n\n        tracer = _get_tracer()\n\n        # Prepare the attributes for the span\n\n        request_attributes = _get_agent_request_attributes(self, args, kwargs)\n        span_name = _get_agent_span_name(request_attributes)\n        function_name = f\"{self.__class__.__name__}.{func.__name__}\"\n        # Begin the llm call span\n        with tracer.start_as_current_span(\n            name=span_name,\n            attributes={\n                **request_attributes,\n                **_get_common_attributes(),\n                SpanAttributes.AGENTSCOPE_FUNCTION_NAME: function_name,\n            },\n            end_on_exit=False,\n        ) as span:\n            try:\n                # Call the agent reply function\n                res = await func(self, *args, **kwargs)\n\n                # Set the output attribute\n                span.set_attributes(_get_agent_response_attributes(res))\n                _set_span_success_status(span)\n                return res\n\n            except Exception as e:\n                _set_span_error_status(span, e)\n                raise e from None\n\n    return wrapper\n\n\ndef trace_embedding(\n    func: Callable[..., Coroutine[Any, Any, EmbeddingResponse]],\n) -> Callable[..., Coroutine[Any, Any, EmbeddingResponse]]:\n    \"\"\"Trace the embedding call with OpenTelemetry.\"\"\"\n\n    @wraps(func)\n    async def wrapper(\n        self: EmbeddingModelBase,\n        *args: Any,\n        **kwargs: Any,\n    ) -> EmbeddingResponse:\n        \"\"\"The wrapper function for tracing the embedding call.\"\"\"\n        if not _check_tracing_enabled():\n            return await func(self, *args, **kwargs)\n\n        if not isinstance(self, EmbeddingModelBase):\n            logger.warning(\n                \"Skipping tracing for %s as the first argument\"\n                \"is not an instance of EmbeddingModelBase, but %s\",\n                func.__name__,\n                type(self),\n            )\n            return await func(self, *args, **kwargs)\n\n        tracer = _get_tracer()\n\n        # Prepare the attributes for the span\n        request_attributes = _get_embedding_request_attributes(\n            self,\n            args,\n            kwargs,\n        )\n        span_name = _get_embedding_span_name(request_attributes)\n        function_name = f\"{self.__class__.__name__}.{func.__name__}\"\n\n        with tracer.start_as_current_span(\n            name=span_name,\n            attributes={\n                **request_attributes,\n                **_get_common_attributes(),\n                SpanAttributes.AGENTSCOPE_FUNCTION_NAME: function_name,\n            },\n            end_on_exit=False,\n        ) as span:\n            try:\n                # Call the embedding function\n                res = await func(self, *args, **kwargs)\n\n                # Set the output attribute\n                span.set_attributes(_get_embedding_response_attributes(res))\n                _set_span_success_status(span)\n                return res\n\n            except Exception as e:\n                _set_span_error_status(span, e)\n                raise e from None\n\n    return wrapper\n\n\ndef trace_format(\n    func: Callable[..., Coroutine[Any, Any, list[dict]]],\n) -> Callable[..., Coroutine[Any, Any, list[dict]]]:\n    \"\"\"Trace the format function of the formatter with OpenTelemetry.\n\n    Args:\n        func (`Callable[..., Coroutine[Any, Any, list[dict]]]`):\n            The async format function to be traced.\n\n    Returns:\n        `Callable[..., Coroutine[Any, Any, list[dict]]]`:\n            An async wrapper function that traces the format call and handles\n            input/output and exceptions.\n    \"\"\"\n\n    @wraps(func)\n    async def wrapper(\n        self: \"FormatterBase\",\n        *args: Any,\n        **kwargs: Any,\n    ) -> list[dict]:\n        \"\"\"Wrap the formatter __call__ method with OpenTelemetry tracing.\"\"\"\n        if not _check_tracing_enabled():\n            return await func(self, *args, **kwargs)\n\n        from ..formatter import FormatterBase\n\n        if not isinstance(self, FormatterBase):\n            logger.warning(\n                \"Skipping tracing for %s as the first argument\"\n                \"is not an instance of FormatterBase, but %s\",\n                func.__name__,\n                type(self),\n            )\n            return await func(self, *args, **kwargs)\n\n        tracer = _get_tracer()\n\n        # Prepare the attributes for the span\n        request_attributes = _get_formatter_request_attributes(\n            self,\n            args,\n            kwargs,\n        )\n        span_name = _get_formatter_span_name(request_attributes)\n        function_name = f\"{self.__class__.__name__}.{func.__name__}\"\n        with tracer.start_as_current_span(\n            name=span_name,\n            attributes={\n                **request_attributes,\n                **_get_common_attributes(),\n                SpanAttributes.AGENTSCOPE_FUNCTION_NAME: function_name,\n            },\n            end_on_exit=False,\n        ) as span:\n            try:\n                # Call the formatter function\n                res = await func(self, *args, **kwargs)\n\n                # Set the output attribute\n                span.set_attributes(_get_formatter_response_attributes(res))\n                _set_span_success_status(span)\n                return res\n\n            except Exception as e:\n                _set_span_error_status(span, e)\n                raise e from None\n\n    return wrapper\n\n\ndef trace_llm(\n    func: Callable[\n        ...,\n        Coroutine[\n            Any,\n            Any,\n            ChatResponse | AsyncGenerator[ChatResponse, None],\n        ],\n    ],\n) -> Callable[\n    ...,\n    Coroutine[Any, Any, ChatResponse | AsyncGenerator[ChatResponse, None]],\n]:\n    \"\"\"Trace the LLM call with OpenTelemetry.\n\n    Args:\n        func (`Callable`):\n            The function to be traced, which should be a coroutine that\n            returns either a `ChatResponse` or an `AsyncGenerator`\n            of `ChatResponse`.\n\n    Returns:\n        `Callable`:\n            A wrapper function that traces the LLM call and handles\n            input/output and exceptions.\n    \"\"\"\n\n    @wraps(func)\n    async def async_wrapper(\n        self: ChatModelBase,\n        *args: Any,\n        **kwargs: Any,\n    ) -> ChatResponse | AsyncGenerator[ChatResponse, None]:\n        \"\"\"The wrapper function for tracing the LLM call.\"\"\"\n        if not _check_tracing_enabled():\n            return await func(self, *args, **kwargs)\n\n        if not isinstance(self, ChatModelBase):\n            logger.warning(\n                \"Skipping tracing for %s as the first argument\"\n                \"is not an instance of ChatModelBase, but %s\",\n                func.__name__,\n                type(self),\n            )\n            return await func(self, *args, **kwargs)\n\n        tracer = _get_tracer()\n\n        # Prepare the attributes for the span\n        request_attributes = _get_llm_request_attributes(self, args, kwargs)\n        span_name = _get_llm_span_name(request_attributes)\n        function_name = f\"{self.__class__.__name__}.__call__\"\n        # Begin the llm call span\n        with tracer.start_as_current_span(\n            name=span_name,\n            attributes={\n                **request_attributes,\n                **_get_common_attributes(),\n                SpanAttributes.AGENTSCOPE_FUNCTION_NAME: function_name,\n            },\n            end_on_exit=False,\n        ) as span:\n            try:\n                # Must be an async calling\n                res = await func(self, *args, **kwargs)\n\n                # If the result is a AsyncGenerator\n                if isinstance(res, AsyncGenerator):\n                    return _trace_async_generator_wrapper(res, span)\n\n                # non-generator result\n                span.set_attributes(_get_llm_response_attributes(res))\n                _set_span_success_status(span)\n                return res\n\n            except Exception as e:\n                _set_span_error_status(span, e)\n                raise e from None\n\n    return async_wrapper\n"
  },
  {
    "path": "src/agentscope/tracing/_utils.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Serialize objects to JSON string.\"\"\"\nimport datetime\nimport enum\nimport inspect\nimport json\nfrom dataclasses import is_dataclass\nfrom typing import Any\n\nfrom pydantic import BaseModel\n\nfrom ..message import Msg\n\n\ndef _to_serializable(\n    obj: Any,\n) -> Any:\n    \"\"\"Convert an object to a JSON serializable type.\n\n    Args:\n        obj (`Any`):\n            The object to be converted to JSON serializable.\n\n    Returns:\n        `Any`:\n            The converted JSON serializable object\n    \"\"\"\n\n    # Handle primitive types first\n    if isinstance(obj, (str, int, bool, float, type(None))):\n        res = obj\n\n    elif isinstance(obj, (list, tuple, set, frozenset)):\n        res = [_to_serializable(x) for x in obj]\n\n    elif isinstance(obj, dict):\n        res = {str(key): _to_serializable(val) for (key, val) in obj.items()}\n\n    elif isinstance(obj, (Msg, BaseModel)) or is_dataclass(obj):\n        res = repr(obj)\n\n    elif inspect.isclass(obj) and issubclass(obj, BaseModel):\n        res = repr(obj)\n\n    elif isinstance(obj, (datetime.date, datetime.datetime, datetime.time)):\n        res = obj.isoformat()\n\n    elif isinstance(obj, datetime.timedelta):\n        res = obj.total_seconds()\n\n    elif isinstance(obj, enum.Enum):\n        res = _to_serializable(obj.value)\n\n    else:\n        res = str(obj)\n\n    return res\n\n\ndef _serialize_to_str(value: Any) -> str:\n    \"\"\"Serialize input value to JSON string.\n\n    Args:\n        value (`Any`):\n            The input value\n\n    Returns:\n        `str`:\n            JSON serialized string of the input value\n    \"\"\"\n    try:\n        return json.dumps(value, ensure_ascii=False)\n\n    except TypeError:\n        return json.dumps(\n            _to_serializable(value),\n            ensure_ascii=False,\n        )\n"
  },
  {
    "path": "src/agentscope/tts/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The TTS (Text-to-Speech) module.\"\"\"\n\nfrom ._tts_base import TTSModelBase\nfrom ._tts_response import TTSResponse, TTSUsage\nfrom ._dashscope_tts_model import DashScopeTTSModel\nfrom ._dashscope_realtime_tts_model import DashScopeRealtimeTTSModel\nfrom ._gemini_tts_model import GeminiTTSModel\nfrom ._openai_tts_model import OpenAITTSModel\nfrom ._dashscope_cosyvoice_tts_model import DashScopeCosyVoiceTTSModel\nfrom ._dashscope_cosyvoice_realtime_tts_model import (\n    DashScopeCosyVoiceRealtimeTTSModel,\n)\n\n__all__ = [\n    \"TTSModelBase\",\n    \"TTSResponse\",\n    \"TTSUsage\",\n    \"DashScopeTTSModel\",\n    \"DashScopeRealtimeTTSModel\",\n    \"GeminiTTSModel\",\n    \"OpenAITTSModel\",\n    \"DashScopeCosyVoiceTTSModel\",\n    \"DashScopeCosyVoiceRealtimeTTSModel\",\n]\n"
  },
  {
    "path": "src/agentscope/tts/_dashscope_cosyvoice_realtime_tts_model.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"DashScope CosyVoice Realtime TTS model implementation.\"\"\"\n\nfrom typing import Any, Literal, AsyncGenerator\n\nfrom ._tts_base import TTSModelBase\nfrom ._tts_response import TTSResponse\nfrom ._utils import _get_cosyvoice_callback_class\nfrom ..message import Msg\nfrom ..types import JSONSerializableObject\n\n\nclass DashScopeCosyVoiceRealtimeTTSModel(TTSModelBase):\n    \"\"\"TTS implementation for DashScope CosyVoice Realtime TTS API,\n    which supports streaming input. The supported models include\n    \"cosyvoice-v3-plus\", \"cosyvoice-v3-flash\", \"sambert\" etc.\n\n    For more details, please see the `official document\n    <https://help.aliyun.com/zh/model-studio/text-to-speech>`_.\n\n    .. note:: The DashScopeCosyVoiceRealtimeTTSModel can only handle one\n    streaming input request at a time, and cannot process multiple\n    streaming input requests concurrently. For example, it cannot handle\n    input sequences like `[msg_1_chunk0, msg_1_chunk1, msg_2_chunk0]`,\n    where the prefixes \"msg_x\" indicate different streaming input requests.\n    \"\"\"\n\n    supports_streaming_input: bool = True\n    \"\"\"Whether the model supports streaming input.\"\"\"\n\n    def __init__(\n        self,\n        api_key: str,\n        model_name: str = \"cosyvoice-v3-plus\",\n        voice: Literal[\n            \"longanyang\",\n            \"longanhuan\",\n            \"longhuhu_v3\",\n            \"longyingmu_v3\",\n        ]\n        | str = \"longanyang\",\n        stream: bool = True,\n        cold_start_length: int | None = None,\n        cold_start_words: int | None = None,\n        client_kwargs: dict[str, JSONSerializableObject] | None = None,\n        generate_kwargs: dict[str, JSONSerializableObject] | None = None,\n        max_retries: int = 3,\n        retry_delay: float = 5.0,\n    ) -> None:\n        \"\"\"Initialize the DashScope CosyVoice Realtime TTS model by\n        specifying the model, voice, and other parameters.\n\n        .. note:: More details about the parameters, such as `model_name`,\n        `voice`, and `mode` can be found in the `official document\n        <https://help.aliyun.com/zh/model-studio/cosyvoice-voice-list>`_.\n\n        .. note:: You can use `cold_start_length` and `cold_start_words`\n        simultaneously to set both character and word thresholds for the first\n        TTS request. For Chinese text, word segmentation (based on spaces) may\n        not be effective.\n\n        Args:\n            api_key (`str`):\n                The DashScope API key.\n            model_name (`str`, defaults to \"cosyvoice-v3-plus\"):\n                The TTS model name, e.g. \"cosyvoice-v3-plus\",\n                \"cosyvoice-v3-flash\", etc.\n            voice (`Literal[\"longanyang\", \"longanhuan\", \"longhuhu_v3\", \\\n            \"longyingmu_v3\"] | str`, defaults to \"longanyang\".):\n                The voice to use for synthesis. Refer to `official document\n                <https://help.aliyun.com/zh/model-studio/cosyvoice-voice-list>`_\n                for the supported voices for each model.\n            stream (`bool`, defaults to `True`):\n                Whether to use streaming synthesis.\n            cold_start_length (`int | None`, optional):\n                The minimum length send threshold for the first TTS request,\n                ensuring there is no pause in the synthesized speech for too\n                short input text. The length is measured in number of\n                characters.\n            cold_start_words (`int | None`, optional):\n                The minimum words send threshold for the first TTS request,\n                ensuring there is no pause in the synthesized speech for too\n                short input text. The words are identified by spaces in the\n                text.\n            client_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n             optional):\n                The extra keyword arguments to initialize the DashScope\n                CosyVoice Realtime tts client.\n            generate_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n             optional):\n               The extra keyword arguments used in DashScope CosyVoice\n               Realtime tts API generation.\n            max_retries (`int`, defaults to 3):\n                The maximum number of retry attempts when TTS synthesis fails.\n            retry_delay (`float`, defaults to 5.0):\n                The delay in seconds before retrying. Uses exponential backoff.\n        \"\"\"\n        super().__init__(model_name=model_name, stream=stream)\n\n        import dashscope\n        from dashscope.audio.tts_v2 import SpeechSynthesizer\n\n        dashscope.api_key = api_key\n\n        # Store configuration\n        self.voice = voice\n        self.cold_start_length = cold_start_length\n        self.cold_start_words = cold_start_words\n        self.client_kwargs = client_kwargs or {}\n        self.generate_kwargs = generate_kwargs or {}\n        self.max_retries = max_retries\n        self.retry_delay = retry_delay\n\n        # Initialize TTS client\n        # Save callback reference (for DashScope SDK)\n        self._dashscope_callback = _get_cosyvoice_callback_class()()\n\n        # The variables for tracking streaming input messages\n        # If we have sent text for the current message\n        self._first_send: bool = True\n        # The current message ID being processed\n        self._current_msg_id: str | None = None\n        # The current prefix text already sent\n        self._current_prefix: str = \"\"\n        self._synthesizer: SpeechSynthesizer | None = None\n\n    async def connect(self) -> None:\n        \"\"\"Connect to the TTS model and initialize resources.\"\"\"\n        from dashscope.audio.tts_v2 import SpeechSynthesizer, AudioFormat\n\n        self._synthesizer = SpeechSynthesizer(\n            model=self.model_name,\n            voice=self.voice,\n            format=AudioFormat.PCM_24000HZ_MONO_16BIT,\n            callback=self._dashscope_callback,\n            **self.client_kwargs,\n            **self.generate_kwargs,\n        )\n\n    async def close(self) -> None:\n        \"\"\"Close the TTS model and release resources.\"\"\"\n        self._synthesizer.close()\n\n    async def push(\n        self,\n        msg: Msg,\n        **kwargs: Any,\n    ) -> TTSResponse:\n        \"\"\"Append text to be synthesized and return the received TTS response.\n        Note this method is non-blocking, and maybe return an empty response\n        if no audio is received yet.\n\n        To receive all the synthesized speech, call the `synthesize` method\n        after pushing all the text chunks.\n\n        Args:\n            msg (`Msg`):\n                The message to be synthesized. The `msg.id` identifies the\n                streaming input request.\n            **kwargs (`Any`):\n                Additional keyword arguments to pass to the TTS API call.\n\n        Returns:\n            `TTSResponse`:\n                The TTSResponse containing audio blocks.\n        \"\"\"\n\n        if self._current_msg_id is not None and self._current_msg_id != msg.id:\n            raise RuntimeError(\n                \"DashScopeCosyVoiceRealtimeTTSModel can only handle one \"\n                \"streaming input request at a time. Please ensure that all \"\n                \"chunks belong to the same message ID.\",\n            )\n\n        # Record current message ID\n        self._current_msg_id = msg.id\n\n        text = msg.get_text_content()\n\n        # Determine if we should send text based on cold start settings only\n        # for the first input chunk and not the last chunk\n        if text:\n            if self._first_send:\n                # If we have cold start settings\n                if self.cold_start_length:\n                    if len(text) < self.cold_start_length:\n                        delta_to_send = \"\"\n                    else:\n                        delta_to_send = text\n                else:\n                    delta_to_send = text\n\n                if delta_to_send and self.cold_start_words:\n                    if len(delta_to_send.split()) < self.cold_start_words:\n                        delta_to_send = \"\"\n            else:\n                # Remove the already sent prefix if not the first send\n                delta_to_send = text.removeprefix(self._current_prefix)\n\n            if delta_to_send:\n                self._synthesizer.streaming_call(delta_to_send)\n\n                # Record sent prefix\n                self._current_prefix += delta_to_send\n                self._first_send = False\n\n            # Wait for the audio data to be available\n            res = await self._dashscope_callback.get_audio_data(block=False)\n\n            return res\n\n        # Return empty response if no text to send\n        return TTSResponse(content=None)\n\n    async def synthesize(\n        self,\n        msg: Msg | None = None,\n        **kwargs: Any,\n    ) -> TTSResponse | AsyncGenerator[TTSResponse, None]:\n        \"\"\"Append text to be synthesized and return TTS response.\n\n        Args:\n            msg (`Msg | None`, optional):\n                The message to be synthesized.\n            **kwargs (`Any`):\n                Additional keyword arguments to pass to the TTS API call.\n\n        Returns:\n            `TTSResponse | AsyncGenerator[TTSResponse, None]`:\n                The TTSResponse object in non-streaming mode, or an async\n                generator yielding TTSResponse objects in streaming mode.\n        \"\"\"\n        if (\n            self._current_msg_id is not None\n            and msg\n            and self._current_msg_id != msg.id\n        ):\n            raise RuntimeError(\n                \"DashScopeCosyVoiceRealtimeTTSModel can only handle one \"\n                \"streaming input request at a time. Please ensure that all \"\n                \"chunks belong to the same message ID.\",\n            )\n\n        if msg is None:\n            delta_to_send = \"\"\n\n        else:\n            # Record current message ID\n            self._current_msg_id = msg.id\n            delta_to_send = (msg.get_text_content() or \"\").removeprefix(\n                self._current_prefix,\n            )\n\n        # Determine if we should send text based on cold start settings only\n        # for the first input chunk and not the last chunk\n        if delta_to_send:\n            self._synthesizer.streaming_call(delta_to_send)\n\n            # To keep correct prefix tracking\n            self._current_prefix += delta_to_send\n            self._first_send = False\n\n        # We need to block until synthesis is complete to get all audio\n        self._synthesizer.streaming_complete()\n\n        if self.stream:\n            # Return an async generator for audio chunks\n            res = self._dashscope_callback.get_audio_chunk()\n\n        else:\n            # Block and wait for all audio data to be available\n            res = await self._dashscope_callback.get_audio_data(block=True)\n\n        # Update state for next message\n        self._current_msg_id = None\n        self._first_send = True\n        self._current_prefix = \"\"\n\n        return res\n"
  },
  {
    "path": "src/agentscope/tts/_dashscope_cosyvoice_tts_model.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"DashScope CosyVoice TTS model implementation.\"\"\"\n\nimport base64\nfrom typing import Any, Literal, AsyncGenerator\n\nfrom ._tts_base import TTSModelBase\nfrom ._tts_response import TTSResponse\nfrom ._utils import _get_cosyvoice_callback_class\nfrom ..message import Msg, AudioBlock, Base64Source\nfrom ..types import JSONSerializableObject\n\n\nclass DashScopeCosyVoiceTTSModel(TTSModelBase):\n    \"\"\"TTS implementation for DashScope CosyVoice TTS API.\n    The supported models include \"cosyvoice-v3-plus\",\n    \"cosyvoice-v3-flash\", \"sambert\" etc.\n\n    This model does NOT support streaming text input. For streaming input,\n    use `DashScopeCosyVoiceRealtimeTTSModel` instead.\n\n    For more details, please see the `official document\n    <https://help.aliyun.com/zh/model-studio/text-to-speech>`_.\n    \"\"\"\n\n    supports_streaming_input: bool = False\n    \"\"\"Whether the model supports streaming input.\"\"\"\n\n    def __init__(\n        self,\n        api_key: str,\n        model_name: str = \"cosyvoice-v3-plus\",\n        voice: Literal[\n            \"longanyang\",\n            \"longanhuan\",\n            \"longhuhu_v3\",\n            \"longyingmu_v3\",\n        ]\n        | str = \"longanyang\",\n        stream: bool = False,\n        client_kwargs: dict[str, JSONSerializableObject] | None = None,\n        generate_kwargs: dict[str, JSONSerializableObject] | None = None,\n    ) -> None:\n        \"\"\"Initialize the DashScope CosyVoice TTS model by\n        specifying the model, voice, and other parameters.\n\n        .. note:: More details about the parameters, such as `model_name`,\n        `voice`, and `mode` can be found in the `official document\n        <https://help.aliyun.com/zh/model-studio/cosyvoice-voice-list>`_.\n\n        Args:\n            api_key (`str`):\n                The DashScope API key.\n            model_name (`str`, defaults to \"cosyvoice-v3-plus\"):\n                The TTS model name, e.g. \"cosyvoice-v3-plus\",\n                \"cosyvoice-v3-flash\", etc.\n            voice (`Literal[\"longanyang\", \"longanhuan\", \"longhuhu_v3\", \\\n            \"longyingmu_v3\"] | str`, defaults to \"longanyang\".):\n                The voice to use for synthesis. Refer to `official document\n                <https://help.aliyun.com/zh/model-studio/cosyvoice-voice-list>`_\n                for the supported voices for each model.\n            stream (`bool`, defaults to `False`):\n                Whether to use streaming audio output.\n            client_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n             optional):\n                The extra keyword arguments to initialize the DashScope\n                CosyVoice tts client.\n            generate_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n             optional):\n                The extra keyword arguments used in DashScope CosyVoice\n                tts API generation.\n        \"\"\"\n        super().__init__(model_name=model_name, stream=stream)\n\n        import dashscope\n\n        dashscope.api_key = api_key\n\n        # Store configuration\n        self.voice = voice\n        self.client_kwargs = client_kwargs or {}\n        self.generate_kwargs = generate_kwargs or {}\n\n    def _create_synthesizer(self) -> tuple:\n        \"\"\"Create a new SpeechSynthesizer instance for each request.\"\"\"\n        from dashscope.audio.tts_v2 import SpeechSynthesizer, AudioFormat\n\n        callback = _get_cosyvoice_callback_class()() if self.stream else None\n\n        synthesizer = SpeechSynthesizer(\n            model=self.model_name,\n            voice=self.voice,\n            format=AudioFormat.PCM_24000HZ_MONO_16BIT,\n            callback=callback,\n            **self.client_kwargs,\n            **self.generate_kwargs,\n        )\n        return synthesizer, callback\n\n    async def push(self, msg: Msg, **kwargs: Any) -> TTSResponse:\n        \"\"\"Push a message to the TTS model and return TTS response.\n\n        Args:\n            msg (`Msg`):\n                The message to be synthesized.\n            **kwargs (`Any`):\n                Additional keyword arguments to pass to the TTS API call.\n\n        Returns:\n            `TTSResponse`:\n                The TTSResponse object.\n        \"\"\"\n        return TTSResponse(content=None)\n\n    async def synthesize(\n        self,\n        msg: Msg | None = None,\n        **kwargs: Any,\n    ) -> TTSResponse | AsyncGenerator[TTSResponse, None]:\n        \"\"\"Synthesize text to speech and return TTS response.\n\n        Args:\n            msg (`Msg | None`, optional):\n                The message to be synthesized.\n            **kwargs (`Any`):\n                Additional keyword arguments to pass to the TTS API call.\n\n        Returns:\n            `TTSResponse | AsyncGenerator[TTSResponse, None]`:\n                The TTSResponse object in non-streaming mode, or an async\n                generator yielding TTSResponse objects in streaming mode.\n        \"\"\"\n        if msg is None:\n            return TTSResponse(content=None)\n\n        text = msg.get_text_content()\n        if not text:\n            return TTSResponse(content=None)\n\n        # Create a new synthesizer for each request to avoid connection issues\n        synthesizer, callback = self._create_synthesizer()\n\n        if self.stream:\n            # Streaming output mode: use callback to get audio chunks\n            synthesizer.call(text=text)\n            return callback.get_audio_chunk()\n        else:\n            # Non-streaming mode: call directly returns audio bytes\n            audio = synthesizer.call(text=text)\n\n            if not audio:\n                return TTSResponse(content=None)\n\n            encoded_data = base64.b64encode(audio).decode()\n\n            return TTSResponse(\n                content=AudioBlock(\n                    type=\"audio\",\n                    source=Base64Source(\n                        type=\"base64\",\n                        data=encoded_data,\n                        media_type=\"audio/pcm;rate=24000\",\n                    ),\n                ),\n            )\n"
  },
  {
    "path": "src/agentscope/tts/_dashscope_realtime_tts_model.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"DashScope Realtime TTS model implementation.\"\"\"\n\nimport threading\nfrom typing import Any, Literal, TYPE_CHECKING, AsyncGenerator\n\nfrom ._tts_base import TTSModelBase\nfrom ._tts_response import TTSResponse\nfrom ..message import Msg, AudioBlock, Base64Source\nfrom ..types import JSONSerializableObject\n\nif TYPE_CHECKING:\n    from dashscope.audio.qwen_tts_realtime import (\n        QwenTtsRealtime,\n        QwenTtsRealtimeCallback,\n    )\nelse:\n    QwenTtsRealtime = \"dashscope.audio.qwen_tts_realtime.QwenTtsRealtime\"\n    QwenTtsRealtimeCallback = (\n        \"dashscope.audio.qwen_tts_realtime.QwenTtsRealtimeCallback\"\n    )\n\n\ndef _get_qwen_tts_realtime_callback_class() -> type[\"QwenTtsRealtimeCallback\"]:\n    from dashscope.audio.qwen_tts_realtime import QwenTtsRealtimeCallback\n\n    class _DashScopeRealtimeTTSCallback(QwenTtsRealtimeCallback):\n        \"\"\"DashScope Realtime TTS callback.\"\"\"\n\n        def __init__(self) -> None:\n            \"\"\"Initialize the DashScope Realtime TTS callback.\"\"\"\n            super().__init__()\n\n            # The event that will be set when a new audio chunk is received\n            self.chunk_event = threading.Event()\n            # The event that will be set when the TTS synthesis is finished\n            self.finish_event = threading.Event()\n            # Cache the audio data\n            self._audio_data: str = \"\"\n\n        def on_event(self, response: dict[str, Any]) -> None:\n            \"\"\"Called when a TTS event is received (DashScope SDK callback).\n\n            Args:\n                response (`dict[str, Any]`):\n                    The event response dictionary.\n            \"\"\"\n            try:\n                event_type = response.get(\"type\")\n\n                if event_type == \"session.created\":\n                    self._audio_data = \"\"\n                    self.finish_event.clear()\n\n                elif event_type == \"response.audio.delta\":\n                    audio_data = response.get(\"delta\")\n                    if audio_data:\n                        # Process audio data in thread callback\n                        if isinstance(audio_data, bytes):\n                            import base64\n\n                            audio_data = base64.b64encode(audio_data).decode()\n\n                        # Accumulate audio data\n                        self._audio_data += audio_data\n\n                        # Signal that a new audio chunk is available\n                        if not self.chunk_event.is_set():\n                            self.chunk_event.set()\n\n                elif event_type == \"response.done\":\n                    # Response completed, can be used for metrics\n                    pass\n\n                elif event_type == \"session.finished\":\n                    self.chunk_event.set()\n                    self.finish_event.set()\n\n            except Exception:\n                import traceback\n\n                traceback.print_exc()\n                self.finish_event.set()\n\n        async def get_audio_data(self, block: bool) -> TTSResponse:\n            \"\"\"Get the current accumulated audio data as base64 string so far.\n\n            Returns:\n                `str`:\n                    The base64-encoded audio data.\n            \"\"\"\n            # Block until synthesis is finished\n            if block:\n                self.finish_event.wait()\n\n            # Return the accumulated audio data\n            if self._audio_data:\n                return TTSResponse(\n                    content=AudioBlock(\n                        type=\"audio\",\n                        source=Base64Source(\n                            type=\"base64\",\n                            data=self._audio_data,\n                            media_type=\"audio/pcm;rate=24000\",\n                        ),\n                    ),\n                )\n\n            # Reset for next tts request\n            await self._reset()\n\n            # Return empty response if no audio data\n            return TTSResponse(content=None)\n\n        async def get_audio_chunk(self) -> AsyncGenerator[TTSResponse, None]:\n            \"\"\"Get the audio data chunk as an async generator of `TTSResponse`\n            objects.\n\n            Returns:\n                `AsyncGenerator[TTSResponse, None]`:\n                    The async generator yielding TTSResponse with audio chunks.\n            \"\"\"\n            while True:\n                if self.finish_event.is_set():\n                    yield TTSResponse(\n                        content=AudioBlock(\n                            type=\"audio\",\n                            source=Base64Source(\n                                type=\"base64\",\n                                data=self._audio_data,\n                                media_type=\"audio/pcm;rate=24000\",\n                            ),\n                        ),\n                        is_last=True,\n                    )\n\n                    # Reset for next tts request\n                    await self._reset()\n\n                    break\n\n                if self.chunk_event.is_set():\n                    # Clear the event for next chunk\n                    self.chunk_event.clear()\n                else:\n                    # Wait for the next chunk\n                    self.chunk_event.wait()\n\n                yield TTSResponse(\n                    content=AudioBlock(\n                        type=\"audio\",\n                        source=Base64Source(\n                            type=\"base64\",\n                            data=self._audio_data,\n                            media_type=\"audio/pcm;rate=24000\",\n                        ),\n                    ),\n                    is_last=False,\n                )\n\n        async def _reset(self) -> None:\n            \"\"\"Reset the callback state for a new TTS request.\"\"\"\n            self.finish_event.clear()\n            self.chunk_event.clear()\n            self._audio_data = \"\"\n\n    return _DashScopeRealtimeTTSCallback\n\n\nclass DashScopeRealtimeTTSModel(TTSModelBase):\n    \"\"\"TTS implementation for DashScope Qwen Realtime TTS API, which supports\n    streaming input. The supported models include \"qwen-3-tts-flash-realtime\",\n    \"qwen-tts-realtime\", etc.\n\n    For more details, please see the `official document\n    <https://bailian.console.aliyun.com/?tab=doc#/doc/?type=model&url=2938790>`_.\n\n    .. note:: The DashScopeRealtimeTTSModel can only handle one streaming\n    input request at a time, and cannot process multiple streaming input\n    requests concurrently. For example, it cannot handle input sequences like\n    `[msg_1_chunk0, msg_1_chunk1, msg_2_chunk0]`, where the prefixes \"msg_x\"\n    indicate different streaming input requests.\n    \"\"\"\n\n    supports_streaming_input: bool = True\n    \"\"\"Whether the model supports streaming input.\"\"\"\n\n    def __init__(\n        self,\n        api_key: str,\n        model_name: str = \"qwen3-tts-flash-realtime\",\n        voice: Literal[\"Cherry\", \"Nofish\", \"Ethan\", \"Jennifer\"]\n        | str = \"Cherry\",\n        stream: bool = True,\n        cold_start_length: int | None = None,\n        cold_start_words: int | None = None,\n        client_kwargs: dict[str, JSONSerializableObject] | None = None,\n        generate_kwargs: dict[str, JSONSerializableObject] | None = None,\n    ) -> None:\n        \"\"\"Initialize the DashScope TTS model by specifying the model, voice,\n        and other parameters.\n\n        .. note:: More details about the parameters, such as `model_name`,\n        `voice`, and `mode` can be found in the `official document\n        <https://bailian.console.aliyun.com/?tab=doc#/doc/?type=model&url=2938790>`_.\n\n        .. note:: You can use `cold_start_length` and `cold_start_words`\n        simultaneously to set both character and word thresholds for the first\n        TTS request. For Chinese text, word segmentation (based on spaces) may\n        not be effective.\n\n        Args:\n            api_key (`str`):\n                The DashScope API key.\n            model_name (`str`, defaults to \"qwen-tts-realtime\"):\n                The TTS model name, e.g. \"qwen3-tts-flash-realtime\",\n                \"qwen-tts-realtime\", etc.\n            voice (`Literal[\"Cherry\", \"Serena\", \"Ethan\", \"Chelsie\"] | str`, \\\n             defaults to \"Cherry\".):\n                The voice to use for synthesis. Refer to `official document\n                <https://bailian.console.aliyun.com/?tab=doc#/doc/?type=model&url=2938790>`_\n                for the supported voices for each model.\n            stream (`bool`, defaults to `True`):\n                Whether to use streaming synthesis.\n            cold_start_length (`int | None`, optional):\n                The minimum length send threshold for the first TTS request,\n                ensuring there is no pause in the synthesized speech for too\n                short input text. The length is measured in number of\n                characters.\n            cold_start_words (`int | None`, optional):\n                The minimum words send threshold for the first TTS request,\n                ensuring there is no pause in the synthesized speech for too\n                short input text. The words are identified by spaces in the\n                text.\n            client_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n             optional):\n                The extra keyword arguments to initialize the DashScope\n                realtime tts client.\n            generate_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n             optional):\n               The extra keyword arguments used in DashScope realtime tts API\n               generation.\n        \"\"\"\n        super().__init__(model_name=model_name, stream=stream)\n\n        import dashscope\n        from dashscope.audio.qwen_tts_realtime import QwenTtsRealtime\n\n        dashscope.api_key = api_key\n\n        # Store configuration\n        self.voice = voice\n        self.mode = \"server_commit\"\n        self.cold_start_length = cold_start_length\n        self.cold_start_words = cold_start_words\n        self.client_kwargs = client_kwargs or {}\n        self.generate_kwargs = generate_kwargs or {}\n\n        # Initialize TTS client\n        # Save callback reference (for DashScope SDK)\n        self._dashscope_callback = _get_qwen_tts_realtime_callback_class()()\n        self._tts_client: QwenTtsRealtime = QwenTtsRealtime(\n            model=self.model_name,\n            callback=self._dashscope_callback,\n            **self.client_kwargs,\n        )\n\n        self._connected = False\n\n        # The variables for tracking streaming input messages\n        # If we have sent text for the current message\n        self._first_send: bool = True\n        # The current message ID being processed\n        self._current_msg_id: str | None = None\n        # The current prefix text already sent\n        self._current_prefix: str = \"\"\n\n    async def connect(self) -> None:\n        \"\"\"Initialize the DashScope TTS model and establish connection.\"\"\"\n        if self._connected:\n            return\n\n        self._tts_client.connect()\n\n        # Update session with voice and format settings\n        self._tts_client.update_session(\n            voice=self.voice,\n            mode=self.mode,\n            **self.generate_kwargs,\n        )\n\n        self._connected = True\n\n    async def close(self) -> None:\n        \"\"\"Close the TTS model and clean up resources.\"\"\"\n        if not self._connected:\n            return\n\n        self._connected = False\n\n        self._tts_client.finish()\n        self._tts_client.close()\n\n    async def push(\n        self,\n        msg: Msg,\n        **kwargs: Any,\n    ) -> TTSResponse:\n        \"\"\"Append text to be synthesized and return the received TTS response.\n        Note this method is non-blocking, and maybe return an empty response\n        if no audio is received yet.\n\n        To receive all the synthesized speech, call the `synthesize` method\n        after pushing all the text chunks.\n\n        Args:\n            msg (`Msg`):\n                The message to be synthesized. The `msg.id` identifies the\n                streaming input request.\n            **kwargs (`Any`):\n                Additional keyword arguments to pass to the TTS API call.\n\n        Returns:\n            `TTSResponse`:\n                The TTSResponse containing audio blocks.\n        \"\"\"\n        if not self._connected:\n            raise RuntimeError(\n                \"TTS model is not connected. Call `connect()` first.\",\n            )\n\n        if self._current_msg_id is not None and self._current_msg_id != msg.id:\n            raise RuntimeError(\n                \"DashScopeRealtimeTTSModel can only handle one streaming \"\n                \"input request at a time. Please ensure that all chunks \"\n                \"belong to the same message ID.\",\n            )\n\n        # Record current message ID\n        self._current_msg_id = msg.id\n\n        text = msg.get_text_content()\n\n        # Determine if we should send text based on cold start settings only\n        # for the first input chunk and not the last chunk\n        if text:\n            if self._first_send:\n                # If we have cold start settings\n                if self.cold_start_length:\n                    if len(text) < self.cold_start_length:\n                        delta_to_send = \"\"\n                    else:\n                        delta_to_send = text\n                else:\n                    delta_to_send = text\n\n                if delta_to_send and self.cold_start_words:\n                    if len(delta_to_send.split()) < self.cold_start_words:\n                        delta_to_send = \"\"\n            else:\n                # Remove the already sent prefix if not the first send\n                delta_to_send = text.removeprefix(self._current_prefix)\n\n            if delta_to_send:\n                self._tts_client.append_text(delta_to_send)\n\n                # Record sent prefix\n                self._current_prefix += delta_to_send\n                self._first_send = False\n\n            # Wait for the audio data to be available\n            res = await self._dashscope_callback.get_audio_data(block=False)\n\n            return res\n\n        # Return empty response if no text to send\n        return TTSResponse(content=None)\n\n    async def synthesize(\n        self,\n        msg: Msg | None = None,\n        **kwargs: Any,\n    ) -> TTSResponse | AsyncGenerator[TTSResponse, None]:\n        \"\"\"Append text to be synthesized and return TTS response.\n\n        Args:\n            msg (`Msg | None`, optional):\n                The message to be synthesized.\n            **kwargs (`Any`):\n                Additional keyword arguments to pass to the TTS API call.\n\n        Returns:\n            `TTSResponse | AsyncGenerator[TTSResponse, None]`:\n                The TTSResponse object in non-streaming mode, or an async\n                generator yielding TTSResponse objects in streaming mode.\n        \"\"\"\n        if not self._connected:\n            raise RuntimeError(\n                \"TTS model is not connected. Call `connect()` first.\",\n            )\n\n        if self._current_msg_id is not None and self._current_msg_id != msg.id:\n            raise RuntimeError(\n                \"DashScopeRealtimeTTSModel can only handle one streaming \"\n                \"input request at a time. Please ensure that all chunks \"\n                \"belong to the same message ID.\",\n            )\n\n        if msg is None:\n            delta_to_send = \"\"\n\n        else:\n            # Record current message ID\n            self._current_msg_id = msg.id\n            delta_to_send = (msg.get_text_content() or \"\").removeprefix(\n                self._current_prefix,\n            )\n\n        # Determine if we should send text based on cold start settings only\n        # for the first input chunk and not the last chunk\n        if delta_to_send:\n            self._tts_client.append_text(delta_to_send)\n\n            # To keep correct prefix tracking\n            self._current_prefix += delta_to_send\n            self._first_send = False\n\n        # We need to block until synthesis is complete to get all audio\n        self._tts_client.commit()\n        self._tts_client.finish()\n\n        if self.stream:\n            # Return an async generator for audio chunks\n            res = self._dashscope_callback.get_audio_chunk()\n\n        else:\n            # Block and wait for all audio data to be available\n            res = await self._dashscope_callback.get_audio_data(block=True)\n\n        # Update state for next message\n        self._current_msg_id = None\n        self._first_send = True\n        self._current_prefix = \"\"\n\n        return res\n"
  },
  {
    "path": "src/agentscope/tts/_dashscope_tts_model.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"DashScope SDK TTS model implementation using MultiModalConversation API.\"\"\"\nfrom typing import (\n    Any,\n    Literal,\n    AsyncGenerator,\n    Generator,\n    TYPE_CHECKING,\n)\n\nfrom ._tts_base import TTSModelBase\nfrom ._tts_response import TTSResponse\nfrom ..message import Msg, AudioBlock, Base64Source\nfrom ..types import JSONSerializableObject\n\nif TYPE_CHECKING:\n    from dashscope.api_entities.dashscope_response import (\n        MultiModalConversationResponse,\n    )\n\nelse:\n    MultiModalConversationResponse = (\n        \"dashscope.api_entities.dashscope_response.\"\n        \"MultiModalConversationResponse\"\n    )\n\n\nclass DashScopeTTSModel(TTSModelBase):\n    \"\"\"DashScope TTS model implementation using MultiModalConversation API.\n    For more details, please see the `official document\n    <https://bailian.console.aliyun.com/?tab=doc#/doc/?type=model&url=2879134>`_.\n    \"\"\"\n\n    supports_streaming_input: bool = False\n    \"\"\"Whether the model supports streaming input.\"\"\"\n\n    def __init__(\n        self,\n        api_key: str,\n        model_name: str = \"qwen3-tts-flash\",\n        voice: Literal[\"Cherry\", \"Serena\", \"Ethan\", \"Chelsie\"]\n        | str = \"Cherry\",\n        language_type: str = \"Auto\",\n        stream: bool = True,\n        generate_kwargs: dict[str, JSONSerializableObject] | None = None,\n    ) -> None:\n        \"\"\"Initialize the DashScope SDK TTS model.\n\n        .. note:: More details about the parameters, such as `model_name`,\n        `voice`, and language_type can be found in the `official document\n        <https://bailian.console.aliyun.com/?tab=doc#/doc/?type=model&url=2879134>`_.\n\n        Args:\n            api_key (`str`):\n                The DashScope API key. Required.\n            model_name (`str`, defaults to \"qwen3-tts-flash\"):\n                The TTS model name. Supported models are qwen3-tts-flash,\n                qwen-tts, etc.\n            voice (`Literal[\"Cherry\", \"Serena\", \"Ethan\", \"Chelsie\"] | str`, \\\n             defaults to \"Cherry\"):\n                The voice to use. Supported voices are \"Cherry\", \"Serena\",\n                \"Ethan\", \"Chelsie\", etc.\n            language_type (`str`, default to \"Auto\"):\n                The language type. Should match the text language for\n                correct pronunciation and natural intonation.\n            generate_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n             optional):\n               The extra keyword arguments used in Dashscope TTS API\n               generation, e.g. `temperature`, `seed`.\n        \"\"\"\n        super().__init__(model_name=model_name, stream=stream)\n\n        self.api_key = api_key\n        self.voice = voice\n        self.language_type = language_type\n        self.generate_kwargs = generate_kwargs or {}\n\n    async def synthesize(\n        self,\n        msg: Msg | None = None,\n        **kwargs: Any,\n    ) -> TTSResponse | AsyncGenerator[TTSResponse, None]:\n        \"\"\"Call the DashScope TTS API to synthesize speech from text.\n\n        Args:\n            msg (`Msg | None`, optional):\n                The message to be synthesized.\n            **kwargs (`Any`):\n                Additional keyword arguments to pass to the TTS API call.\n\n        Returns:\n            `TTSResponse | AsyncGenerator[TTSResponse, None]`:\n                The TTS response or an async generator yielding TTSResponse\n                objects in streaming mode.\n        \"\"\"\n\n        if msg is None:\n            return TTSResponse(content=None)\n\n        text = msg.get_text_content()\n\n        import dashscope\n\n        # Call DashScope TTS API with streaming mode\n        response = dashscope.MultiModalConversation.call(\n            model=self.model_name,\n            api_key=self.api_key,\n            text=text,\n            voice=self.voice,\n            language_type=self.language_type,\n            stream=True,\n            **self.generate_kwargs,\n            **kwargs,\n        )\n\n        if self.stream:\n            return self._parse_into_async_generator(response)\n\n        audio_data = \"\"\n        for chunk in response:\n            if chunk.output is not None:\n                audio_data += chunk.output.audio.data\n\n        res = TTSResponse(\n            content=AudioBlock(\n                type=\"audio\",\n                source=Base64Source(\n                    type=\"base64\",\n                    data=audio_data,\n                    media_type=\"audio/pcm;rate=24000\",\n                ),\n            ),\n        )\n        return res\n\n    @staticmethod\n    async def _parse_into_async_generator(\n        response: Generator[MultiModalConversationResponse, None, None],\n    ) -> AsyncGenerator[TTSResponse, None]:\n        \"\"\"Parse the TTS response into an async generator.\n\n        Args:\n            response (`Generator[MultiModalConversationResponse, None, None]`):\n                The streaming response from DashScope TTS API.\n\n        Returns:\n            `AsyncGenerator[TTSResponse, None]`:\n                An async generator yielding TTSResponse objects.\n        \"\"\"\n        audio_data = \"\"\n        for chunk in response:\n            if chunk.output is not None:\n                audio = chunk.output.audio\n                if audio and audio.data:\n                    audio_data += audio.data\n                    yield TTSResponse(\n                        content=AudioBlock(\n                            type=\"audio\",\n                            source=Base64Source(\n                                type=\"base64\",\n                                data=audio_data,\n                                media_type=\"audio/pcm;rate=24000\",\n                            ),\n                        ),\n                        is_last=False,\n                    )\n        yield TTSResponse(\n            content=AudioBlock(\n                type=\"audio\",\n                source=Base64Source(\n                    type=\"base64\",\n                    data=audio_data,\n                    media_type=\"audio/pcm;rate=24000\",\n                ),\n            ),\n            is_last=True,\n        )\n"
  },
  {
    "path": "src/agentscope/tts/_gemini_tts_model.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Gemini TTS model implementation.\"\"\"\nimport base64\nfrom typing import TYPE_CHECKING, Any, Literal, AsyncGenerator, Iterator\n\nfrom ._tts_base import TTSModelBase\nfrom ._tts_response import TTSResponse\nfrom ..message import Msg, AudioBlock, Base64Source\nfrom ..types import JSONSerializableObject\n\nif TYPE_CHECKING:\n    from google.genai import Client\n    from google.genai.types import GenerateContentResponse\nelse:\n    Client = \"google.genai.Client\"\n    GenerateContentResponse = \"google.genai.types.GenerateContentResponse\"\n\n\nclass GeminiTTSModel(TTSModelBase):\n    \"\"\"Gemini TTS model implementation.\n    For more details, please see the `official document\n    <https://ai.google.dev/gemini-api/docs/speech-generation>`_.\n    \"\"\"\n\n    supports_streaming_input: bool = False\n    \"\"\"Whether the model supports streaming input.\"\"\"\n\n    def __init__(\n        self,\n        api_key: str,\n        model_name: str = \"gemini-2.5-flash-preview-tts\",\n        voice: Literal[\"Zephyr\", \"Kore\", \"Orus\", \"Autonoe\"] | str = \"Kore\",\n        stream: bool = True,\n        client_kwargs: dict[str, JSONSerializableObject] | None = None,\n        generate_kwargs: dict[str, JSONSerializableObject] | None = None,\n    ) -> None:\n        \"\"\"Initialize the Gemini TTS model.\n\n        .. note::\n            More details about the parameters, such as `model_name` and\n            `voice` can be found in the `official document\n            <https://ai.google.dev/gemini-api/docs/speech-generation>`_.\n\n        Args:\n            api_key (`str`):\n                The Gemini API key.\n            model_name (`str`, defaults to \"gemini-2.5-flash-preview-tts\"):\n                The TTS model name. Supported models are\n                \"gemini-2.5-flash-preview-tts\",\n                \"gemini-2.5-pro-preview-tts\", etc.\n            voice (`Literal[\"Zephyr\", \"Kore\", \"Orus\", \"Autonoe\"] | str`, \\\n             defaults to \"Kore\"):\n                The voice name to use. Supported voices are \"Zephyr\",\n                \"Kore\", \"Orus\", \"Autonoe\", etc.\n            stream (`bool`, defaults to `True`):\n                Whether to use streaming synthesis if supported by the model.\n            client_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n             optional):\n                The extra keyword arguments to initialize the Gemini client.\n            generate_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n             optional):\n               The extra keyword arguments used in Gemini API generation,\n               e.g. `temperature`, `seed`.\n        \"\"\"\n        super().__init__(model_name=model_name, stream=stream)\n\n        self.api_key = api_key\n        self.voice = voice\n\n        from google import genai\n\n        self._client = genai.Client(\n            api_key=self.api_key,\n            **(client_kwargs or {}),\n        )\n\n        self.generate_kwargs = generate_kwargs or {}\n\n    async def synthesize(\n        self,\n        msg: Msg | None = None,\n        **kwargs: Any,\n    ) -> TTSResponse | AsyncGenerator[TTSResponse, None]:\n        \"\"\"Append text to be synthesized and return TTS response.\n\n        Args:\n            msg (`Msg | None`, optional):\n                The message to be synthesized.\n            **kwargs (`Any`):\n                Additional keyword arguments to pass to the TTS API call.\n\n        Returns:\n            `TTSResponse | AsyncGenerator[TTSResponse, None]`:\n                The TTSResponse object in non-streaming mode, or an async\n                generator yielding TTSResponse objects in streaming mode.\n        \"\"\"\n        if msg is None:\n            return TTSResponse(content=None)\n\n        from google.genai import types\n\n        # Only call API for synthesis when last=True\n        text = msg.get_text_content()\n\n        # Prepare config\n        config = types.GenerateContentConfig(\n            response_modalities=[\"AUDIO\"],\n            speech_config=types.SpeechConfig(\n                voice_config=types.VoiceConfig(\n                    prebuilt_voice_config=types.PrebuiltVoiceConfig(\n                        voice_name=self.voice,\n                    ),\n                ),\n            ),\n            **self.generate_kwargs,\n            **kwargs,\n        )\n\n        # Prepare API kwargs\n        api_kwargs: dict[str, JSONSerializableObject] = {\n            \"model\": self.model_name,\n            \"contents\": text,\n            \"config\": config,\n        }\n\n        if self.stream:\n            response = self._client.models.generate_content_stream(\n                **api_kwargs,\n            )\n            return self._parse_into_async_generator(response)\n\n        # Call Gemini TTS API\n        response = self._client.models.generate_content(**api_kwargs)\n\n        # Extract audio data\n        if (\n            response.candidates\n            and response.candidates[0].content\n            and response.candidates[0].content.parts\n            and response.candidates[0].content.parts[0].inline_data\n        ):\n            audio_data = (\n                response.candidates[0].content.parts[0].inline_data.data\n            )\n            mime_type = (\n                response.candidates[0].content.parts[0].inline_data.mime_type\n            )\n            # Convert PCM data to base64\n            audio_base64 = base64.b64encode(audio_data).decode(\"utf-8\")\n\n            audio_block = AudioBlock(\n                type=\"audio\",\n                source=Base64Source(\n                    type=\"base64\",\n                    data=audio_base64,\n                    media_type=mime_type,\n                ),\n            )\n            return TTSResponse(content=audio_block)\n\n        else:\n            # Not the last chunk, return empty AudioBlock\n            return TTSResponse(\n                content=AudioBlock(\n                    type=\"audio\",\n                    source=Base64Source(\n                        type=\"base64\",\n                        data=\"\",\n                        media_type=\"audio/pcm;rate=24000\",\n                    ),\n                ),\n            )\n\n    @staticmethod\n    async def _parse_into_async_generator(\n        response: Iterator[GenerateContentResponse],\n    ) -> AsyncGenerator[TTSResponse, None]:\n        \"\"\"Parse the TTS response into an async generator.\n\n        Args:\n            response (`Iterator[GenerateContentResponse]`):\n                The streaming response from Gemini TTS API.\n\n        Returns:\n            `AsyncGenerator[TTSResponse, None]`:\n                An async generator yielding TTSResponse objects.\n        \"\"\"\n        audio_data = \"\"\n        for chunk in response:\n            chunk_audio_data = (\n                chunk.candidates[0].content.parts[0].inline_data.data\n            )\n            mime_type = (\n                chunk.candidates[0].content.parts[0].inline_data.mime_type\n            )\n            chunk_audio_base64 = base64.b64encode(chunk_audio_data).decode(\n                \"utf-8\",\n            )\n            audio_data += chunk_audio_base64\n            yield TTSResponse(\n                content=AudioBlock(\n                    type=\"audio\",\n                    source=Base64Source(\n                        type=\"base64\",\n                        data=audio_data,\n                        media_type=mime_type,\n                    ),\n                ),\n            )\n        yield TTSResponse(content=None)\n"
  },
  {
    "path": "src/agentscope/tts/_openai_tts_model.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"OpenAI TTS model implementation.\"\"\"\nimport base64\nfrom typing import TYPE_CHECKING, Any, Literal, AsyncGenerator\n\nfrom ._tts_base import TTSModelBase\nfrom ._tts_response import TTSResponse\nfrom ..message import Msg, AudioBlock, Base64Source\nfrom ..types import JSONSerializableObject\n\nif TYPE_CHECKING:\n    from openai import HttpxBinaryResponseContent\nelse:\n    HttpxBinaryResponseContent = \"openai.HttpxBinaryResponseContent\"\n\n\nclass OpenAITTSModel(TTSModelBase):\n    \"\"\"OpenAI TTS model implementation.\n    For more details, please see the `official document\n    <https://platform.openai.com/docs/api-reference/audio>`_.\n    \"\"\"\n\n    # This model does not support streaming input (requires complete text)\n    supports_streaming_input: bool = False\n\n    def __init__(\n        self,\n        api_key: str,\n        model_name: str = \"gpt-4o-mini-tts\",\n        voice: Literal[\"alloy\", \"ash\", \"ballad\", \"coral\"] | str = \"alloy\",\n        stream: bool = True,\n        client_kwargs: dict | None = None,\n        generate_kwargs: dict[str, JSONSerializableObject] | None = None,\n    ) -> None:\n        \"\"\"Initialize the OpenAI TTS model.\n\n        .. note::\n            More details about the parameters, such as `model_name` and\n            `voice` can be found in the `official document\n            <https://platform.openai.com/docs/api-reference/audio/createSpeech>`_.\n\n        Args:\n            api_key (`str`):\n                The OpenAI API key.\n            model_name (`str`,  defaults to \"gpt-4o-mini-tts\"):\n                The TTS model name. Supported models are \"gpt-4o-mini-tts\",\n                \"tts-1\", etc.\n            voice (`Literal[\"alloy\", \"ash\", \"ballad\", \"coral\"] | str `,\n             defaults to \"alloy\"):\n                The voice to use. Supported voices are \"alloy\", \"ash\",\n                \"ballad\", \"coral\", etc.\n            client_kwargs (`dict | None`, default `None`):\n                The extra keyword arguments to initialize the OpenAI client.\n            generate_kwargs (`dict[str, JSONSerializableObject] | None`, \\\n             optional):\n               The extra keyword arguments used in OpenAI API generation,\n               e.g. `temperature`, `seed`.\n        \"\"\"\n        super().__init__(model_name=model_name, stream=stream)\n\n        self.api_key = api_key\n        self.voice = voice\n        self.stream = stream\n\n        import openai\n\n        self._client = openai.AsyncOpenAI(\n            api_key=self.api_key,\n            **client_kwargs or {},\n        )\n\n        # Text buffer for each message to accumulate text before synthesis\n        # Key is msg.id, value is the accumulated text\n        self.generate_kwargs = generate_kwargs or {}\n\n    async def synthesize(\n        self,\n        msg: Msg | None = None,\n        **kwargs: Any,\n    ) -> TTSResponse | AsyncGenerator[TTSResponse, None]:\n        \"\"\"Append text to be synthesized and return TTS response.\n\n        Args:\n            msg (`Msg | None`, optional):\n                The message to be synthesized.\n            **kwargs (`Any`):\n                Additional keyword arguments to pass to the TTS API call.\n\n        Returns:\n            `TTSResponse | AsyncGenerator[TTSResponse, None]`:\n                The TTSResponse object in non-streaming mode, or an async\n                generator yielding TTSResponse objects in streaming mode.\n        \"\"\"\n        if msg is None:\n            return TTSResponse(content=None)\n\n        text = msg.get_text_content()\n\n        if text:\n            if self.stream:\n                response = (\n                    self._client.audio.speech.with_streaming_response.create(\n                        model=self.model_name,\n                        voice=self.voice,\n                        input=text,\n                        response_format=\"pcm\",\n                        **self.generate_kwargs,\n                        **kwargs,\n                    )\n                )\n                return self._parse_into_async_generator(response)\n\n            response = await self._client.audio.speech.create(\n                model=self.model_name,\n                voice=self.voice,\n                input=text,\n                response_format=\"pcm\",\n                **self.generate_kwargs,\n                **kwargs,\n            )\n\n            audio_base64 = base64.b64encode(response.content).decode(\n                \"utf-8\",\n            )\n            return TTSResponse(\n                content=AudioBlock(\n                    type=\"audio\",\n                    source=Base64Source(\n                        type=\"base64\",\n                        data=audio_base64,\n                        media_type=\"audio/pcm\",\n                    ),\n                ),\n            )\n\n        return TTSResponse(content=None)\n\n    @staticmethod\n    async def _parse_into_async_generator(\n        response: HttpxBinaryResponseContent,\n    ) -> AsyncGenerator[TTSResponse, None]:\n        \"\"\"Parse the streaming response into an async generator of TTSResponse.\n\n        Args:\n            response (`HttpxBinaryResponseContent`):\n                The streaming response from OpenAI TTS API.\n\n        Yields:\n            `TTSResponse`:\n                The TTSResponse object containing audio blocks.\n        \"\"\"\n        # Iterate through the streaming response chunks\n        async with response as stream:\n            audio_base64 = \"\"\n            async for chunk in stream.iter_bytes():\n                if chunk:\n                    # Encode chunk to base64\n                    audio_base64 = base64.b64encode(chunk).decode(\"utf-8\")\n\n                    # Create TTSResponse for this chunk\n                    yield TTSResponse(\n                        content=AudioBlock(\n                            type=\"audio\",\n                            source=Base64Source(\n                                type=\"base64\",\n                                data=audio_base64,\n                                media_type=\"audio/pcm\",\n                            ),\n                        ),\n                        is_last=False,  # Not the last chunk yet\n                    )\n\n            # Yield final response with is_last=True to indicate end of stream\n            yield TTSResponse(\n                content=AudioBlock(\n                    type=\"audio\",\n                    source=Base64Source(\n                        type=\"base64\",\n                        data=audio_base64,\n                        media_type=\"audio/pcm\",\n                    ),\n                ),\n                is_last=True,\n            )\n"
  },
  {
    "path": "src/agentscope/tts/_tts_base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The TTS model base class.\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom typing import Any, AsyncGenerator\n\nfrom agentscope.message import Msg\n\nfrom ._tts_response import TTSResponse\n\n\nclass TTSModelBase(ABC):\n    \"\"\"Base class for TTS models in AgentScope.\n\n    This base class provides general abstraction for both realtime and\n    non-realtime TTS models (depending on whether streaming input is\n    supported).\n\n    For non-realtime TTS models, the `synthesize` method is used to\n    synthesize speech from the input text. You only need to implement the\n    `_call_api` method to handle the TTS API calls.\n\n    For realtime TTS models, its lifecycle is managed via the async context\n    manager or calling `connect` and `close` methods. The `push` method will\n    append text chunks and return the received TTS response, while the\n    `synthesize` method will block until the full speech is synthesized.\n    You need to implement the `connect`, `close`, and `_call_api` methods\n    to handle the TTS API calls and resource management.\n    \"\"\"\n\n    supports_streaming_input: bool = False\n    \"\"\"If the TTS model class supports streaming input.\"\"\"\n\n    model_name: str\n    \"\"\"The name of the TTS model.\"\"\"\n\n    stream: bool\n    \"\"\"Whether to use streaming synthesis if supported by the model.\"\"\"\n\n    def __init__(self, model_name: str, stream: bool) -> None:\n        \"\"\"Initialize the TTS model base class.\n\n        Args:\n            model_name (`str`):\n                The name of the TTS model\n            stream  (`bool`):\n                Whether to use streaming synthesis if supported by the model.\n        \"\"\"\n        self.model_name = model_name\n        self.stream = stream\n\n    async def __aenter__(self) -> \"TTSModelBase\":\n        \"\"\"Enter the async context manager and initialize resources if\n        needed.\"\"\"\n        if self.supports_streaming_input:\n            await self.connect()\n\n        return self\n\n    async def __aexit__(\n        self,\n        exc_type: Any,\n        exc_value: Any,\n        traceback: Any,\n    ) -> None:\n        \"\"\"Exit the async context manager and clean up resources if needed.\"\"\"\n        if self.supports_streaming_input:\n            await self.close()\n\n    async def connect(self) -> None:\n        \"\"\"Connect to the TTS model and initialize resources. For non-realtime\n        TTS models, leave this method empty.\n\n        .. note:: Only needs to be implemented for realtime TTS models.\n\n        \"\"\"\n        raise NotImplementedError(\n            f\"The connect method is not implemented for \"\n            f\"{self.__class__.__name__} class.\",\n        )\n\n    async def close(self) -> None:\n        \"\"\"Close the connection to the TTS model and clean up resources. For\n        non-realtime TTS models, leave this method empty.\n\n        .. note:: Only needs to be implemented for realtime TTS models.\n\n        \"\"\"\n        raise NotImplementedError(\n            \"The close method is not implemented for \"\n            f\"{self.__class__.__name__} class.\",\n        )\n\n    async def push(\n        self,\n        msg: Msg,\n        **kwargs: Any,\n    ) -> TTSResponse:\n        \"\"\"Append text to be synthesized and return the received TTS response.\n        Note this method is non-blocking, and maybe return an empty response\n        if no audio is received yet.\n\n        To receive all the synthesized speech, call the `synthesize` method\n        after pushing all the text chunks.\n\n        .. note:: Only needs to be implemented for realtime TTS models.\n\n        Args:\n            msg (`Msg`):\n                The message to be synthesized. The `msg.id` identifies the\n                streaming input request.\n            **kwargs (`Any`):\n                Additional keyword arguments to pass to the TTS API call.\n\n        Returns:\n            `TTSResponse`:\n                The TTSResponse containing audio block.\n        \"\"\"\n        raise NotImplementedError(\n            \"The push method is not implemented for \"\n            f\"{self.__class__.__name__} class.\",\n        )\n\n    @abstractmethod\n    async def synthesize(\n        self,\n        msg: Msg | None = None,\n        **kwargs: Any,\n    ) -> TTSResponse | AsyncGenerator[TTSResponse, None]:\n        \"\"\"Synthesize speech from the appended text. Different from the `push`\n        method, this method will block until the full speech is synthesized.\n\n        Args:\n            msg (`Msg | None`, defaults to `None`):\n                The message to be synthesized. If `None`, this method will\n                wait for all previously pushed text to be synthesized, and\n                return the last synthesized TTSResponse.\n\n        Returns:\n            `TTSResponse | AsyncGenerator[TTSResponse, None]`:\n                The TTSResponse containing audio blocks, or an async generator\n                yielding TTSResponse objects in streaming mode.\n        \"\"\"\n"
  },
  {
    "path": "src/agentscope/tts/_tts_response.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The TTS response module.\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import Literal\n\nfrom .._utils._common import _get_timestamp\nfrom .._utils._mixin import DictMixin\nfrom ..message import AudioBlock\nfrom ..types import JSONSerializableObject\n\n\n@dataclass\nclass TTSUsage(DictMixin):\n    \"\"\"The usage of a TTS model API invocation.\"\"\"\n\n    input_tokens: int\n    \"\"\"The number of input tokens.\"\"\"\n\n    output_tokens: int\n    \"\"\"The number of output tokens.\"\"\"\n\n    time: float\n    \"\"\"The time used in seconds.\"\"\"\n\n    type: Literal[\"tts\"] = field(default_factory=lambda: \"tts\")\n    \"\"\"The type of the usage, must be `tts`.\"\"\"\n\n\n@dataclass\nclass TTSResponse(DictMixin):\n    \"\"\"The response of TTS models.\"\"\"\n\n    content: AudioBlock | None\n    \"\"\"The content of the TTS response, which contains audio block\"\"\"\n\n    id: str = field(default_factory=lambda: _get_timestamp(True))\n    \"\"\"The unique identifier of the response.\"\"\"\n\n    created_at: str = field(default_factory=_get_timestamp)\n    \"\"\"When the response was created.\"\"\"\n\n    type: Literal[\"tts\"] = field(default_factory=lambda: \"tts\")\n    \"\"\"The type of the response, which is always 'tts'.\"\"\"\n\n    usage: TTSUsage | None = field(default_factory=lambda: None)\n    \"\"\"The usage information of the TTS response, if available.\"\"\"\n\n    metadata: dict[str, JSONSerializableObject] | None = field(\n        default_factory=lambda: None,\n    )\n    \"\"\"The metadata of the TTS response.\"\"\"\n\n    is_last: bool = True\n    \"\"\"Whether this is the last response in a stream of TTS responses.\"\"\"\n"
  },
  {
    "path": "src/agentscope/tts/_utils.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Utility classes for DashScope CosyVoice TTS models.\"\"\"\n\nimport base64\nimport threading\nfrom typing import TYPE_CHECKING, AsyncGenerator, Any\n\nfrom ._tts_response import TTSResponse\nfrom .._logging import logger\nfrom ..message import AudioBlock, Base64Source\n\nif TYPE_CHECKING:\n    from dashscope.audio.tts_v2 import ResultCallback\nelse:\n    ResultCallback = \"dashscope.audio.tts_v2.ResultCallback\"\n\n\ndef _get_cosyvoice_callback_class() -> type[\"ResultCallback\"]:\n    \"\"\"Get the callback class for CosyVoice TTS streaming audio output.\n\n    This callback handles audio data accumulation with proper PCM and base64\n    alignment. It encodes audio in chunks of 6 bytes (LCM of 2 and 3) to\n    ensure both PCM alignment (2 bytes per sample) and base64 alignment\n    (3 bytes per encoded unit).\n\n    Returns:\n        The callback class for CosyVoice TTS.\n    \"\"\"\n    from dashscope.audio.tts_v2 import ResultCallback\n\n    class _CosyVoiceTTSCallback(ResultCallback):\n        \"\"\"CosyVoice TTS callback for streaming audio output.\"\"\"\n\n        def __init__(self) -> None:\n            \"\"\"Initialize the CosyVoice TTS callback.\"\"\"\n            super().__init__()\n\n            # The event that will be set when a new audio chunk is received\n            self.chunk_event = threading.Event()\n            # The event that will be set when the TTS synthesis is finished\n            self.finish_event = threading.Event()\n            # Accumulate raw bytes\n            self._audio_bytes: bytes = b\"\"\n            # Accumulated base64 string (boundary-aligned)\n            self._audio_base64: str = \"\"\n            # Last encoded byte position (must be multiple of 6 for alignment)\n            self._last_encoded_pos: int = 0\n\n        def on_open(self) -> None:\n            \"\"\"Called when the WebSocket connection is opened.\"\"\"\n            self._audio_bytes = b\"\"\n            self._audio_base64 = \"\"\n            self._last_encoded_pos = 0\n            self.finish_event.clear()\n\n        def on_data(self, data: bytes) -> None:\n            \"\"\"Called when data is received from the WebSocket connection.\n\n            Args:\n                data (`bytes`):\n                    The data received from the WebSocket connection.\n            \"\"\"\n            if data:\n                self._audio_bytes += data\n                # Encode in chunks of 6 bytes (LCM of 2 and 3)\n                # This ensures both PCM alignment (2 bytes) and\n                # base64 alignment (3 bytes)\n                aligned_len = (len(self._audio_bytes) // 6) * 6\n                if aligned_len > self._last_encoded_pos:\n                    new_chunk = self._audio_bytes[\n                        self._last_encoded_pos : aligned_len\n                    ]\n                    self._audio_base64 += base64.b64encode(new_chunk).decode()\n                    self._last_encoded_pos = aligned_len\n\n                # Signal that a new audio chunk is available\n                if not self.chunk_event.is_set():\n                    self.chunk_event.set()\n\n        def on_close(self) -> None:\n            \"\"\"Called when the WebSocket connection is closed.\"\"\"\n            # Encode any remaining bytes\n            if len(self._audio_bytes) > self._last_encoded_pos:\n                remaining = self._audio_bytes[self._last_encoded_pos :]\n                self._audio_base64 += base64.b64encode(remaining).decode()\n                self._last_encoded_pos = len(self._audio_bytes)\n\n            # Unblock waiting operations to prevent deadlock\n            self.finish_event.set()\n            self.chunk_event.set()\n\n        def on_error(self, message: Any) -> None:\n            \"\"\"Called when an error occurs.\"\"\"\n            logger.error(message)\n\n            # Unblock waiting operations to prevent deadlock\n            self.finish_event.set()\n            self.chunk_event.set()\n\n        async def get_audio_data(self, block: bool = True) -> TTSResponse:\n            \"\"\"Get the current accumulated audio data as base64 string.\n\n            Args:\n                block (`bool`, defaults to `True`):\n                    Whether to block until synthesis is finished.\n\n            Returns:\n                `TTSResponse`:\n                    The TTSResponse containing base64-encoded audio data.\n            \"\"\"\n            # Block until synthesis is finished\n            if block:\n                self.finish_event.wait()\n\n            # Return the accumulated audio data\n            if self._audio_base64:\n                return TTSResponse(\n                    content=AudioBlock(\n                        type=\"audio\",\n                        source=Base64Source(\n                            type=\"base64\",\n                            data=self._audio_base64,\n                            media_type=\"audio/pcm;rate=24000\",\n                        ),\n                    ),\n                )\n\n            # Reset for next tts request\n            await self._reset()\n\n            # Return empty response if no audio data\n            return TTSResponse(content=None)\n\n        async def get_audio_chunk(self) -> AsyncGenerator[TTSResponse, None]:\n            \"\"\"Get the audio data chunk as an async generator of TTSResponse.\n\n            Returns accumulated base64 string. The agent code uses string\n            slicing to get the delta, so we must ensure boundary alignment.\n\n            Returns:\n                `AsyncGenerator[TTSResponse, None]`:\n                    The async generator yielding TTSResponse with audio chunks.\n            \"\"\"\n            while True:\n                if self.finish_event.is_set():\n                    # Yield final chunk with all accumulated data\n                    yield TTSResponse(\n                        content=AudioBlock(\n                            type=\"audio\",\n                            source=Base64Source(\n                                type=\"base64\",\n                                data=self._audio_base64,\n                                media_type=\"audio/pcm;rate=24000\",\n                            ),\n                        ),\n                        is_last=True,\n                    )\n\n                    # Reset for next tts request\n                    await self._reset()\n\n                    break\n\n                if self.chunk_event.is_set():\n                    # Clear the event for next chunk\n                    self.chunk_event.clear()\n                else:\n                    # Wait for the next chunk\n                    self.chunk_event.wait()\n\n                # Yield current accumulated data\n                if self._audio_base64:\n                    yield TTSResponse(\n                        content=AudioBlock(\n                            type=\"audio\",\n                            source=Base64Source(\n                                type=\"base64\",\n                                data=self._audio_base64,\n                                media_type=\"audio/pcm;rate=24000\",\n                            ),\n                        ),\n                        is_last=False,\n                    )\n\n        async def _reset(self) -> None:\n            \"\"\"Reset the callback state for a new TTS request.\"\"\"\n            self.finish_event.clear()\n            self.chunk_event.clear()\n            self._audio_bytes = b\"\"\n            self._audio_base64 = \"\"\n            self._last_encoded_pos = 0\n\n        def has_audio_data(self) -> bool:\n            \"\"\"Check if audio data has been received.\"\"\"\n            return bool(self._audio_bytes)\n\n    return _CosyVoiceTTSCallback\n"
  },
  {
    "path": "src/agentscope/tune/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"This module has been deprecated and renamed to 'agentscope.tuner'.\"\"\"\n\nraise ImportError(\n    \"The 'agentscope.tune' module has been renamed to 'agentscope.tuner'. \"\n    \"Please update your imports: 'from agentscope.tuner import ...'\",\n)\n"
  },
  {
    "path": "src/agentscope/tuner/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The learning module of AgentScope, including RL and SFT.\"\"\"\n\nfrom ._tune import tune\nfrom ._dataset import DatasetConfig\nfrom ._judge import JudgeType, JudgeOutput\nfrom ._workflow import WorkflowType, WorkflowOutput\nfrom ._algorithm import AlgorithmConfig\nfrom ._model import TunerModelConfig, TinkerConfig\nfrom ._config import check_judge_function, check_workflow_function\n\n\n__all__ = [\n    \"tune\",\n    \"AlgorithmConfig\",\n    \"WorkflowType\",\n    \"WorkflowOutput\",\n    \"JudgeType\",\n    \"JudgeOutput\",\n    \"DatasetConfig\",\n    \"TunerModelConfig\",\n    \"TinkerConfig\",\n    \"check_workflow_function\",\n    \"check_judge_function\",\n]\n"
  },
  {
    "path": "src/agentscope/tuner/_algorithm.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"AlgorithmConfig definition for tuner.\"\"\"\n\nfrom pydantic import BaseModel, Field\n\n\nclass AlgorithmConfig(BaseModel):\n    \"\"\"Algorithm configuration for tuning.\"\"\"\n\n    algorithm_type: str = Field(\n        description=(\n            \"The tuning algorithm type \"\n            \"e.g., 'multi_step_grpo', 'sft'.\"\n            \"Please refer to https://github.com/agentscope-ai/Trinity-RFT\"\n            \"for all supported algorithms. We recommend 'multi_step_grpo'\"\n            \"for most agent tuning scenarios.\"\n        ),\n        default=\"multi_step_grpo\",\n    )\n    learning_rate: float = Field(\n        description=\"The learning rate for the algorithm.\",\n        default=1e-6,\n    )\n    group_size: int = Field(\n        description=(\n            \"The group size for algorithms \"\n            \"requiring group rollout, e.g., GRPO.\"\n        ),\n        default=8,\n    )\n    batch_size: int = Field(\n        description=\"The batch size of each training step.\",\n        default=32,\n    )\n    save_interval_steps: int = Field(\n        description=\"The interval steps to save the model.\",\n        default=100,\n    )\n    eval_interval_steps: int = Field(\n        description=\"The interval steps to evaluate the model.\",\n        default=100,\n    )\n"
  },
  {
    "path": "src/agentscope/tuner/_config.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Configuration conversion for tuner.\"\"\"\nfrom typing import Any, Callable, List, Tuple\nfrom datetime import datetime\nimport inspect\n\nfrom ._workflow import WorkflowType\nfrom ._judge import JudgeType\nfrom ._model import TunerModelConfig\nfrom ._dataset import DatasetConfig\nfrom ._algorithm import AlgorithmConfig\n\n\ndef _set_if_not_none(obj: Any, field: str, value: Any) -> None:\n    \"\"\"Set the field of obj to value if value is not None.\"\"\"\n    if value is not None:\n        setattr(obj, field, value)\n\n\ndef _to_trinity_config(\n    *,\n    config_path: str | None = None,\n    workflow_func: WorkflowType | None = None,\n    judge_func: JudgeType | None = None,\n    model: TunerModelConfig | None = None,\n    auxiliary_models: dict[str, TunerModelConfig] | None = None,\n    train_dataset: DatasetConfig | None = None,\n    eval_dataset: DatasetConfig | None = None,\n    algorithm: AlgorithmConfig | None = None,\n    project_name: str | None = None,\n    experiment_name: str | None = None,\n    monitor_type: str | None = None,\n) -> Any:\n    \"\"\"Convert to Trinity-RFT compatible configuration.\"\"\"\n    from trinity.common.config import (\n        Config,\n        TasksetConfig,\n        InferenceModelConfig,\n        TinkerConfig,\n    )\n\n    config, auto_config = _load_config_from_path_or_default(config_path)\n    assert isinstance(config, Config), \"Loaded config is not valid.\"\n\n    _set_if_not_none(config, \"project\", project_name)\n    if experiment_name is None and auto_config:\n        config.name = \"Experiment-\" + datetime.now().strftime(\n            \"%Y%m%d%H%M%S\",\n        )\n\n    _set_if_not_none(config, \"monitor\", monitor_type)\n\n    workflow_name = \"agentscope_workflow_adapter_v1\"\n    if train_dataset is not None:\n        if config.buffer.explorer_input.taskset is None:\n            config.buffer.explorer_input.taskset = TasksetConfig(\n                name=\"train_taskset\",\n                path=train_dataset.path,\n                split=train_dataset.split,\n                subset_name=train_dataset.name,\n            )\n        else:\n            config.buffer.explorer_input.taskset.path = train_dataset.path\n            config.buffer.explorer_input.taskset.split = train_dataset.split\n            config.buffer.explorer_input.taskset.subset_name = (\n                train_dataset.name\n            )\n        config.buffer.total_epochs = train_dataset.total_epochs\n        config.buffer.total_steps = train_dataset.total_steps\n    config.buffer.explorer_input.taskset.default_workflow_type = workflow_name\n    config.buffer.explorer_input.default_workflow_type = workflow_name\n    workflow_args = {\n        \"workflow_func\": workflow_func,\n    }\n    if judge_func is not None:\n        workflow_args[\"judge_func\"] = judge_func\n\n    config.buffer.explorer_input.taskset.workflow_args.update(workflow_args)\n\n    if model is not None:\n        model_config = model.get_config()\n        config.model.model_path = model_config[\"model_path\"]\n        config.model.max_model_len = model_config[\"max_model_len\"]\n        config.model.max_response_tokens = model.max_tokens\n        config.explorer.rollout_model = InferenceModelConfig(\n            **model.get_config(),\n        )\n        config.explorer.rollout_model.enable_history = True\n        if model.tinker_config is not None:\n            config.model.tinker = TinkerConfig(\n                **model.tinker_config.get_config(),\n            )\n            config.model.tinker.enable = True\n    if auxiliary_models is not None:\n        for name, aux_chat_model in auxiliary_models.items():\n            model_config = InferenceModelConfig(\n                **aux_chat_model.get_config(),\n            )\n            model_config.name = name\n            config.explorer.auxiliary_models.append(\n                model_config,\n            )\n    if eval_dataset is not None:\n        config.buffer.explorer_input.eval_tasksets.append(\n            TasksetConfig(\n                name=\"eval_taskset\",\n                path=eval_dataset.path,\n                split=eval_dataset.split,\n                subset_name=eval_dataset.name,\n            ),\n        )\n    for eval_taskset in config.buffer.explorer_input.eval_tasksets:\n        eval_taskset.workflow_args.update(workflow_args)\n    if algorithm is not None:\n        config.algorithm.algorithm_type = algorithm.algorithm_type\n        config.algorithm.repeat_times = algorithm.group_size\n        config.algorithm.optimizer.lr = algorithm.learning_rate\n        config.buffer.batch_size = algorithm.batch_size\n        config.trainer.save_interval = algorithm.save_interval_steps\n        config.explorer.eval_interval = algorithm.eval_interval_steps\n    return config\n\n\ndef _load_config_from_path_or_default(\n    config_path: str | None,\n) -> Tuple[Any, bool]:\n    \"\"\"Load configuration from the given path or default template.\n\n    Args:\n        config_path (`str | None`): The path to the configuration file.\n    Returns:\n        `Tuple[Any, bool]`: The loaded configuration and a boolean\n            indicating whether the default template was used.\n    \"\"\"\n    from trinity.common.config import (\n        Config,\n        load_config,\n    )\n    import tempfile\n    import yaml\n\n    template_used = False\n    if config_path is None:\n        default_config = {\n            \"project\": \"AgentScope\",\n            \"name\": \"Experiment\",\n            \"checkpoint_root_dir\": \"./checkpoints\",\n            \"algorithm\": {\n                \"algorithm_type\": \"multi_step_grpo\",\n            },\n            \"buffer\": {\n                \"total_epochs\": 1,\n            },\n            \"explorer\": {\n                \"runner_per_model\": 16,\n                \"max_timeout\": 3600,\n                \"max_repeat_times_per_runner\": 1,\n            },\n            \"synchronizer\": {\n                \"sync_style\": \"dynamic_by_explorer\",\n                \"sync_method\": \"nccl\",\n                \"sync_interval\": 1,\n                \"sync_timeout\": 7200,\n            },\n            \"trainer\": {\n                \"save_interval\": 100,\n            },\n            \"monitor\": {\n                \"monitor_type\": \"tensorboard\",\n            },\n        }\n        with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".yaml\") as tmp:\n            yaml.dump(default_config, tmp)\n            tmp.flush()\n            config = load_config(tmp.name)\n        template_used = True\n    else:\n        config = load_config(config_path)\n\n    assert isinstance(config, Config), \"Loaded config is not valid.\"\n    return config, template_used\n\n\ndef check_workflow_function(\n    func: Callable,\n) -> None:\n    \"\"\"Check if the given function is a valid WorkflowType.\n\n    Args:\n        func (Callable): The function to check.\n    \"\"\"\n    essential_params = [\"task\", \"model\"]\n    optional_params = [\"auxiliary_models\", \"logger\"]\n    _check_function_signature(\n        func,\n        essential_params,\n        optional_params,\n    )\n\n\ndef check_judge_function(\n    func: Callable,\n) -> None:\n    \"\"\"Check if the given function is a valid JudgeType.\n\n    Args:\n        func (Callable): The function to check.\n    \"\"\"\n    essential_params = [\"task\", \"response\"]\n    optional_params = [\"auxiliary_models\", \"logger\"]\n    _check_function_signature(\n        func,\n        essential_params,\n        optional_params,\n    )\n\n\ndef _check_function_signature(\n    func: Callable,\n    essential_params: List[str],\n    optional_params: List[str] | None = None,\n) -> None:\n    \"\"\"\n    Check if the given function has the required signature.\n\n    Args:\n        func (`Callable`): The function to check.\n        essential_params (`List[str]`): List of essential parameter names\n            that must be present in the function.\n        optional_params (`List[str] | None`): List of optional parameter names\n            that can be present in the function.\n    \"\"\"\n    if optional_params is None:\n        optional_params = []\n\n    sig = inspect.signature(func)\n    actual_params = []\n\n    for param_name, param in sig.parameters.items():\n        # *args and **kwargs are not allowed\n        if param.kind == inspect.Parameter.VAR_POSITIONAL:\n            raise ValueError(f\"*args parameter is not allowed: *{param_name}\")\n        if param.kind == inspect.Parameter.VAR_KEYWORD:\n            raise ValueError(\n                f\"**kwargs parameter is not allowed: **{param_name}\",\n            )\n        actual_params.append(param_name)\n\n    # Convert to sets for easier comparison\n    actual_params_set = set(actual_params)\n    essential_params_set = set(essential_params)\n    optional_params_set = set(optional_params)\n    allowed_params_set = essential_params_set | optional_params_set\n\n    # Check 1: All essential parameters are present\n    missing_essential = essential_params_set - actual_params_set\n    if missing_essential:\n        raise ValueError(\n            f\"Missing essential parameters: {sorted(missing_essential)}\",\n        )\n\n    # Check 2: Whether there are disallowed parameters\n    extra_params = actual_params_set - allowed_params_set\n    if extra_params:\n        raise ValueError(\n            f\"Contains disallowed parameters: {sorted(extra_params)}\",\n        )\n"
  },
  {
    "path": "src/agentscope/tuner/_dataset.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"DatasetConfig definition for tuner.\"\"\"\nfrom itertools import islice\nfrom typing import List\nfrom pydantic import BaseModel, Field\n\n\nclass DatasetConfig(BaseModel):\n    \"\"\"Dataset configuration for tuning.\n    Compatible with huggingface dataset format.\n    Agentscope will load the dataset from the given path using\n    `datasets.load_dataset`.\n    \"\"\"\n\n    path: str = Field(\n        description=\"Path to your dataset.\",\n    )\n    name: str | None = Field(\n        description=\"The name of the dataset configuration.\",\n        default=None,\n    )\n    split: str | None = Field(\n        description=\"The dataset split to use.\",\n        default=\"train\",\n    )\n    total_epochs: int = Field(\n        description=\"Total number of epochs to run.\",\n        default=1,\n    )\n    total_steps: int | None = Field(\n        description=(\n            \"Total number of steps to run. \"\n            \"If set, it will override total_epochs.\"\n        ),\n        default=None,\n    )\n\n    def preview(self, n: int = 5) -> List:\n        \"\"\"Preview the dataset information.\n\n        Args:\n            n (`int`): Number of samples to preview. Defaults to 5.\n        \"\"\"\n        try:\n            from datasets import load_dataset\n        except ImportError as e:\n            raise ImportError(\n                \"The `datasets` library is not installed. \"\n                \"Please install it with `pip install datasets`.\",\n            ) from e\n        import json\n\n        ds = load_dataset(\n            path=self.path,\n            name=self.name,\n            split=self.split,\n            streaming=True,\n        )\n        samples = list(islice(ds, n))\n        print(json.dumps(samples, indent=2, ensure_ascii=False))\n        return samples\n"
  },
  {
    "path": "src/agentscope/tuner/_judge.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The judge module for tuner.\"\"\"\nfrom typing import Any, Callable, Dict, Awaitable\nfrom logging import Logger\nfrom pydantic import BaseModel, Field\nfrom ..model import ChatModelBase\n\n\nclass JudgeOutput(BaseModel):\n    \"\"\"The output of a judge function.\"\"\"\n\n    reward: float = Field(\n        description=\"The reward value assigned by the judge function.\",\n    )\n\n    metrics: Dict[str, float] | None = Field(\n        description=\"Metrics from the judge function.\",\n        default=None,\n    )\n\n\nJudgeType = Callable[\n    [Dict, Any, Dict[str, ChatModelBase] | None, Logger | None],\n    Awaitable[JudgeOutput],\n]\n# A judge function type for tuning.\n\n# Args:\n#     task (`Dict`):\n#         The task information for the corresponding workflow.\n#     response (`Any`):\n#         The response field of the WorkflowOutput generated by the\n#         corresponding workflow.\n#     auxiliary_models (`Dict[str, ChatModelBase] | None`, optional):\n#         A dictionary of additional chat models available for LLM-as-a-Judge\n#         usage. The keys are model names, and the values are the corresponding\n#         `ChatModelBase` instances.\n#     logger (`Logger | None`, optional):\n#         An optional logger for logging information during the judge\n#         execution.\n# Returns:\n#     `JudgeOutput`:\n#         The reward value assigned by the judge function along with optional\n#         metrics.\n"
  },
  {
    "path": "src/agentscope/tuner/_model.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"TunerModelConfig definition.\"\"\"\nfrom __future__ import annotations\nfrom typing import Dict, Any\nfrom pydantic import BaseModel, Field\n\n\nclass TunerModelConfig(BaseModel):\n    \"\"\"Model configuration for tuning.\"\"\"\n\n    model_path: str = Field(\n        description=\"The path to the model checkpoint.\",\n    )\n\n    max_model_len: int = Field(\n        description=(\n            \"The maximum length of the model, including context\"\n            \" and generated tokens.\"\n        ),\n    )\n\n    temperature: float = Field(\n        description=\"Sampling temperature.\",\n        default=1.0,\n    )\n\n    top_p: float = Field(\n        description=\"Top-p sampling parameter.\",\n        default=1.0,\n    )\n\n    max_tokens: int = Field(\n        description=\"Maximum tokens for generation.\",\n        default=8192,\n    )\n\n    enable_thinking: bool | None = Field(\n        description=(\n            \"Whether to enable thinking capability. \"\n            \"Only applicable for Qwen3 series models.\"\n        ),\n        default=None,\n    )\n\n    tensor_parallel_size: int = Field(\n        description=\"The tensor parallel size for model inference.\",\n        default=1,\n    )\n\n    inference_engine_num: int = Field(\n        description=\"The number of engines for model inference.\",\n        default=1,\n    )\n\n    tool_call_parser: str = Field(\n        description=(\n            \"The tool call parser to use. The default setting \"\n            \"is for Qwen3 series models.\"\n        ),\n        default=\"hermes\",\n    )\n\n    reasoning_parser: str = Field(\n        description=(\n            \"The reasoning parser to use. The default \"\n            \"setting is for Qwen3 series models.\"\n        ),\n        default=\"deepseek_r1\",\n    )\n\n    tinker_config: TinkerConfig | None = Field(\n        description=(\n            \"The configuration for Tinker. \" \"If None, Tinker is not used.\"\n        ),\n        default=None,\n    )\n\n    def get_config(self) -> Dict[str, Any]:\n        \"\"\"Get the model configuration.\n\n        Returns:\n            `Dict[str, Any]`: The model configuration dictionary.\n        \"\"\"\n        return {\n            \"model_path\": self.model_path,\n            \"max_model_len\": self.max_model_len,\n            \"tensor_parallel_size\": self.tensor_parallel_size,\n            \"engine_num\": self.inference_engine_num,\n            \"tool_call_parser\": self.tool_call_parser,\n            \"reasoning_parser\": self.reasoning_parser,\n            \"enable_openai_api\": True,\n            \"enable_auto_tool_choice\": True,\n        }\n\n\nclass TinkerConfig(BaseModel):\n    \"\"\"Model configuration for Tinker.\"\"\"\n\n    rank: int = Field(\n        description=\"The LoRA rank of the Tinker model.\",\n        default=16,\n    )\n\n    seed: int | None = Field(\n        description=(\n            \"The seed for initializing LoRA weights in the model. \"\n            \"If None, weights are initialized randomly.\"\n        ),\n        default=None,\n    )\n\n    train_mlp: bool = Field(\n        description=\"Whether to add LoRA to the MLP layers.\",\n        default=True,\n    )\n\n    train_attn: bool = Field(\n        description=\"Whether to add LoRA to the attention layers.\",\n        default=True,\n    )\n\n    train_unembed: bool = Field(\n        description=\"Whether to add LoRA to the unembedding layer.\",\n        default=True,\n    )\n\n    base_url: str | None = Field(\n        description=(\n            \"The base URL for Tinker services. If None, the default \"\n            \"service URL is used.\"\n        ),\n        default=None,\n    )\n\n    def get_config(self) -> Dict[str, Any]:\n        \"\"\"Get the Tinker model configuration.\n\n        Returns:\n            `Dict[str, Any]`: The Tinker model configuration dictionary.\n        \"\"\"\n        return {\n            \"rank\": self.rank,\n            \"seed\": self.seed,\n            \"train_mlp\": self.train_mlp,\n            \"train_attn\": self.train_attn,\n            \"train_unembed\": self.train_unembed,\n            \"base_url\": self.base_url,\n        }\n"
  },
  {
    "path": "src/agentscope/tuner/_tune.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The main entry point for agent learning.\"\"\"\nimport os\nfrom ._workflow import WorkflowType\nfrom ._judge import JudgeType\nfrom ._model import TunerModelConfig\nfrom ._dataset import DatasetConfig\nfrom ._config import (\n    _to_trinity_config,\n    check_judge_function,\n    check_workflow_function,\n)\nfrom ._algorithm import AlgorithmConfig\n\n\ndef tune(\n    *,\n    workflow_func: WorkflowType,\n    judge_func: JudgeType | None = None,\n    train_dataset: DatasetConfig | None = None,\n    eval_dataset: DatasetConfig | None = None,\n    model: TunerModelConfig | None = None,\n    auxiliary_models: dict[str, TunerModelConfig] | None = None,\n    algorithm: AlgorithmConfig | None = None,\n    project_name: str | None = None,\n    experiment_name: str | None = None,\n    monitor_type: str | None = None,\n    config_path: str | None = None,\n) -> None:\n    \"\"\"Train the agent workflow with the specific configuration.\n\n    Args:\n        workflow_func (`WorkflowType`): The learning workflow function\n            to execute.\n        judge_func (`JudgeType`, optional): The judge function used to\n            evaluate the workflow output. Defaults to None.\n        train_dataset (`DatasetConfig`, optional): The training dataset for\n            the learning process. Defaults to None.\n        eval_dataset (`DatasetConfig`, optional): The evaluation dataset for\n            the learning process. Defaults to None.\n        model (`TunerModelConfig`, optional): The model to be tuned.\n            Defaults to None.\n        auxiliary_models (`dict[str, TunerModelConfig]`, optional): A\n            dictionary of auxiliary models for LLM-as-a-Judge\n            or acting other agents in multi-agent scenarios.\n            Defaults to None.\n        algorithm (`AlgorithmConfig`, optional): The tuning algorithm\n            configuration. Defaults to None.\n        project_name (`str`, optional): Name of the project.\n            Defaults to None.\n        experiment_name (`str`, optional): Name of the experiment.\n            Leave None to use timestamp. Defaults to None.\n        monitor_type (`str`, optional): Type of the monitor to use.\n            Could be one of 'tensorboard', 'wandb', 'mlflow', 'swanlab'.\n            Leave None to use tensorboard. Defaults to None.\n        config_path (`str`, optional): Path to a trinity yaml configuration\n            file. If provided, only `workflow_func` is necessary, other\n            arguments will override the corresponding fields in the config.\n            Defaults to None.\n    \"\"\"\n    try:\n        from trinity.cli.launcher import run_stage\n        from trinity.utils.dlc_utils import setup_ray_cluster, stop_ray_cluster\n    except ImportError as e:\n        raise ImportError(\n            \"Trinity-RFT is not installed. Please install it with \"\n            \"`pip install trinity-rft`.\",\n        ) from e\n\n    check_workflow_function(workflow_func)\n    if judge_func is not None:\n        check_judge_function(judge_func)\n\n    config = _to_trinity_config(\n        config_path=config_path,\n        workflow_func=workflow_func,\n        judge_func=judge_func,\n        model=model,\n        auxiliary_models=auxiliary_models,\n        train_dataset=train_dataset,\n        eval_dataset=eval_dataset,\n        algorithm=algorithm,\n        project_name=project_name,\n        experiment_name=experiment_name,\n        monitor_type=monitor_type,\n    )\n    use_dlc = os.environ.get(\"USE_ALIYUN_PAI_DLC\", \"0\") == \"1\"\n    if use_dlc:\n        config.cluster.ray_address = setup_ray_cluster(namespace=\"agentscope\")\n    try:\n        return run_stage(\n            config=config.check_and_update(),\n        )\n    finally:\n        if use_dlc:\n            stop_ray_cluster(namespace=\"agentscope\")\n"
  },
  {
    "path": "src/agentscope/tuner/_workflow.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The workflow module for tuner.\"\"\"\nfrom logging import Logger\nfrom typing import Any, Callable, Dict, Awaitable\nfrom pydantic import BaseModel, Field\nfrom ..model import ChatModelBase\n\n\nclass WorkflowOutput(BaseModel):\n    \"\"\"The output of a workflow function.\"\"\"\n\n    reward: float | None = Field(\n        description=(\n            \"The reward obtained from the workflow function. \"\n            \"Used for direct reward output.\"\n        ),\n        default=None,\n    )\n    response: Any | None = Field(\n        description=(\n            \"The response generated by the workflow function. \"\n            \"Used as judge input.\"\n        ),\n        default=None,\n    )\n\n    metrics: Dict[str, float] | None = Field(\n        description=\"Metrics from the workflow function.\",\n        default=None,\n    )\n\n\nWorkflowType = Callable[\n    [Dict, ChatModelBase, Dict[str, ChatModelBase] | None, Logger | None],\n    Awaitable[WorkflowOutput],\n]\n# An agent workflow function type for tuning.\n\n# Args:\n#     task (`Dict`):\n#         The task information for the workflow run.\n#     model (`ChatModelBase`):\n#         The primary chat model used in the workflow, this is the main model\n#         being tuned.\n#     auxiliary_models (`Dict[str, ChatModelBase] | None`, optional):\n#         A dictionary of additional chat models available for LLM-as-a-Judge\n#         usage. The keys are model names, and the values are the corresponding\n#         `ChatModelBase` instances. Note that these auxiliary models are not\n#         tuned during the workflow.\n#     logger (`Logger | None`, optional):\n#         An optional logger for logging information during the workflow\n#         execution.\n# Returns:\n#     `WorkflowOutput`:\n#         The workflow execution results, including optional reward, raw\n#         response and metrics.\n"
  },
  {
    "path": "src/agentscope/types/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The types in agentscope\"\"\"\n\nfrom ._hook import (\n    AgentHookTypes,\n    ReActAgentHookTypes,\n)\nfrom ._object import Embedding\nfrom ._json import (\n    JSONPrimitive,\n    JSONSerializableObject,\n)\nfrom ._tool import ToolFunction\n\n__all__ = [\n    \"AgentHookTypes\",\n    \"ReActAgentHookTypes\",\n    \"Embedding\",\n    \"JSONPrimitive\",\n    \"JSONSerializableObject\",\n    \"ToolFunction\",\n]\n"
  },
  {
    "path": "src/agentscope/types/_hook.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The agent hooks types.\"\"\"\nfrom typing import Literal\n\nAgentHookTypes = (\n    str\n    | Literal[\n        \"pre_reply\",\n        \"post_reply\",\n        \"pre_print\",\n        \"post_print\",\n        \"pre_observe\",\n        \"post_observe\",\n    ]\n)\n\nReActAgentHookTypes = (\n    AgentHookTypes\n    | Literal[\n        \"pre_reasoning\",\n        \"post_reasoning\",\n        \"pre_acting\",\n        \"post_acting\",\n    ]\n)\n"
  },
  {
    "path": "src/agentscope/types/_json.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The JSON related types\"\"\"\n\nfrom typing import Union\n\nJSONPrimitive = Union[\n    str,\n    int,\n    float,\n    bool,\n    None,\n]\n\nJSONSerializableObject = Union[\n    JSONPrimitive,\n    list[\"JSONSerializableObject\"],\n    dict[\n        str,\n        \"JSONSerializableObject\",\n    ],\n]\n"
  },
  {
    "path": "src/agentscope/types/_object.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The object types in agentscope.\"\"\"\nfrom typing import List\n\nEmbedding = List[float]\n"
  },
  {
    "path": "src/agentscope/types/_tool.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The tool related types\"\"\"\n\nfrom typing import (\n    Callable,\n    Union,\n    Awaitable,\n    AsyncGenerator,\n    Generator,\n    Coroutine,\n    Any,\n    TYPE_CHECKING,\n)\n\nif TYPE_CHECKING:\n    from ..tool import ToolResponse\nelse:\n    ToolResponse = \"ToolResponse\"\n\nToolFunction = Callable[\n    ...,\n    Union[\n        # sync function\n        ToolResponse,\n        # async function\n        Awaitable[ToolResponse],\n        # sync generator function\n        Generator[ToolResponse, None, None],\n        # async generator function\n        AsyncGenerator[ToolResponse, None],\n        # async function that returns async generator\n        Coroutine[Any, Any, AsyncGenerator[ToolResponse, None]],\n        # async function that returns sync generator\n        Coroutine[Any, Any, Generator[ToolResponse, None, None]],\n    ],\n]\n"
  },
  {
    "path": "tests/a2a_agent_test.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=protected-access\n\"\"\"The A2A agent unittests.\"\"\"\nfrom typing import Any, AsyncIterator\nfrom unittest import IsolatedAsyncioTestCase\n\nfrom a2a.types import (\n    AgentCard,\n    AgentCapabilities,\n    Message as A2AMessage,\n    Part,\n    Role as A2ARole,\n    Task,\n    TaskState,\n    TaskStatus,\n    TextPart,\n    Artifact,\n)\n\nfrom agentscope.agent import A2AAgent\nfrom agentscope.message import Msg\n\n\nclass MockA2AClient:\n    \"\"\"Mock A2A client for testing.\"\"\"\n\n    def __init__(self, response_type: str = \"message\") -> None:\n        \"\"\"Initialize mock client.\n\n        Args:\n            response_type (`str`):\n                Type of response to simulate: \"message\", \"task\", or \"error\".\n        \"\"\"\n        self.response_type = response_type\n        self.sent_messages = []\n\n    async def send_message(\n        self,\n        message: A2AMessage,\n    ) -> AsyncIterator[A2AMessage | tuple[Task, Any]]:\n        \"\"\"Mock send_message method.\"\"\"\n        self.sent_messages.append(message)\n\n        if self.response_type == \"message\":\n            # Return a simple A2A message\n            response = A2AMessage(\n                message_id=\"test-msg-id\",\n                role=A2ARole.agent,\n                parts=[\n                    Part(root=TextPart(text=\"Hello from remote agent\")),\n                ],\n            )\n            yield response\n\n        elif self.response_type == \"task\":\n            # Return a task with completed state\n            task = Task(\n                id=\"test-task-id\",\n                context_id=\"test-context-id\",\n                status=TaskStatus(\n                    state=TaskState.completed,\n                    message=A2AMessage(\n                        message_id=\"status-msg-id\",\n                        role=A2ARole.agent,\n                        parts=[\n                            Part(root=TextPart(text=\"Task completed\")),\n                        ],\n                    ),\n                ),\n                artifacts=[\n                    Artifact(\n                        artifact_id=\"artifact-1\",\n                        name=\"test_artifact\",\n                        description=\"Test artifact\",\n                        parts=[\n                            Part(root=TextPart(text=\"Artifact content\")),\n                        ],\n                    ),\n                ],\n            )\n            yield (task, None)\n\n        elif self.response_type == \"error\":\n            raise RuntimeError(\"Simulated communication error\")\n\n\nclass MockClientFactory:\n    \"\"\"Mock ClientFactory for testing.\"\"\"\n\n    def __init__(self, response_type: str = \"message\") -> None:\n        \"\"\"Initialize mock factory.\"\"\"\n        self.response_type = response_type\n        self.created_clients = []\n\n    def create(self, card: AgentCard) -> MockA2AClient:\n        \"\"\"Create a mock client.\"\"\"\n        _ = card  # Used by real ClientFactory, not needed in mock\n        client = MockA2AClient(self.response_type)\n        self.created_clients.append(client)\n        return client\n\n\nclass A2AAgentTest(IsolatedAsyncioTestCase):\n    \"\"\"Test class for A2AAgent.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up test fixtures.\"\"\"\n        self.test_agent_card = AgentCard(\n            name=\"TestAgent\",\n            url=\"http://localhost:8000\",\n            description=\"Test A2A agent\",\n            version=\"1.0.0\",\n            capabilities=AgentCapabilities(),\n            default_input_modes=[\"text/plain\"],\n            default_output_modes=[\"text/plain\"],\n            skills=[],\n        )\n        self.agent = A2AAgent(self.test_agent_card)\n\n    async def test_reply_with_task(self) -> None:\n        \"\"\"Test reply method with task response.\"\"\"\n        # Mock the client factory\n        self.agent._a2a_client_factory = MockClientFactory(\n            response_type=\"task\",\n        )\n\n        response = await self.agent(\n            Msg(name=\"user\", content=\"Process this\", role=\"user\"),\n        )\n\n        self.assertEqual(response.name, \"TestAgent\")\n        self.assertEqual(response.role, \"assistant\")\n\n        # Should contain artifact content\n        self.assertEqual(\n            response.content,\n            [\n                {\n                    \"type\": \"text\",\n                    \"text\": \"Task completed\",\n                },\n                {\n                    \"type\": \"text\",\n                    \"text\": \"Artifact content\",\n                },\n            ],\n        )\n\n    async def test_reply_with_no_messages(self) -> None:\n        \"\"\"Test reply method with no messages returns prompt message.\"\"\"\n        self.agent._a2a_client_factory = MockClientFactory()\n\n        # Test with None - should return prompt message\n        response = await self.agent(None)\n        self.assertEqual(response.name, \"TestAgent\")\n        self.assertEqual(response.role, \"assistant\")\n        self.assertListEqual(\n            response.get_content_blocks(),\n            [\n                {\n                    \"type\": \"text\",\n                    \"text\": \"Hello from remote agent\",\n                },\n            ],\n        )\n\n        # Test with empty list - should return prompt message\n        response = await self.agent([])\n        self.assertListEqual(\n            response.get_content_blocks(),\n            [\n                {\n                    \"type\": \"text\",\n                    \"text\": \"Hello from remote agent\",\n                },\n            ],\n        )\n\n        # Test with list of None - should return prompt message\n        response = await self.agent([None, None])\n        self.assertListEqual(\n            response.get_content_blocks(),\n            [\n                {\n                    \"type\": \"text\",\n                    \"text\": \"Hello from remote agent\",\n                },\n            ],\n        )\n\n    async def test_observe_method(self) -> None:\n        \"\"\"Test observe method stores messages for next reply.\"\"\"\n        # Initially no observed messages\n        self.assertEqual(len(self.agent._observed_msgs), 0)\n\n        # Observe single message\n        await self.agent.observe(\n            Msg(name=\"user\", content=\"First observed\", role=\"user\"),\n        )\n        self.assertEqual(len(self.agent._observed_msgs), 1)\n\n        # Observe multiple messages\n        msg2 = Msg(name=\"user\", content=\"Second observed\", role=\"user\")\n        msg3 = Msg(name=\"user\", content=\"Third observed\", role=\"user\")\n        await self.agent.observe([msg2, msg3])\n        self.assertEqual(len(self.agent._observed_msgs), 3)\n\n        # Observe None should not change anything\n        await self.agent.observe(None)\n        self.assertEqual(len(self.agent._observed_msgs), 3)\n\n    async def test_observe_and_reply_merge(self) -> None:\n        \"\"\"Test that observed messages are merged with reply input.\"\"\"\n        mock_factory = MockClientFactory()\n        self.agent._a2a_client_factory = mock_factory\n\n        # Observe some messages\n        await self.agent.observe(\n            Msg(name=\"user\", content=\"Observed message\", role=\"user\"),\n        )\n\n        # Reply with another message\n        await self.agent.reply(\n            Msg(name=\"user\", content=\"Reply message\", role=\"user\"),\n        )\n\n        # Check that the send A2A message contains both observed and input\n        sent_msg = mock_factory.created_clients[0].sent_messages[0]\n        self.assertEqual(len(sent_msg.parts), 2)\n\n        # Check observed messages were cleared after reply\n        self.assertEqual(len(self.agent._observed_msgs), 0)\n\n    async def test_reply_with_only_observed_messages(self) -> None:\n        \"\"\"Test reply with None input uses only observed messages.\"\"\"\n        mock_factory = MockClientFactory()\n        self.agent._a2a_client_factory = mock_factory\n\n        # Observe a message\n        await self.agent.observe(\n            Msg(name=\"user\", content=\"Only observed\", role=\"user\"),\n        )\n\n        # Reply with None\n        await self.agent(None)\n\n        # Should have sent the observed message\n        sent_msg = mock_factory.created_clients[0].sent_messages[0]\n        self.assertEqual(len(sent_msg.parts), 1)\n        self.assertEqual(sent_msg.parts[0].root.text, \"Only observed\")\n\n        # Observed messages should be cleared\n        self.assertEqual(len(self.agent._observed_msgs), 0)\n"
  },
  {
    "path": "tests/a2a_resolver_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The agent card resolver tests for A2A agents.\"\"\"\nimport json\nimport os\nfrom unittest import IsolatedAsyncioTestCase\n\nfrom a2a.types import AgentCard, AgentCapabilities, AgentSkill\n\nfrom agentscope.a2a import FileAgentCardResolver\n\n\nclass A2AAgentCardResolverTest(IsolatedAsyncioTestCase):\n    \"\"\"Test the A2A agent card resolver.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the test case.\"\"\"\n        self.agent_card = AgentCard(\n            name=\"Friday\",\n            description=\"A simple ReAct agent that handles input queries\",\n            url=\"http://localhost:8000\",\n            version=\"1.0.0\",\n            capabilities=AgentCapabilities(\n                push_notifications=False,\n                state_transition_history=True,\n                streaming=True,\n            ),\n            default_input_modes=[\"text/plain\"],\n            default_output_modes=[\"text/plain\"],\n            skills=[\n                AgentSkill(\n                    name=\"execute_python_code\",\n                    id=\"execute_python_code\",\n                    description=\"Execute Python code snippets.\",\n                    tags=[\"code_execution\"],\n                ),\n                AgentSkill(\n                    name=\"execute_shell_command\",\n                    id=\"execute_shell_command\",\n                    description=\"Execute shell commands on the server.\",\n                    tags=[\"code_execution\"],\n                ),\n                AgentSkill(\n                    name=\"view_text_file\",\n                    id=\"view_text_file\",\n                    description=\"View the content of a text file on the \"\n                    \"server.\",\n                    tags=[\"file_viewing\"],\n                ),\n            ],\n        )\n        self.agent_card_path = \"./test_agent_card.json\"\n\n    async def test_file_card_resolver(self) -> None:\n        \"\"\"Test the file agent card resolver.\"\"\"\n        try:\n            # Save one agent card to a file\n            json_dict = self.agent_card.model_dump()\n            with open(self.agent_card_path, \"w\", encoding=\"utf-8\") as file:\n                json.dump(json_dict, file)\n\n            agent_card = await FileAgentCardResolver(\n                file_path=self.agent_card_path,\n            ).get_agent_card()\n\n            self.assertDictEqual(\n                agent_card.model_dump(),\n                self.agent_card.model_dump(),\n            )\n\n        finally:\n            if os.path.exists(self.agent_card_path):\n                os.remove(self.agent_card_path)\n"
  },
  {
    "path": "tests/config_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unittests for the config module.\"\"\"\nimport asyncio\nimport threading\nfrom unittest.async_case import IsolatedAsyncioTestCase\n\nimport agentscope\nfrom agentscope import _config\n\n\nresult = {}\n\n\nasync def async_task(field: str) -> str:\n    \"\"\"A sample async task to demonstrate context variable usage.\"\"\"\n    prefix = \"async_task\"\n    agentscope.init(\n        run_id=f\"{prefix}_run_id\",\n        project=f\"{prefix}_project\",\n        name=f\"{prefix}_name\",\n    )\n    if field == \"run_id\":\n        return _config.run_id\n    elif field == \"project\":\n        return _config.project\n    elif field == \"name\":\n        return _config.name\n    else:\n        return \"\"\n\n\ndef sync_task(field: str) -> None:\n    \"\"\"A sample sync task to demonstrate context variable usage.\"\"\"\n    prefix = \"sync_task\"\n    agentscope.init(\n        run_id=f\"{prefix}_run_id\",\n        project=f\"{prefix}_project\",\n        name=f\"{prefix}_name\",\n    )\n    if field == \"run_id\":\n        result[\"value\"] = _config.run_id\n    elif field == \"project\":\n        result[\"value\"] = _config.project\n    elif field == \"name\":\n        result[\"value\"] = _config.name\n    else:\n        result[\"value\"] = None\n\n\nclass ConfigTest(IsolatedAsyncioTestCase):\n    \"\"\"Unittests for the config module.\"\"\"\n\n    async def test_config_attributes(self) -> None:\n        \"\"\"Test the config attributes.\"\"\"\n        agentscope.init(\n            project=\"root_project\",\n            name=\"root_name\",\n            run_id=\"root_run_id\",\n        )\n\n        for field in [\"project\", \"name\", \"run_id\"]:\n            # Test root context\n            self.assertEqual(getattr(_config, field), f\"root_{field}\")\n\n            # Test asynchronous task\n            res = await asyncio.create_task(async_task(field))\n            self.assertEqual(res, f\"async_task_{field}\")\n\n            # Test synchronous task in a separate thread\n            thread = threading.Thread(target=sync_task, args=(field,))\n            thread.start()\n            thread.join()\n\n            self.assertEqual(result[\"value\"], f\"sync_task_{field}\")\n"
  },
  {
    "path": "tests/embedding_cache_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The embedding cache tests in agentscope.\"\"\"\nimport os\nimport shutil\nimport time\nfrom unittest.async_case import IsolatedAsyncioTestCase\n\nimport numpy as np\n\nfrom agentscope.embedding import FileEmbeddingCache\n\n\nclass EmbeddingCacheTest(IsolatedAsyncioTestCase):\n    \"\"\"The embedding cache tests in agentscope.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the test case.\"\"\"\n        self.embeddings = [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]\n        self.identifier1 = {\n            \"model\": \"text-embedding-v1\",\n            \"text\": [\"This is a test text for embedding cache.\"],\n        }\n        self.identifier2 = {\n            \"model\": \"text-embedding-v2\",\n            \"text\": [\"This is a test text for embedding cache.\"],\n        }\n        self.identifier3 = {\n            \"model\": \"text-embedding-v3\",\n            \"text\": [\"This is a test text for embedding cache.\"],\n        }\n        self.identifier4 = {\n            \"model\": \"text-embedding-v4\",\n            \"text\": [\"This is a test text for embedding cache.\"],\n        }\n        self.identifier5 = {\n            \"model\": \"text-embedding-v5\",\n            \"text\": [\"This is a test text for embedding cache.\"],\n        }\n        self.ground_truth_filenames = [\n            \"56fe1fc64cb2830d0026559607e2ee5b9ae1d4524b256f83fdc2e2c8c23cefb7\"\n            \".npy\",\n            \"fd9deb42e60e87c8358bc262c0ede4f0490385ffad1b85dddec3937ce6a24b03\"\n            \".npy\",\n            \"394cb1f647fa493309abd4b97066649de9d5e5f2bbb03f43b11d56bf7d161497\"\n            \".npy\",\n            \"ce97eac56670400eef1a7c8b7e366107af2129649b02479729bceefe2edd5727\"\n            \".npy\",\n            \"675caa8a352219a9b74ebda4b1930e96d4c292f288f79ccb65e42293dd9de162\"\n            \".npy\",\n        ]\n\n        self.large_embeddings = np.zeros((600, 600)).tolist()\n\n        self.embedding_cache = FileEmbeddingCache(\n            max_file_number=3,\n            max_cache_size=2,\n        )\n\n    def _get_filenames(self, path_dir: str) -> list[str]:\n        \"\"\"Get the filenames in the cache directory.\"\"\"\n        filenames = [\n            (_.name, _.stat().st_mtime)\n            for _ in os.scandir(path_dir)\n            if _.is_file() and _.name.endswith(\".npy\")\n        ]\n        filenames.sort(key=lambda x: x[1])\n\n        return [_[0] for _ in filenames]\n\n    async def test_embedding_cache(self) -> None:\n        \"\"\"Test the embedding cache.\"\"\"\n\n        await self.embedding_cache.store(self.embeddings, self.identifier1)\n        self.assertListEqual(\n            self._get_filenames(self.embedding_cache.cache_dir),\n            self.ground_truth_filenames[:1],\n        )\n\n        time.sleep(1)\n\n        # when overwrite is False\n        await self.embedding_cache.store([[1, 2]], self.identifier1)\n        self.assertListEqual(\n            self._get_filenames(self.embedding_cache.cache_dir),\n            self.ground_truth_filenames[:1],\n        )\n        cached_embedding = await self.embedding_cache.retrieve(\n            self.identifier1,\n        )\n        self.assertListEqual(\n            cached_embedding,\n            self.embeddings,\n        )\n\n        time.sleep(1)\n\n        # when overwrite is True\n        await self.embedding_cache.store(\n            [[1, 2]],\n            self.identifier1,\n            overwrite=True,\n        )\n        self.assertListEqual(\n            self._get_filenames(self.embedding_cache.cache_dir),\n            self.ground_truth_filenames[:1],\n        )\n        cached_embedding = await self.embedding_cache.retrieve(\n            self.identifier1,\n        )\n        self.assertListEqual(\n            cached_embedding,\n            [[1, 2]],\n        )\n\n        time.sleep(1)\n\n        await self.embedding_cache.store(self.embeddings, self.identifier2)\n        self.assertListEqual(\n            self._get_filenames(self.embedding_cache.cache_dir),\n            self.ground_truth_filenames[:2],\n        )\n\n        time.sleep(1)\n\n        await self.embedding_cache.store(self.embeddings, self.identifier3)\n        self.assertListEqual(\n            self._get_filenames(self.embedding_cache.cache_dir),\n            self.ground_truth_filenames[:3],\n        )\n\n        time.sleep(1)\n\n        await self.embedding_cache.store(self.embeddings, self.identifier4)\n        self.assertListEqual(\n            self._get_filenames(self.embedding_cache.cache_dir),\n            self.ground_truth_filenames[1:4],\n        )\n\n        time.sleep(1)\n\n        await self.embedding_cache.store(self.embeddings, self.identifier5)\n        self.assertListEqual(\n            self._get_filenames(self.embedding_cache.cache_dir),\n            self.ground_truth_filenames[2:5],\n        )\n\n        time.sleep(1)\n\n        await self.embedding_cache.store(\n            self.large_embeddings,\n            self.identifier1,\n            overwrite=True,\n        )\n        self.assertListEqual(\n            self._get_filenames(self.embedding_cache.cache_dir),\n            [],\n        )\n\n        await self.embedding_cache.clear()\n        self.assertListEqual(\n            self._get_filenames(self.embedding_cache.cache_dir),\n            [],\n        )\n\n    async def asyncTearDown(self) -> None:\n        \"\"\"Tear down the test case.\"\"\"\n        if os.path.exists(self.embedding_cache.cache_dir):\n            shutil.rmtree(self.embedding_cache.cache_dir)\n"
  },
  {
    "path": "tests/evaluation_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Evaluation module tests in agentscope.\"\"\"\nimport os\nimport sys\nimport shutil\nfrom typing import Generator, Callable, Any, cast\nfrom unittest.async_case import IsolatedAsyncioTestCase\nimport ray\n\nfrom agentscope.agent import AgentBase\nfrom agentscope.message import Msg\nfrom agentscope.evaluate import (\n    SolutionOutput,\n    MetricBase,\n    MetricResult,\n    MetricType,\n    Task,\n    BenchmarkBase,\n    GeneralEvaluator,\n    RayEvaluator,\n    FileEvaluatorStorage,\n)\n\n\nTASK_ID_1 = \"math_problem_1\"\nTASK_ID_2 = \"math_problem_2\"\n\nTOY_BENCHMARK = [\n    {\n        \"id\": TASK_ID_1,\n        \"question\": \"What is 2 + 2?\",\n        \"ground_truth\": 4.0,\n        \"tags\": {\n            \"difficulty\": \"easy\",\n            \"category\": \"math\",\n        },\n    },\n    {\n        \"id\": TASK_ID_2,\n        \"question\": \"What is 12345 + 54321 + 6789 + 9876?\",\n        \"ground_truth\": 83331,\n        \"tags\": {\n            \"difficulty\": \"medium\",\n            \"category\": \"math\",\n        },\n    },\n]\n\n\nMETRIC_NAME = \"math_check_number_equal\"\n\n\nclass CheckEqual(MetricBase):\n    \"\"\"Metric to check whether the provide answer is equal to ground truth.\"\"\"\n\n    def __init__(\n        self,\n        ground_truth: float,\n    ):\n        super().__init__(\n            name=METRIC_NAME,\n            metric_type=MetricType.NUMERICAL,\n            description=\"Toy metric checking if two numbers are equal\",\n            categories=[],\n        )\n        self.ground_truth = ground_truth\n\n    async def __call__(\n        self,\n        solution: SolutionOutput,\n    ) -> MetricResult:\n        if solution.output == self.ground_truth:\n            return MetricResult(\n                name=self.name,\n                result=1.0,\n                message=\"Correct\",\n            )\n        else:\n            return MetricResult(\n                name=self.name,\n                result=0.0,\n                message=\"Incorrect\",\n            )\n\n\nclass ToyBenchmark(BenchmarkBase):\n    \"\"\"A toy benchmark for testing\"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=\"Toy bench\",\n            description=\"A toy benchmark for testing \"\n            \"the evaluation module.\",\n        )\n        self.dataset = self._load_data()\n\n    @staticmethod\n    def _load_data() -> list[Task]:\n        dataset = []\n        for item in TOY_BENCHMARK:\n            dataset.append(\n                Task(\n                    id=item[\"id\"],\n                    input=item[\"question\"],\n                    ground_truth=item[\"ground_truth\"],\n                    tags=item.get(\"tags\", {}),\n                    metrics=[\n                        CheckEqual(cast(float, item[\"ground_truth\"])),\n                    ],\n                    metadata={},\n                ),\n            )\n        return dataset\n\n    def __iter__(self) -> Generator[Task, None, None]:\n        \"\"\"Iterate over the benchmark.\"\"\"\n        for task in self.dataset:\n            yield task\n\n    def __getitem__(self, index: int) -> Task:\n        \"\"\"Get a task by index.\"\"\"\n        return self.dataset[index]\n\n    def __len__(self) -> int:\n        \"\"\"Get the length of the benchmark.\"\"\"\n        return len(self.dataset)\n\n\nclass EvalTestAgent(AgentBase):\n    \"\"\"Test agent class for testing hooks.\"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"Initialize the test agent.\"\"\"\n        super().__init__()\n        self.records: list[str] = []\n        self.memory: list[Msg] = []\n\n    async def reply(self, msg: Msg) -> Msg:\n        \"\"\"Reply to the message.\"\"\"\n        return Msg(\n            name=\"test_eval_agent\",\n            content=msg.content,\n            role=\"assistant\",\n            metadata={\"answer_as_number\": 4.0},\n        )\n\n    async def handle_interrupt(\n        self,\n        *args: Any,\n        **kwargs: Any,\n    ) -> Msg:\n        \"\"\"Dummy handle interrupt.\"\"\"\n        print(args, kwargs)\n\n    async def observe(self, msg: Msg | list[Msg] | None) -> None:\n        \"\"\"Dummy observe function.\"\"\"\n        print(msg.content)\n\n\nasync def dummy_solution_generation(\n    task: Task,\n    pre_hook: Callable,  # pylint: disable=W0613\n) -> SolutionOutput:\n    \"\"\"Solution generation function for test\"\"\"\n    agent = EvalTestAgent()\n\n    msg_input = Msg(\"user\", task.input, role=\"user\")\n    res = await agent(msg_input)\n    return SolutionOutput(\n        success=True,\n        output=res.metadata.get(\"answer_as_number\", None),\n        trajectory=[],\n    )\n\n\nclass EvaluatorTest(IsolatedAsyncioTestCase):\n    \"\"\"Test for evaluators in AS\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the test environment.\"\"\"\n        current_dir = os.path.dirname(os.path.abspath(__file__))\n        self.file_storage_general = FileEvaluatorStorage(\n            save_dir=os.path.join(\n                current_dir,\n                \"general_results\",\n            ),\n        )\n        self.file_storage_ray = FileEvaluatorStorage(\n            save_dir=os.path.join(\n                current_dir,\n                \"ray_results\",\n            ),\n        )\n        # Initialize Ray with proper serialization settings\n        if not ray.is_initialized():\n            # Add the current directory to Python path for Ray workers\n            current_dir = os.path.dirname(os.path.abspath(__file__))\n            if current_dir not in sys.path:\n                sys.path.insert(0, current_dir)\n\n            ray.init(\n                _temp_dir=None,  # Use default temp directory\n                ignore_reinit_error=True,  # Allow re-initialization\n                runtime_env={\n                    \"working_dir\": current_dir,\n                    \"py_modules\": [__file__],  # Include this test file\n                },\n            )\n\n    async def test_general_evaluator(self) -> None:\n        \"\"\"Test general evaluator.\"\"\"\n        evaluator = GeneralEvaluator(\n            name=\"Test evaluation\",\n            benchmark=ToyBenchmark(),\n            # Repeat how many times\n            n_repeat=1,\n            storage=self.file_storage_general,\n            # How many workers to use\n            n_workers=1,\n        )\n\n        # Run the evaluation\n        await evaluator.run(dummy_solution_generation)\n        metric_result_1 = self.file_storage_general.get_evaluation_result(\n            task_id=TASK_ID_1,\n            repeat_id=\"0\",\n            metric_name=METRIC_NAME,\n        )\n        self.assertEqual(\n            metric_result_1.result,\n            1.0,\n        )\n        metric_result_2 = self.file_storage_general.get_evaluation_result(\n            task_id=TASK_ID_2,\n            repeat_id=\"0\",\n            metric_name=METRIC_NAME,\n        )\n        self.assertEqual(\n            metric_result_2.result,\n            0.0,\n        )\n\n    async def test_ray_evaluator(self) -> None:\n        \"\"\"Test ray evaluator.\"\"\"\n        evaluator = RayEvaluator(\n            name=\"Test evaluation\",\n            benchmark=ToyBenchmark(),\n            # Repeat how many times\n            n_repeat=1,\n            storage=self.file_storage_ray,\n            # How many workers to use\n            n_workers=1,\n        )\n\n        # Run the evaluation\n        await evaluator.run(dummy_solution_generation)\n        metric_result_1 = self.file_storage_ray.get_evaluation_result(\n            task_id=TASK_ID_1,\n            repeat_id=\"0\",\n            metric_name=METRIC_NAME,\n        )\n        self.assertEqual(\n            metric_result_1.result,\n            1.0,\n        )\n        metric_result_2 = self.file_storage_ray.get_evaluation_result(\n            task_id=TASK_ID_2,\n            repeat_id=\"0\",\n            metric_name=METRIC_NAME,\n        )\n        self.assertEqual(\n            metric_result_2.result,\n            0.0,\n        )\n\n    async def asyncTearDown(self) -> None:\n        \"\"\"Clean up the test environment.\"\"\"\n        # Shutdown Ray if it was initialized\n        if ray.is_initialized():\n            ray.shutdown()\n\n        # clean written files\n        if os.path.exists(self.file_storage_general.save_dir):\n            shutil.rmtree(self.file_storage_general.save_dir)\n\n        if os.path.exists(self.file_storage_ray.save_dir):\n            shutil.rmtree(self.file_storage_ray.save_dir)\n"
  },
  {
    "path": "tests/formatter_a2a_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Test the a2a formatter class.\"\"\"\nfrom unittest import IsolatedAsyncioTestCase\n\nfrom a2a.types import (\n    Message,\n    TextPart,\n    FilePart,\n    DataPart,\n    FileWithUri,\n    FileWithBytes,\n    Role,\n    Part,\n    Task,\n    Artifact,\n    TaskStatus,\n    TaskState,\n)\n\nfrom agentscope.formatter import A2AChatFormatter\nfrom agentscope.message import (\n    Msg,\n    TextBlock,\n    ThinkingBlock,\n    ToolUseBlock,\n    ToolResultBlock,\n    ImageBlock,\n    URLSource,\n    Base64Source,\n    AudioBlock,\n    VideoBlock,\n)\n\n\nclass A2AFormatterTest(IsolatedAsyncioTestCase):\n    \"\"\"Test the A2A formatter class.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the test case.\"\"\"\n        self.formatter = A2AChatFormatter()\n        self.as_msgs = [\n            Msg(\n                \"user\",\n                content=\"Hello, how are you?\",\n                role=\"user\",\n            ),\n            Msg(\n                \"user\",\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=\"Hello, how are you?\",\n                    ),\n                    ThinkingBlock(\n                        type=\"thinking\",\n                        thinking=\"yes\",\n                    ),\n                    ToolUseBlock(\n                        type=\"tool_use\",\n                        id=\"tool_1\",\n                        name=\"tool_1\",\n                        input={\"param1\": \"value1\"},\n                    ),\n                    ToolResultBlock(\n                        type=\"tool_result\",\n                        id=\"tool_1\",\n                        name=\"tool_1\",\n                        output=\"Tool output here.\",\n                    ),\n                    ImageBlock(\n                        type=\"image\",\n                        source=URLSource(\n                            type=\"url\",\n                            url=\"https://example.com/image.png\",\n                        ),\n                    ),\n                    AudioBlock(\n                        type=\"audio\",\n                        source=Base64Source(\n                            type=\"base64\",\n                            data=\"UklGRigAAABXQVZFZm10IBAAAAABAAEAQB8AAIA+\"\n                            \"AAACABAAZGF0YQAAAAA=\",\n                            media_type=\"audio/wav\",\n                        ),\n                    ),\n                    VideoBlock(\n                        type=\"video\",\n                        source=URLSource(\n                            type=\"url\",\n                            url=\"https://example.com/video.mp4\",\n                        ),\n                    ),\n                ],\n                role=\"user\",\n            ),\n        ]\n        self.a2a_msg = Message(\n            role=Role.user,\n            context_id=\"123\",\n            extensions=[\"ext1\", \"ext2\"],\n            message_id=\"abc\",\n            parts=[\n                Part(\n                    root=TextPart(\n                        text=\"Hello, how are you?\",\n                    ),\n                ),\n                Part(\n                    root=FilePart(\n                        file=FileWithUri(\n                            mime_type=\"audio/wav\",\n                            name=\"greeting.wav\",\n                            uri=\"https://example.com/greeting.wav\",\n                        ),\n                    ),\n                ),\n                Part(\n                    root=FilePart(\n                        file=FileWithBytes(\n                            bytes=\"UklGRigAAABXQVZFZm10IBAAAAABAAEAQB8AAIA+\"\n                            \"AAACABAAZGF0YQAAAAA=\",\n                            mime_type=\"audio/wav\",\n                            name=\"greeting.wav\",\n                        ),\n                    ),\n                ),\n                Part(\n                    root=DataPart(\n                        data={\n                            \"type\": \"tool_use\",\n                            \"id\": \"tool_1\",\n                            \"name\": \"tool_1\",\n                            \"input\": {\n                                \"param1\": \"value1\",\n                            },\n                        },\n                    ),\n                ),\n                Part(\n                    root=DataPart(\n                        data={\n                            \"type\": \"tool_result\",\n                            \"id\": \"tool_1\",\n                            \"name\": \"tool_1\",\n                            \"output\": \"Tool output here.\",\n                        },\n                    ),\n                ),\n                Part(\n                    root=DataPart(\n                        data={\n                            \"type\": \"unknown_type\",\n                            \"content\": \"Some unknown content\",\n                        },\n                    ),\n                ),\n            ],\n        )\n\n    async def test_as_to_a2a(self) -> None:\n        \"\"\"Test conversion from agentscope message to A2A message.\"\"\"\n        a2a_msg = await self.formatter.format(self.as_msgs)\n        self.assertIsInstance(a2a_msg, Message)\n        self.assertListEqual(\n            [_.model_dump() for _ in a2a_msg.parts],\n            [\n                {\n                    \"kind\": \"text\",\n                    \"metadata\": None,\n                    \"text\": \"Hello, how are you?\",\n                },\n                {\n                    \"kind\": \"text\",\n                    \"metadata\": None,\n                    \"text\": \"Hello, how are you?\",\n                },\n                {\n                    \"kind\": \"text\",\n                    \"metadata\": None,\n                    \"text\": \"yes\",\n                },\n                {\n                    \"data\": {\n                        \"type\": \"tool_use\",\n                        \"id\": \"tool_1\",\n                        \"name\": \"tool_1\",\n                        \"input\": {\n                            \"param1\": \"value1\",\n                        },\n                    },\n                    \"kind\": \"data\",\n                    \"metadata\": None,\n                },\n                {\n                    \"data\": {\n                        \"type\": \"tool_result\",\n                        \"id\": \"tool_1\",\n                        \"name\": \"tool_1\",\n                        \"output\": \"Tool output here.\",\n                    },\n                    \"kind\": \"data\",\n                    \"metadata\": None,\n                },\n                {\n                    \"file\": {\n                        \"mimeType\": None,\n                        \"name\": None,\n                        \"uri\": \"https://example.com/image.png\",\n                    },\n                    \"kind\": \"file\",\n                    \"metadata\": None,\n                },\n                {\n                    \"file\": {\n                        \"bytes\": \"UklGRigAAABXQVZFZm10IBAAAAABAAEAQB8AAIA+\"\n                        \"AAACABAAZGF0YQAAAAA=\",\n                        \"mimeType\": \"audio/wav\",\n                        \"name\": None,\n                    },\n                    \"kind\": \"file\",\n                    \"metadata\": None,\n                },\n                {\n                    \"file\": {\n                        \"mimeType\": None,\n                        \"name\": None,\n                        \"uri\": \"https://example.com/video.mp4\",\n                    },\n                    \"kind\": \"file\",\n                    \"metadata\": None,\n                },\n            ],\n        )\n        self.assertEqual(\n            a2a_msg.role,\n            \"user\",\n        )\n\n        a2a_msg = await self.formatter.format([])\n        self.assertListEqual(\n            a2a_msg.parts,\n            [],\n        )\n        self.assertEqual(\n            a2a_msg.role,\n            \"user\",\n        )\n\n    async def test_a2a_msg_to_as(self) -> None:\n        \"\"\"Test conversion from A2A message to agentscope message.\"\"\"\n        as_msg = await self.formatter.format_a2a_message(\n            \"Friday\",\n            self.a2a_msg,\n        )\n\n        self.assertEqual(\n            as_msg.role,\n            \"user\",\n        )\n        self.assertListEqual(\n            as_msg.get_content_blocks(),\n            [\n                {\"type\": \"text\", \"text\": \"Hello, how are you?\"},\n                {\n                    \"type\": \"audio\",\n                    \"source\": {\n                        \"type\": \"url\",\n                        \"url\": \"https://example.com/greeting.wav\",\n                    },\n                },\n                {\n                    \"type\": \"audio\",\n                    \"source\": {\n                        \"type\": \"base64\",\n                        \"media_type\": \"audio/wav\",\n                        \"data\": \"UklGRigAAABXQVZFZm10IBAAAAABAAEAQB8AAIA+\"\n                        \"AAACABAAZGF0YQAAAAA=\",\n                    },\n                },\n                {\n                    \"type\": \"tool_use\",\n                    \"id\": \"tool_1\",\n                    \"name\": \"tool_1\",\n                    \"input\": {\"param1\": \"value1\"},\n                },\n                {\n                    \"type\": \"tool_result\",\n                    \"id\": \"tool_1\",\n                    \"name\": \"tool_1\",\n                    \"output\": \"Tool output here.\",\n                },\n                {\n                    \"type\": \"text\",\n                    \"text\": \"{'type': 'unknown_type', 'content': 'Some \"\n                    \"unknown content'}\",\n                },\n            ],\n        )\n\n    async def test_a2a_task_to_as(self) -> None:\n        \"\"\"Test conversion from A2A task to agentscope message.\"\"\"\n\n        as_msgs = await self.formatter.format_a2a_task(\n            name=\"Friday\",\n            task=Task(\n                context_id=\"abc\",\n                artifacts=[\n                    Artifact(\n                        artifact_id=\"123\",\n                        parts=[\n                            Part(\n                                root=TextPart(\n                                    text=\"This is an artifact text part.\",\n                                ),\n                            ),\n                            Part(\n                                root=DataPart(\n                                    data={\n                                        \"type\": \"tool_result\",\n                                        \"id\": \"tool_2\",\n                                        \"name\": \"tool_2\",\n                                        \"output\": \"Artifact tool output.\",\n                                    },\n                                ),\n                            ),\n                        ],\n                    ),\n                ],\n                id=\"task_1\",\n                status=TaskStatus(\n                    message=self.a2a_msg,\n                    state=TaskState.completed,\n                    timestamp=\"def\",\n                ),\n            ),\n        )\n        self.assertEqual(len(as_msgs), 2)\n        self.maxDiff = None\n        self.assertDictEqual(\n            as_msgs[0].to_dict(),\n            {\n                \"id\": as_msgs[0].id,\n                \"name\": \"Friday\",\n                \"role\": \"user\",\n                \"content\": [\n                    {\"type\": \"text\", \"text\": \"Hello, how are you?\"},\n                    {\n                        \"type\": \"audio\",\n                        \"source\": {\n                            \"type\": \"url\",\n                            \"url\": \"https://example.com/greeting.wav\",\n                        },\n                    },\n                    {\n                        \"type\": \"audio\",\n                        \"source\": {\n                            \"type\": \"base64\",\n                            \"media_type\": \"audio/wav\",\n                            \"data\": \"UklGRigAAABXQVZFZm10IBAAAAABAAEAQB8AAIA+\"\n                            \"AAACABAAZGF0YQAAAAA=\",\n                        },\n                    },\n                    {\n                        \"type\": \"tool_use\",\n                        \"id\": \"tool_1\",\n                        \"name\": \"tool_1\",\n                        \"input\": {\"param1\": \"value1\"},\n                    },\n                    {\n                        \"type\": \"tool_result\",\n                        \"id\": \"tool_1\",\n                        \"name\": \"tool_1\",\n                        \"output\": \"Tool output here.\",\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"{'type': 'unknown_type', 'content': 'Some \"\n                        \"unknown content'}\",\n                    },\n                ],\n                \"metadata\": {},\n                \"timestamp\": as_msgs[0].timestamp,\n            },\n        )\n\n        self.assertDictEqual(\n            as_msgs[1].to_dict(),\n            {\n                \"id\": as_msgs[1].id,\n                \"name\": \"Friday\",\n                \"role\": \"assistant\",\n                \"content\": [\n                    {\"type\": \"text\", \"text\": \"This is an artifact text part.\"},\n                    {\n                        \"type\": \"tool_result\",\n                        \"id\": \"tool_2\",\n                        \"name\": \"tool_2\",\n                        \"output\": \"Artifact tool output.\",\n                    },\n                ],\n                \"metadata\": {},\n                \"timestamp\": as_msgs[1].timestamp,\n            },\n        )\n"
  },
  {
    "path": "tests/formatter_anthropic_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The Anthropic formatter unittests.\"\"\"\nfrom unittest.async_case import IsolatedAsyncioTestCase\n\nfrom agentscope.formatter import (\n    AnthropicMultiAgentFormatter,\n    AnthropicChatFormatter,\n)\nfrom agentscope.message import (\n    Msg,\n    ToolUseBlock,\n    ToolResultBlock,\n    TextBlock,\n    ImageBlock,\n    URLSource,\n)\n\n\nclass TestAnthropicChatFormatterFormatter(IsolatedAsyncioTestCase):\n    \"\"\"Unittest for the AnthropicChatFormatter.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the test environment.\"\"\"\n        self.image_url = \"www.example_image.png\"\n\n        self.msgs_system = [\n            Msg(\n                \"system\",\n                \"You're a helpful assistant.\",\n                \"system\",\n            ),\n        ]\n        self.msgs_conversation = [\n            Msg(\n                \"user\",\n                [\n                    TextBlock(\n                        type=\"text\",\n                        text=\"What is the capital of France?\",\n                    ),\n                    ImageBlock(\n                        type=\"image\",\n                        source=URLSource(\n                            type=\"url\",\n                            url=self.image_url,\n                        ),\n                    ),\n                ],\n                \"user\",\n            ),\n            Msg(\n                \"assistant\",\n                \"The capital of France is Paris.\",\n                \"assistant\",\n            ),\n            Msg(\n                \"user\",\n                \"What is the capital of Japan?\",\n                \"user\",\n            ),\n        ]\n\n        self.msgs_tools = [\n            Msg(\n                \"assistant\",\n                [\n                    ToolUseBlock(\n                        type=\"tool_use\",\n                        id=\"1\",\n                        name=\"get_capital\",\n                        input={\"country\": \"Japan\"},\n                    ),\n                ],\n                \"assistant\",\n            ),\n            Msg(\n                \"system\",\n                [\n                    ToolResultBlock(\n                        type=\"tool_result\",\n                        id=\"1\",\n                        name=\"get_capital\",\n                        output=\"The capital of Japan is Tokyo.\",\n                    ),\n                ],\n                \"system\",\n            ),\n            Msg(\n                \"assistant\",\n                \"The capital of Japan is Tokyo.\",\n                \"assistant\",\n            ),\n        ]\n\n        self.msgs_conversation_2 = [\n            Msg(\n                \"user\",\n                \"What is the capital of South Korea?\",\n                \"user\",\n            ),\n        ]\n\n        self.msgs_tools_2 = [\n            Msg(\n                \"assistant\",\n                [\n                    ToolUseBlock(\n                        type=\"tool_use\",\n                        id=\"2\",\n                        name=\"get_capital\",\n                        input={\"country\": \"South Korea\"},\n                    ),\n                ],\n                \"assistant\",\n            ),\n            Msg(\n                \"system\",\n                [\n                    ToolResultBlock(\n                        type=\"tool_result\",\n                        id=\"2\",\n                        name=\"get_capital\",\n                        output=\"The capital of South Korea is Seoul.\",\n                    ),\n                ],\n                \"system\",\n            ),\n            Msg(\n                \"assistant\",\n                \"The capital of South Korea is Seoul.\",\n                \"assistant\",\n            ),\n        ]\n\n        self.ground_truth_chat = [\n            {\n                \"role\": \"system\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"You're a helpful assistant.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"What is the capital of France?\",\n                    },\n                    {\n                        \"type\": \"image\",\n                        \"source\": {\n                            \"type\": \"url\",\n                            \"url\": \"www.example_image.png\",\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"The capital of France is Paris.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"What is the capital of Japan?\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"tool_use\",\n                        \"name\": \"get_capital\",\n                        \"input\": {\n                            \"country\": \"Japan\",\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"tool_result\",\n                        \"tool_use_id\": \"1\",\n                        \"content\": [\n                            {\n                                \"type\": \"text\",\n                                \"text\": \"The capital of Japan is Tokyo.\",\n                            },\n                        ],\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"The capital of Japan is Tokyo.\",\n                    },\n                ],\n            },\n        ]\n\n        self.ground_truth_multiagent = [\n            {\n                \"role\": \"system\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"You're a helpful assistant.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"text\": \"# Conversation History\\nThe content \"\n                        \"between <history></history> tags contains \"\n                        \"your conversation history\\n<history>\\nuser:\"\n                        \" What is the capital of France?\",\n                        \"type\": \"text\",\n                    },\n                    {\n                        \"type\": \"image\",\n                        \"source\": {\n                            \"type\": \"url\",\n                            \"url\": \"www.example_image.png\",\n                        },\n                    },\n                    {\n                        \"text\": \"assistant: The capital of France is Paris.\"\n                        \"\\nuser: What is the capital of Japan?\"\n                        \"\\n</history>\",\n                        \"type\": \"text\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"tool_use\",\n                        \"name\": \"get_capital\",\n                        \"input\": {\n                            \"country\": \"Japan\",\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"tool_result\",\n                        \"tool_use_id\": \"1\",\n                        \"content\": [\n                            {\n                                \"type\": \"text\",\n                                \"text\": \"The capital of Japan is Tokyo.\",\n                            },\n                        ],\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"text\": \"<history>\\nassistant:\"\n                        \" The capital of Japan is Tokyo.\\n</history>\",\n                        \"type\": \"text\",\n                    },\n                ],\n            },\n        ]\n\n        self.ground_truth_multiagent_without_first_conversation = [\n            {\n                \"role\": \"system\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"You're a helpful assistant.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"tool_use\",\n                        \"name\": \"get_capital\",\n                        \"input\": {\n                            \"country\": \"Japan\",\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"tool_result\",\n                        \"tool_use_id\": \"1\",\n                        \"content\": [\n                            {\n                                \"type\": \"text\",\n                                \"text\": \"The capital of Japan is Tokyo.\",\n                            },\n                        ],\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"text\": \"# Conversation History\\nThe content \"\n                        \"between <history></history> tags contains \"\n                        \"your conversation history\\n<history>\\nassistant:\"\n                        \" The capital of Japan is Tokyo.\\n</history>\",\n                        \"type\": \"text\",\n                    },\n                ],\n            },\n        ]\n\n        self.ground_truth_multiagent_2 = [\n            {\n                \"role\": \"system\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"You're a helpful assistant.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"text\": \"# Conversation History\\nThe content between\"\n                        \" <history></history> tags contains your\"\n                        \" conversation history\\n<history>\\nuser: What\"\n                        \" is the capital of France?\",\n                        \"type\": \"text\",\n                    },\n                    {\n                        \"type\": \"image\",\n                        \"source\": {\n                            \"type\": \"url\",\n                            \"url\": \"www.example_image.png\",\n                        },\n                    },\n                    {\n                        \"text\": \"assistant: The capital of France is Paris.\"\n                        \"\\nuser: What is the capital of Japan?\"\n                        \"\\n</history>\",\n                        \"type\": \"text\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"tool_use\",\n                        \"name\": \"get_capital\",\n                        \"input\": {\n                            \"country\": \"Japan\",\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"tool_result\",\n                        \"tool_use_id\": \"1\",\n                        \"content\": [\n                            {\n                                \"type\": \"text\",\n                                \"text\": \"The capital of Japan is Tokyo.\",\n                            },\n                        ],\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"text\": \"<history>\\nassistant:\"\n                        \" The capital of Japan is Tokyo.\\nuser: What\"\n                        \" is the capital of South Korea?\\n</history>\",\n                        \"type\": \"text\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": [\n                    {\n                        \"id\": \"2\",\n                        \"type\": \"tool_use\",\n                        \"name\": \"get_capital\",\n                        \"input\": {\n                            \"country\": \"South Korea\",\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"tool_result\",\n                        \"tool_use_id\": \"2\",\n                        \"content\": [\n                            {\n                                \"type\": \"text\",\n                                \"text\": \"The capital of South Korea is Seoul.\",\n                            },\n                        ],\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"text\": \"<history>\\nassistant:\"\n                        \" The capital of South Korea is Seoul.\"\n                        \"\\n</history>\",\n                        \"type\": \"text\",\n                    },\n                ],\n            },\n        ]\n\n    async def test_chat_formatter(self) -> None:\n        \"\"\"Test the chat formatter.\"\"\"\n        formatter = AnthropicChatFormatter()\n\n        # Full history\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation, *self.msgs_tools],\n        )\n\n        self.assertListEqual(self.ground_truth_chat, res)\n\n        # Without system message\n        res = await formatter.format(\n            [*self.msgs_conversation, *self.msgs_tools],\n        )\n\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat[1:],\n        )\n\n        # Without conversation messages\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat[:1]\n            + self.ground_truth_chat[\n                1\n                + len(self.msgs_conversation) : 1\n                + len(self.msgs_conversation)\n                + len(self.msgs_tools)\n            ],\n        )\n\n        # Without tool messages\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat[: 1 + len(self.msgs_conversation)],\n        )\n\n        # Empty messages\n        res = await formatter.format([])\n        self.assertListEqual(res, [])\n\n    async def test_multiagent_formater(self) -> None:\n        \"\"\"Test the multi-agent formatter.\"\"\"\n        formatter = AnthropicMultiAgentFormatter()\n\n        # system + conversation + tools + conversation + tools\n        res = await formatter.format(\n            [\n                *self.msgs_system,\n                *self.msgs_conversation,\n                *self.msgs_tools,\n                *self.msgs_conversation_2,\n                *self.msgs_tools_2,\n            ],\n        )\n\n        self.assertListEqual(res, self.ground_truth_multiagent_2)\n\n        # system + conversation + tools + conversation\n        res = await formatter.format(\n            [\n                *self.msgs_system,\n                *self.msgs_conversation,\n                *self.msgs_tools,\n                *self.msgs_conversation_2,\n            ],\n        )\n\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent_2[: -len(self.msgs_tools_2)],\n        )\n\n        # system + conversation + tools\n        res = await formatter.format(\n            [\n                *self.msgs_system,\n                *self.msgs_conversation,\n                *self.msgs_tools,\n            ],\n        )\n\n        self.assertListEqual(res, self.ground_truth_multiagent)\n\n        # Without system message\n        res = await formatter.format(\n            [*self.msgs_conversation, *self.msgs_tools],\n        )\n\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent[\n                1 : 1 + len(self.msgs_conversation) + len(self.msgs_tools) - 2\n            ],\n        )\n\n        # Without conversation messages\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent_without_first_conversation,\n        )\n\n        # Without tool messages\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent[:2],\n        )\n\n        # With only system message\n        res = await formatter.format(self.msgs_system)\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent[:1],\n        )\n\n        # With only conversation messages\n        res = await formatter.format(self.msgs_conversation)\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent[1 : -len(self.msgs_tools)],\n        )\n\n        # With only tool messages\n        res = await formatter.format(self.msgs_tools)\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent_without_first_conversation[1:],\n        )\n"
  },
  {
    "path": "tests/formatter_dashscope_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The dashscope formatter unittests.\"\"\"\nimport os\nfrom unittest.async_case import IsolatedAsyncioTestCase\nfrom unittest.mock import patch, MagicMock\n\nfrom agentscope.formatter import (\n    DashScopeMultiAgentFormatter,\n    DashScopeChatFormatter,\n)\nfrom agentscope.message import (\n    Msg,\n    ToolUseBlock,\n    ToolResultBlock,\n    TextBlock,\n    ImageBlock,\n    AudioBlock,\n    VideoBlock,\n    URLSource,\n    Base64Source,\n)\n\n\nclass TestDashScopeFormatter(IsolatedAsyncioTestCase):\n    \"\"\"Unittest for the DashScopeFormatter.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the test environment.\"\"\"\n        self.image_path = \"./image.png\"\n        with open(self.image_path, \"wb\") as f:\n            f.write(b\"fake image content\")\n\n        self.mock_audio_path = (\n            \"/var/folders/gf/krg8x_ws409cpw_46b2s6rjc0000gn/T/tmpfymnv2w9.wav\"\n        )\n        self.mock_video_path = (\n            \"/var/folders/gf/krg8x_ws409cpw_46b2s6rjc0000gn/T/tmpfymnv2w9.mp4\"\n        )\n\n        self.msgs_system = [\n            Msg(\n                \"system\",\n                \"You're a helpful assistant.\",\n                \"system\",\n            ),\n        ]\n        self.msgs_conversation = [\n            Msg(\n                \"user\",\n                [\n                    TextBlock(\n                        type=\"text\",\n                        text=\"What is the capital of France?\",\n                    ),\n                    ImageBlock(\n                        type=\"image\",\n                        source=URLSource(\n                            type=\"url\",\n                            url=self.image_path,\n                        ),\n                    ),\n                ],\n                \"user\",\n            ),\n            Msg(\n                \"assistant\",\n                \"The capital of France is Paris.\",\n                \"assistant\",\n            ),\n            Msg(\n                \"user\",\n                [\n                    TextBlock(\n                        type=\"text\",\n                        text=\"What is the capital of Germany?\",\n                    ),\n                    AudioBlock(\n                        type=\"audio\",\n                        source=URLSource(\n                            type=\"url\",\n                            url=\"https://example.com/audio1.mp3\",\n                        ),\n                    ),\n                ],\n                \"user\",\n            ),\n            Msg(\n                \"assistant\",\n                \"The capital of Germany is Berlin.\",\n                \"assistant\",\n            ),\n            Msg(\n                \"user\",\n                \"What is the capital of Japan?\",\n                \"user\",\n            ),\n        ]\n\n        self.msgs_tools = [\n            Msg(\n                \"assistant\",\n                [\n                    ToolUseBlock(\n                        type=\"tool_use\",\n                        id=\"1\",\n                        name=\"get_capital\",\n                        input={\"country\": \"Japan\"},\n                    ),\n                ],\n                \"assistant\",\n            ),\n            Msg(\n                \"system\",\n                [\n                    ToolResultBlock(\n                        type=\"tool_result\",\n                        id=\"1\",\n                        name=\"get_capital\",\n                        output=[\n                            TextBlock(\n                                type=\"text\",\n                                text=\"The capital of Japan is Tokyo.\",\n                            ),\n                            ImageBlock(\n                                type=\"image\",\n                                source=URLSource(\n                                    type=\"url\",\n                                    url=self.image_path,\n                                ),\n                            ),\n                            AudioBlock(\n                                type=\"audio\",\n                                source=Base64Source(\n                                    type=\"base64\",\n                                    media_type=\"audio/wav\",\n                                    data=\"ZmFrZSBhdWRpbyBjb250ZW50\",\n                                ),\n                            ),\n                            VideoBlock(\n                                type=\"video\",\n                                source=Base64Source(\n                                    type=\"base64\",\n                                    media_type=\"video/mp4\",\n                                    data=\"ZmFrZSB2aWRlbyBjb250ZW50\",\n                                ),\n                            ),\n                        ],\n                    ),\n                ],\n                \"system\",\n            ),\n            Msg(\n                \"assistant\",\n                \"The capital of Japan is Tokyo.\",\n                \"assistant\",\n            ),\n        ]\n\n        self.msgs_conversation_2 = [\n            Msg(\n                \"user\",\n                \"What is the capital of South Korea?\",\n                \"user\",\n            ),\n        ]\n\n        self.msgs_tools_2 = [\n            Msg(\n                \"assistant\",\n                [\n                    ToolUseBlock(\n                        type=\"tool_use\",\n                        id=\"1\",\n                        name=\"get_capital\",\n                        input={\"country\": \"South Korea\"},\n                    ),\n                ],\n                \"assistant\",\n            ),\n            Msg(\n                \"system\",\n                [\n                    ToolResultBlock(\n                        type=\"tool_result\",\n                        id=\"2\",\n                        name=\"get_capital\",\n                        output=[\n                            TextBlock(\n                                type=\"text\",\n                                text=\"The capital of South Korea is Seoul.\",\n                            ),\n                            ImageBlock(\n                                type=\"image\",\n                                source=URLSource(\n                                    type=\"url\",\n                                    url=self.image_path,\n                                ),\n                            ),\n                            AudioBlock(\n                                type=\"audio\",\n                                source=Base64Source(\n                                    type=\"base64\",\n                                    media_type=\"audio/wav\",\n                                    data=\"ZmFrZSBhdWRpbyBjb250ZW50\",\n                                ),\n                            ),\n                        ],\n                    ),\n                ],\n                \"system\",\n            ),\n            Msg(\n                \"assistant\",\n                \"The capital of South Korea is Seoul.\",\n                \"assistant\",\n            ),\n        ]\n\n        self.ground_truth_chat = [\n            {\n                \"role\": \"system\",\n                \"content\": \"You're a helpful assistant.\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"text\": \"What is the capital of France?\",\n                    },\n                    {\n                        \"image\": f\"file://{os.path.abspath(self.image_path)}\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": \"The capital of France is Paris.\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"text\": \"What is the capital of Germany?\",\n                    },\n                    {\n                        \"audio\": \"https://example.com/audio1.mp3\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": \"The capital of Germany is Berlin.\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"What is the capital of Japan?\",\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": [],\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": '{\"country\": \"Japan\"}',\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"- The capital of Japan is Tokyo.\\n\"\n                \"- The returned image can be found at: ./image.png\"\n                \"\\n- The returned audio can be found at: \"\n                f\"{self.mock_audio_path}\"\n                f\"\\n- The returned video can be found at: \"\n                f\"{self.mock_video_path}\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": \"The capital of Japan is Tokyo.\",\n            },\n        ]\n\n        self.ground_truth_multiagent = [\n            {\n                \"role\": \"system\",\n                \"content\": \"You're a helpful assistant.\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"text\": \"# Conversation History\\nThe content between\"\n                        \" <history></history> tags contains your\"\n                        \" conversation history\\n<history>\\nuser: What\"\n                        \" is the capital of France?\",\n                    },\n                    {\n                        \"image\": f\"file://{os.path.abspath(self.image_path)}\",\n                    },\n                    {\n                        \"text\": \"assistant: The capital of France is Paris.\"\n                        \"\\nuser: What is the capital of Germany?\",\n                    },\n                    {\n                        \"audio\": \"https://example.com/audio1.mp3\",\n                    },\n                    {\n                        \"text\": \"assistant: The capital of Germany is Berlin.\"\n                        \"\\nuser: What is the capital of Japan?\"\n                        \"\\n</history>\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": [],\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": '{\"country\": \"Japan\"}',\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"- The capital of Japan is Tokyo.\\n- The returned\"\n                \" image can be found at: ./image.png\\n- The\"\n                \" returned audio can be found at: \"\n                f\"{self.mock_audio_path}\\n\"\n                f\"- The returned video can be found at: \"\n                f\"{self.mock_video_path}\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"<history>\\nassistant:\"\n                \" The capital of Japan is Tokyo.\\n</history>\",\n            },\n        ]\n\n        self.ground_truth_multiagent_without_first_conversation = [\n            {\n                \"role\": \"system\",\n                \"content\": \"You're a helpful assistant.\",\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": [],\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": '{\"country\": \"Japan\"}',\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"- The capital of Japan is Tokyo.\\n- The returned\"\n                \" image can be found at: ./image.png\\n- The\"\n                \" returned audio can be found at: \"\n                f\"{self.mock_audio_path}\\n\"\n                f\"- The returned video can be found at: \"\n                f\"{self.mock_video_path}\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"# Conversation History\\nThe content between\"\n                \" <history></history> tags contains your\"\n                \" conversation history\\n<history>\\nassistant:\"\n                \" The capital of Japan is Tokyo.\\n</history>\",\n            },\n        ]\n\n        self.ground_truth_multiagent_2 = [\n            {\n                \"role\": \"system\",\n                \"content\": \"You're a helpful assistant.\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"text\": \"# Conversation History\\nThe content between \"\n                        \"<history></history> tags contains your conversation \"\n                        \"history\\n<history>\\nuser: What is the capital of \"\n                        \"France?\",\n                    },\n                    {\n                        \"image\": f\"file://{os.path.abspath(self.image_path)}\",\n                    },\n                    {\n                        \"text\": \"assistant: The capital of France is Paris.\"\n                        \"\\nuser: What is the capital of Germany?\",\n                    },\n                    {\n                        \"audio\": \"https://example.com/audio1.mp3\",\n                    },\n                    {\n                        \"text\": \"assistant: The capital of Germany is Berlin.\"\n                        \"\\nuser: What is the capital of Japan?\"\n                        \"\\n</history>\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": [],\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": '{\"country\": \"Japan\"}',\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"- The capital of Japan is Tokyo.\\n- The returned\"\n                \" image can be found at: ./image.png\\n- The returned audio can\"\n                f\" be found at: {self.mock_audio_path}\"\n                f\"\\n- The returned video can be found at: \"\n                f\"{self.mock_video_path}\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"<history>\\nassistant: The capital of Japan \"\n                \"is Tokyo.\\nuser: What is the capital of South Korea?\"\n                \"\\n</history>\",\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": [],\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": '{\"country\": \"South Korea\"}',\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"2\",\n                \"content\": \"- The capital of South Korea is Seoul.\\n- The \"\n                \"returned image can be found at: ./image.png\\n- The returned\"\n                \" audio can be found at: \"\n                f\"{self.mock_audio_path}\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"<history>\\nassistant: The capital of South\"\n                \" Korea is Seoul.\\n</history>\",\n            },\n        ]\n\n    def _mock_save_base64_data(\n        self,\n        media_type: str,\n        _base64_data: str,\n    ) -> str:\n        \"\"\"Mock function for _save_base64_data that returns different paths\n        based on media_type.\n\n        Args:\n            media_type: The MIME type of the media (e.g., \"audio/wav\",\n            \"video/mp4\")\n            _base64_data: The base64 encoded data (not used in mock)\n\n        Returns:\n            The mock file path for the media type\n        \"\"\"\n        if \"audio\" in media_type:\n            return self.mock_audio_path\n        elif \"video\" in media_type:\n            return self.mock_video_path\n        else:\n            return self.mock_audio_path  # fallback\n\n    @patch(\"agentscope.formatter._formatter_base._save_base64_data\")\n    async def test_chat_formatter(\n        self,\n        mock_save_base64_data: MagicMock,\n    ) -> None:\n        \"\"\"Test the chat formatter.\"\"\"\n        mock_save_base64_data.side_effect = self._mock_save_base64_data\n\n        formatter = DashScopeChatFormatter()\n\n        # Full history\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation, *self.msgs_tools],\n        )\n        self.assertListEqual(self.ground_truth_chat, res)\n\n        # Without system message\n        res = await formatter.format(\n            [*self.msgs_conversation, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat[1:],\n        )\n\n        # Without conversation messages\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat[:1]\n            + self.ground_truth_chat[-len(self.msgs_tools) :],\n        )\n\n        # Without tool messages\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat[: -len(self.msgs_tools)],\n        )\n\n        # Empty messages\n        res = await formatter.format([])\n        self.assertListEqual(res, [])\n\n    @patch(\"agentscope.formatter._formatter_base._save_base64_data\")\n    async def test_chat_formatter_with_extract_media_blocks(\n        self,\n        mock_save_base64_data: MagicMock,\n    ) -> None:\n        \"\"\"Test the chat formatter with promote_tool_result_images=True.\"\"\"\n        mock_save_base64_data.side_effect = self._mock_save_base64_data\n\n        formatter = DashScopeChatFormatter(\n            promote_tool_result_images=True,\n            promote_tool_result_audios=True,\n            promote_tool_result_videos=True,\n        )\n\n        # Test with tool result containing image blocks\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation, *self.msgs_tools],\n        )\n\n        # Expected result: image blocks should be extracted and inserted\n        # as a separate user message after the tool result message\n        expected_result = [\n            {\n                \"role\": \"system\",\n                \"content\": \"You're a helpful assistant.\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"text\": \"What is the capital of France?\",\n                    },\n                    {\n                        \"image\": f\"file://{os.path.abspath(self.image_path)}\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": \"The capital of France is Paris.\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"text\": \"What is the capital of Germany?\",\n                    },\n                    {\n                        \"audio\": \"https://example.com/audio1.mp3\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": \"The capital of Germany is Berlin.\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"What is the capital of Japan?\",\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": [],\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": '{\"country\": \"Japan\"}',\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"- The capital of Japan is Tokyo.\\n\"\n                \"- The returned image can be found at: ./image.png\"\n                \"\\n- The returned audio can be found at: \"\n                f\"{self.mock_audio_path}\"\n                f\"\\n- The returned video can be found at: \"\n                f\"{self.mock_video_path}\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"text\": \"<system-info>The following are \"\n                        \"the media contents from the tool \"\n                        \"result of 'get_capital':\",\n                    },\n                    {\n                        \"text\": \"\\n- The image from './image.png': \",\n                    },\n                    {\n                        \"image\": f\"file://{os.path.abspath(self.image_path)}\",\n                    },\n                    {\n                        \"text\": \"\\n- The audio from \"\n                        f\"'{self.mock_audio_path}': \",\n                    },\n                    {\n                        \"audio\": self.mock_audio_path,\n                    },\n                    {\n                        \"text\": \"\\n- The video from \"\n                        f\"'{self.mock_video_path}': \",\n                    },\n                    {\n                        \"video\": self.mock_video_path,\n                    },\n                    {\n                        \"text\": \"</system-info>\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": \"The capital of Japan is Tokyo.\",\n            },\n        ]\n\n        self.assertListEqual(expected_result, res)\n\n    @patch(\"agentscope.formatter._formatter_base._save_base64_data\")\n    async def test_multiagent_formater(\n        self,\n        mock_save_base64_data: MagicMock,\n    ) -> None:\n        \"\"\"Test the multi-agent formatter.\"\"\"\n        mock_save_base64_data.side_effect = self._mock_save_base64_data\n\n        formatter = DashScopeMultiAgentFormatter()\n\n        # system + conversation + tools + conversation + tools\n        res = await formatter.format(\n            [\n                *self.msgs_system,\n                *self.msgs_conversation,\n                *self.msgs_tools,\n                *self.msgs_conversation_2,\n                *self.msgs_tools_2,\n            ],\n        )\n\n        self.assertListEqual(res, self.ground_truth_multiagent_2)\n\n        # system + conversation + tools + conversation\n        res = await formatter.format(\n            [\n                *self.msgs_system,\n                *self.msgs_conversation,\n                *self.msgs_tools,\n                *self.msgs_conversation_2,\n            ],\n        )\n\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent_2[: -len(self.msgs_tools_2)],\n        )\n\n        # system + conversation + tools\n        res = await formatter.format(\n            [\n                *self.msgs_system,\n                *self.msgs_conversation,\n                *self.msgs_tools,\n            ],\n        )\n\n        self.assertListEqual(res, self.ground_truth_multiagent)\n\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation, *self.msgs_tools],\n        )\n        self.assertListEqual(res, self.ground_truth_multiagent)\n\n        # Without system message\n        res = await formatter.format(\n            [*self.msgs_conversation, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent[1:],\n        )\n\n        # Without conversation messages\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent_without_first_conversation,\n        )\n\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent[:2],\n        )\n\n        # With only system message\n        res = await formatter.format(self.msgs_system)\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent[:1],\n        )\n\n        # With only conversation messages\n        res = await formatter.format(self.msgs_conversation)\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent[1 : -len(self.msgs_tools)],\n        )\n\n        # Without only tool messages\n        res = await formatter.format(self.msgs_tools)\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent_without_first_conversation[1:],\n        )\n\n    @patch(\"agentscope.formatter._formatter_base._save_base64_data\")\n    async def test_multiagent_formatter_with_promote_media_tool_result(\n        self,\n        mock_save_base64_data: MagicMock,\n    ) -> None:\n        \"\"\"Test the multi-agent formatter with\n        promote_tool_result_images=True.\"\"\"\n        mock_save_base64_data.side_effect = self._mock_save_base64_data\n\n        formatter = DashScopeMultiAgentFormatter(\n            promote_tool_result_images=True,\n            promote_tool_result_audios=True,\n            promote_tool_result_videos=True,\n        )\n\n        # Test with tool result containing image blocks\n        res = await formatter.format(\n            [\n                *self.msgs_system,\n                *self.msgs_conversation,\n                *self.msgs_tools,\n            ],\n        )\n\n        # Expected result: image blocks should be promoted and inserted\n        # as a separate user message after the tool result message\n        expected_result = [\n            {\n                \"role\": \"system\",\n                \"content\": \"You're a helpful assistant.\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"text\": \"# Conversation History\\nThe content between\"\n                        \" <history></history> tags contains your\"\n                        \" conversation history\\n<history>\\nuser: What\"\n                        \" is the capital of France?\",\n                    },\n                    {\n                        \"image\": f\"file://{os.path.abspath(self.image_path)}\",\n                    },\n                    {\n                        \"text\": \"assistant: The capital of France is Paris.\"\n                        \"\\nuser: What is the capital of Germany?\",\n                    },\n                    {\n                        \"audio\": \"https://example.com/audio1.mp3\",\n                    },\n                    {\n                        \"text\": \"assistant: The capital of Germany is Berlin.\"\n                        \"\\nuser: What is the capital of Japan?\"\n                        \"\\n</history>\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": [],\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": '{\"country\": \"Japan\"}',\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"- The capital of Japan is Tokyo.\\n- The returned\"\n                \" image can be found at: ./image.png\\n- The\"\n                \" returned audio can be found at: \"\n                f\"{self.mock_audio_path}\"\n                f\"\\n- The returned video can be found at: \"\n                f\"{self.mock_video_path}\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"text\": \"<system-info>The following are \"\n                        \"the media contents from the tool \"\n                        \"result of 'get_capital':\",\n                    },\n                    {\n                        \"text\": \"\\n- The image from './image.png': \",\n                    },\n                    {\n                        \"image\": f\"file://{os.path.abspath(self.image_path)}\",\n                    },\n                    {\n                        \"text\": \"\\n- The audio from \"\n                        f\"'{self.mock_audio_path}': \",\n                    },\n                    {\n                        \"audio\": self.mock_audio_path,\n                    },\n                    {\n                        \"text\": \"\\n- The video from \"\n                        f\"'{self.mock_video_path}': \",\n                    },\n                    {\n                        \"video\": self.mock_video_path,\n                    },\n                    {\n                        \"text\": \"</system-info>\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"<history>\\nassistant:\"\n                \" The capital of Japan is Tokyo.\\n</history>\",\n            },\n        ]\n\n        self.maxDiff = None\n        self.assertListEqual(expected_result, res)\n\n    async def asyncTearDown(self) -> None:\n        \"\"\"Clean up the test environment.\"\"\"\n        if os.path.exists(self.image_path):\n            os.remove(self.image_path)\n"
  },
  {
    "path": "tests/formatter_deepseek_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The DeepSeek formatter unittests.\"\"\"\nimport unittest\nfrom unittest.async_case import IsolatedAsyncioTestCase\n\nfrom agentscope.formatter import (\n    DeepSeekChatFormatter,\n    DeepSeekMultiAgentFormatter,\n)\nfrom agentscope.message import (\n    Msg,\n    TextBlock,\n    ToolUseBlock,\n    ToolResultBlock,\n)\n\n\nclass TestDeepSeekFormatter(IsolatedAsyncioTestCase):\n    \"\"\"Unittest for the DeepSeek formatter.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the test environment.\"\"\"\n\n        self.msgs_system = [\n            Msg(\n                \"system\",\n                \"You're a helpful assistant.\",\n                \"system\",\n            ),\n        ]\n        self.msgs_conversation = [\n            Msg(\n                \"user\",\n                [\n                    TextBlock(\n                        type=\"text\",\n                        text=\"What is the capital of France?\",\n                    ),\n                ],\n                \"user\",\n            ),\n            Msg(\n                \"assistant\",\n                \"The capital of France is Paris.\",\n                \"assistant\",\n            ),\n            Msg(\n                \"user\",\n                \"What is the capital of Japan?\",\n                \"user\",\n            ),\n        ]\n\n        self.msgs_tools = [\n            Msg(\n                \"assistant\",\n                [\n                    ToolUseBlock(\n                        type=\"tool_use\",\n                        id=\"1\",\n                        name=\"get_capital\",\n                        input={\"country\": \"Japan\"},\n                    ),\n                ],\n                \"assistant\",\n            ),\n            Msg(\n                \"system\",\n                [\n                    ToolResultBlock(\n                        type=\"tool_result\",\n                        id=\"1\",\n                        name=\"get_capital\",\n                        output=[\n                            TextBlock(\n                                type=\"text\",\n                                text=\"The capital of Japan is Tokyo.\",\n                            ),\n                        ],\n                    ),\n                ],\n                \"system\",\n            ),\n            Msg(\n                \"assistant\",\n                \"The capital of Japan is Tokyo.\",\n                \"assistant\",\n            ),\n        ]\n\n        self.msgs_conversation_2 = [\n            Msg(\n                \"user\",\n                \"What is the capital of South Korea?\",\n                \"user\",\n            ),\n        ]\n\n        self.msgs_tools_2 = [\n            Msg(\n                \"assistant\",\n                [\n                    ToolUseBlock(\n                        type=\"tool_use\",\n                        id=\"2\",\n                        name=\"get_capital\",\n                        input={\"country\": \"South Korea\"},\n                    ),\n                ],\n                \"assistant\",\n            ),\n            Msg(\n                \"system\",\n                [\n                    ToolResultBlock(\n                        type=\"tool_result\",\n                        id=\"2\",\n                        name=\"get_capital\",\n                        output=[\n                            TextBlock(\n                                type=\"text\",\n                                text=\"The capital of South Korea is Seoul.\",\n                            ),\n                        ],\n                    ),\n                ],\n                \"system\",\n            ),\n            Msg(\n                \"assistant\",\n                \"The capital of Japan is Tokyo.\",\n                \"assistant\",\n            ),\n        ]\n\n        self.ground_truth_chat = [\n            {\n                \"role\": \"system\",\n                \"content\": \"You're a helpful assistant.\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"What is the capital of France?\",\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": \"The capital of France is Paris.\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"What is the capital of Japan?\",\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": None,\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": '{\"country\": \"Japan\"}',\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"The capital of Japan is Tokyo.\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": \"The capital of Japan is Tokyo.\",\n            },\n        ]\n\n        self.ground_truth_multiagent = [\n            {\n                \"role\": \"system\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"You're a helpful assistant.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"# Conversation History\\nThe content between\"\n                \" <history></history> tags contains your\"\n                \" conversation history\\n<history>\\nuser: What is\"\n                \" the capital of France?\\nassistant: The capital\"\n                \" of France is Paris.\\nuser: What is the capital\"\n                \" of Japan?\\n</history>\",\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": None,\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": '{\"country\": \"Japan\"}',\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"The capital of Japan is Tokyo.\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"<history>\\nassistant:\"\n                \" The capital of Japan is Tokyo.\\n</history>\",\n            },\n        ]\n\n        self.ground_truth_multiagent_without_first_conversation = [\n            {\n                \"role\": \"system\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"You're a helpful assistant.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": None,\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": '{\"country\": \"Japan\"}',\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"The capital of Japan is Tokyo.\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"# Conversation History\\nThe content between\"\n                \" <history></history> tags contains your\"\n                \" conversation history\\n<history>\\nassistant:\"\n                \" The capital of Japan is Tokyo.\\n</history>\",\n            },\n        ]\n\n        self.ground_truth_multiagent_2 = [\n            {\n                \"role\": \"system\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"You're a helpful assistant.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"# Conversation History\\nThe content between \"\n                \"<history></history> tags contains your conversation history\"\n                \"\\n<history>\\nuser: What is the capital of France?\"\n                \"\\nassistant: The capital of France is Paris.\"\n                \"\\nuser: What is the capital of Japan?\\n</history>\",\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": None,\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": '{\"country\": \"Japan\"}',\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"The capital of Japan is Tokyo.\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"<history>\\nassistant: The capital of Japan is \"\n                \"Tokyo.\\nuser: What is the capital of South Korea?\"\n                \"\\n</history>\",\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": None,\n                \"tool_calls\": [\n                    {\n                        \"id\": \"2\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": '{\"country\": \"South Korea\"}',\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"2\",\n                \"content\": \"The capital of South Korea is Seoul.\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"<history>\\nassistant: The capital of Japan is\"\n                \" Tokyo.\\n</history>\",\n            },\n        ]\n\n    async def test_chat_formatter(self) -> None:\n        \"\"\"Test the DeepSeek chat formatter.\"\"\"\n        formatter = DeepSeekChatFormatter()\n\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation, *self.msgs_tools],\n        )\n\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat,\n        )\n\n        # Without system message\n        res = await formatter.format(\n            [*self.msgs_conversation, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat[1:],\n        )\n\n        # Without conversation messages\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat[:1]\n            + self.ground_truth_chat[-len(self.msgs_tools) :],\n        )\n\n        # Without tools messages\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat[: -len(self.msgs_tools)],\n        )\n\n    async def test_multi_agent_formatter(\n        self,\n    ) -> None:\n        \"\"\"Test the DeepSeek multi-agent formatter.\"\"\"\n\n        formatter = DeepSeekMultiAgentFormatter()\n\n        # system + conversation + tools + conversation + tools\n        res = await formatter.format(\n            [\n                *self.msgs_system,\n                *self.msgs_conversation,\n                *self.msgs_tools,\n                *self.msgs_conversation_2,\n                *self.msgs_tools_2,\n            ],\n        )\n\n        self.assertListEqual(res, self.ground_truth_multiagent_2)\n\n        # system + conversation + tools + conversation\n        res = await formatter.format(\n            [\n                *self.msgs_system,\n                *self.msgs_conversation,\n                *self.msgs_tools,\n                *self.msgs_conversation_2,\n            ],\n        )\n\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent_2[: -len(self.msgs_tools_2)],\n        )\n\n        # system + conversation + tools\n        res = await formatter.format(\n            [\n                *self.msgs_system,\n                *self.msgs_conversation,\n                *self.msgs_tools,\n            ],\n        )\n\n        self.assertListEqual(res, self.ground_truth_multiagent)\n\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation, *self.msgs_tools],\n        )\n\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent,\n        )\n\n        res = await formatter.format(\n            [*self.msgs_conversation, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent[1:],\n        )\n\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent_without_first_conversation,\n        )\n\n        # Only system message\n        res = await formatter.format(self.msgs_system)\n        self.assertListEqual(res, self.ground_truth_multiagent[:1])\n\n        # Only conversation messages\n        res = await formatter.format(self.msgs_conversation)\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent[1 : -len(self.msgs_tools)],\n        )\n\n        # Only tools messages\n        res = await formatter.format(self.msgs_tools)\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent_without_first_conversation[1:],\n        )\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/formatter_gemini_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The gemini formatter unittests.\"\"\"\nimport os\nfrom unittest.async_case import IsolatedAsyncioTestCase\nfrom unittest.mock import patch, MagicMock\n\nfrom agentscope.formatter import GeminiChatFormatter, GeminiMultiAgentFormatter\nfrom agentscope.message import (\n    Msg,\n    URLSource,\n    TextBlock,\n    AudioBlock,\n    ImageBlock,\n    ToolUseBlock,\n    ToolResultBlock,\n    Base64Source,\n)\n\n\nclass TestGeminiFormatter(IsolatedAsyncioTestCase):\n    \"\"\"Unittest for the Gemini formatter.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the test environment.\"\"\"\n        self.image_path = \"./image.png\"\n        with open(self.image_path, \"wb\") as f:\n            f.write(b\"fake image content\")\n\n        self.mock_audio_path = (\n            \"/var/folders/gf/krg8x_ws409cpw_46b2s6rjc0000gn/T/tmpfymnv2w9.wav\"\n        )\n\n        self.audio_path = \"./audio.mp3\"\n        with open(self.audio_path, \"wb\") as f:\n            f.write(b\"fake audio content\")\n\n        self.msgs_system = [\n            Msg(\n                \"system\",\n                \"You're a helpful assistant.\",\n                \"system\",\n            ),\n        ]\n        self.msgs_conversation = [\n            Msg(\n                \"user\",\n                [\n                    TextBlock(\n                        type=\"text\",\n                        text=\"What is the capital of France?\",\n                    ),\n                    ImageBlock(\n                        type=\"image\",\n                        source=URLSource(\n                            type=\"url\",\n                            url=self.image_path,\n                        ),\n                    ),\n                ],\n                \"user\",\n            ),\n            Msg(\n                \"assistant\",\n                \"The capital of France is Paris.\",\n                \"assistant\",\n            ),\n            Msg(\n                \"user\",\n                [\n                    TextBlock(\n                        type=\"text\",\n                        text=\"What is the capital of Germany?\",\n                    ),\n                    AudioBlock(\n                        type=\"audio\",\n                        source=URLSource(\n                            type=\"url\",\n                            url=self.audio_path,\n                        ),\n                    ),\n                ],\n                \"user\",\n            ),\n            Msg(\n                \"assistant\",\n                \"The capital of Germany is Berlin.\",\n                \"assistant\",\n            ),\n            Msg(\n                \"user\",\n                \"What is the capital of Japan?\",\n                \"user\",\n            ),\n        ]\n\n        self.msgs_tools = [\n            Msg(\n                \"assistant\",\n                [\n                    ToolUseBlock(\n                        type=\"tool_use\",\n                        id=\"1\",\n                        name=\"get_capital\",\n                        input={\"country\": \"Japan\"},\n                    ),\n                ],\n                \"assistant\",\n            ),\n            Msg(\n                \"system\",\n                [\n                    ToolResultBlock(\n                        type=\"tool_result\",\n                        id=\"1\",\n                        name=\"get_capital\",\n                        output=[\n                            TextBlock(\n                                type=\"text\",\n                                text=\"The capital of Japan is Tokyo.\",\n                            ),\n                            ImageBlock(\n                                type=\"image\",\n                                source=URLSource(\n                                    type=\"url\",\n                                    url=self.image_path,\n                                ),\n                            ),\n                            AudioBlock(\n                                type=\"audio\",\n                                source=Base64Source(\n                                    type=\"base64\",\n                                    media_type=\"audio/wav\",\n                                    data=\"ZmFrZSBhdWRpbyBjb250ZW50\",\n                                ),\n                            ),\n                        ],\n                    ),\n                ],\n                \"system\",\n            ),\n            Msg(\n                \"assistant\",\n                \"The capital of Japan is Tokyo.\",\n                \"assistant\",\n            ),\n        ]\n\n        self.msgs_conversation_2 = [\n            Msg(\n                \"user\",\n                \"What is the capital of South Korea?\",\n                \"user\",\n            ),\n        ]\n\n        self.msgs_tools_2 = [\n            Msg(\n                \"assistant\",\n                [\n                    ToolUseBlock(\n                        type=\"tool_use\",\n                        id=\"2\",\n                        name=\"get_capital\",\n                        input={\"country\": \"South Korea\"},\n                    ),\n                ],\n                \"assistant\",\n            ),\n            Msg(\n                \"system\",\n                [\n                    ToolResultBlock(\n                        type=\"tool_result\",\n                        id=\"2\",\n                        name=\"get_capital\",\n                        output=[\n                            TextBlock(\n                                type=\"text\",\n                                text=\"The capital of South Korea is Seoul.\",\n                            ),\n                            ImageBlock(\n                                type=\"image\",\n                                source=URLSource(\n                                    type=\"url\",\n                                    url=self.image_path,\n                                ),\n                            ),\n                            AudioBlock(\n                                type=\"audio\",\n                                source=Base64Source(\n                                    type=\"base64\",\n                                    media_type=\"audio/wav\",\n                                    data=\"ZmFrZSBhdWRpbyBjb250ZW50\",\n                                ),\n                            ),\n                        ],\n                    ),\n                ],\n                \"system\",\n            ),\n            Msg(\n                \"assistant\",\n                \"The capital of South Korea is Seoul.\",\n                \"assistant\",\n            ),\n        ]\n\n        self.ground_truth_chat = [\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"You're a helpful assistant.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"What is the capital of France?\",\n                    },\n                    {\n                        \"inline_data\": {\n                            \"data\": \"ZmFrZSBpbWFnZSBjb250ZW50\",\n                            \"mime_type\": \"image/png\",\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"model\",\n                \"parts\": [\n                    {\n                        \"text\": \"The capital of France is Paris.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"What is the capital of Germany?\",\n                    },\n                    {\n                        \"inline_data\": {\n                            \"data\": \"ZmFrZSBhdWRpbyBjb250ZW50\",\n                            \"mime_type\": \"audio/mp3\",\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"model\",\n                \"parts\": [\n                    {\n                        \"text\": \"The capital of Germany is Berlin.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"What is the capital of Japan?\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"model\",\n                \"parts\": [\n                    {\n                        \"function_call\": {\n                            \"id\": None,\n                            \"name\": \"get_capital\",\n                            \"args\": {\n                                \"country\": \"Japan\",\n                            },\n                        },\n                        \"thought_signature\": \"1\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"function_response\": {\n                            \"id\": \"1\",\n                            \"name\": \"get_capital\",\n                            \"response\": {\n                                \"output\": \"- The capital of Japan is Tokyo.\\n\"\n                                \"- The returned image can be found\"\n                                \" at: ./image.png\\n- The returned \"\n                                \"audio can be found at: \"\n                                f\"{self.mock_audio_path}\",\n                            },\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"model\",\n                \"parts\": [\n                    {\n                        \"text\": \"The capital of Japan is Tokyo.\",\n                    },\n                ],\n            },\n        ]\n        self.ground_truth_multiagent = [\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"You're a helpful assistant.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"# Conversation History\\nThe content between\"\n                        \" <history></history> tags contains your\"\n                        \" conversation history\\n<history>user: What\"\n                        \" is the capital of France?\",\n                    },\n                    {\n                        \"inline_data\": {\n                            \"data\": \"ZmFrZSBpbWFnZSBjb250ZW50\",\n                            \"mime_type\": \"image/png\",\n                        },\n                    },\n                    {\n                        \"text\": \"assistant: The capital of France is Paris.\"\n                        \"\\nuser: What is the capital of Germany?\",\n                    },\n                    {\n                        \"inline_data\": {\n                            \"data\": \"ZmFrZSBhdWRpbyBjb250ZW50\",\n                            \"mime_type\": \"audio/mp3\",\n                        },\n                    },\n                    {\n                        \"text\": \"assistant: The capital of Germany is Berlin.\"\n                        \"\\nuser: What is the capital of Japan?\"\n                        \"\\n</history>\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"model\",\n                \"parts\": [\n                    {\n                        \"function_call\": {\n                            \"id\": None,\n                            \"name\": \"get_capital\",\n                            \"args\": {\n                                \"country\": \"Japan\",\n                            },\n                        },\n                        \"thought_signature\": \"1\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"function_response\": {\n                            \"id\": \"1\",\n                            \"name\": \"get_capital\",\n                            \"response\": {\n                                \"output\": \"- The capital of Japan is Tokyo.\"\n                                \"\\n- The returned image can be found\"\n                                \" at: ./image.png\\n- The returned\"\n                                \" audio can be found at: \"\n                                f\"{self.mock_audio_path}\",\n                            },\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"<history>assistant:\"\n                        \" The capital of Japan is Tokyo.\\n</history>\",\n                    },\n                ],\n            },\n        ]\n\n        self.ground_truth_multiagent_without_first_conversation = [\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"You're a helpful assistant.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"model\",\n                \"parts\": [\n                    {\n                        \"function_call\": {\n                            \"id\": None,\n                            \"name\": \"get_capital\",\n                            \"args\": {\n                                \"country\": \"Japan\",\n                            },\n                        },\n                        \"thought_signature\": \"1\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"function_response\": {\n                            \"id\": \"1\",\n                            \"name\": \"get_capital\",\n                            \"response\": {\n                                \"output\": \"- The capital of Japan is Tokyo.\"\n                                \"\\n- The returned image can be found\"\n                                \" at: ./image.png\\n- The returned\"\n                                \" audio can be found at: \"\n                                f\"{self.mock_audio_path}\",\n                            },\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"# Conversation History\\nThe content between\"\n                        \" <history></history> tags contains your\"\n                        \" conversation history\\n<history>assistant:\"\n                        \" The capital of Japan is Tokyo.\\n</history>\",\n                    },\n                ],\n            },\n        ]\n\n        self.ground_truth_multiagent_2 = [\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"You're a helpful assistant.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"# Conversation History\\nThe content between \"\n                        \"<history></history> tags contains your \"\n                        \"conversation history\\n<history>user: What is \"\n                        \"the capital of France?\",\n                    },\n                    {\n                        \"inline_data\": {\n                            \"data\": \"ZmFrZSBpbWFnZSBjb250ZW50\",\n                            \"mime_type\": \"image/png\",\n                        },\n                    },\n                    {\n                        \"text\": \"assistant: The capital of France is Paris.\"\n                        \"\\nuser: What is the capital of Germany?\",\n                    },\n                    {\n                        \"inline_data\": {\n                            \"data\": \"ZmFrZSBhdWRpbyBjb250ZW50\",\n                            \"mime_type\": \"audio/mp3\",\n                        },\n                    },\n                    {\n                        \"text\": \"assistant: The capital of Germany is Berlin.\"\n                        \"\\nuser: What is the capital of Japan?\\n</history>\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"model\",\n                \"parts\": [\n                    {\n                        \"function_call\": {\n                            \"id\": None,\n                            \"name\": \"get_capital\",\n                            \"args\": {\n                                \"country\": \"Japan\",\n                            },\n                        },\n                        \"thought_signature\": \"1\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"function_response\": {\n                            \"id\": \"1\",\n                            \"name\": \"get_capital\",\n                            \"response\": {\n                                \"output\": \"- The capital of Japan is Tokyo.\"\n                                \"\\n- The returned image can be found \"\n                                \"at: ./image.png\\n- The returned audio\"\n                                \" can be found at: \"\n                                f\"{self.mock_audio_path}\",\n                            },\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"<history>assistant: \"\n                        \"The capital of Japan is Tokyo.\\nuser: What \"\n                        \"is the capital of South Korea?\\n</history>\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"model\",\n                \"parts\": [\n                    {\n                        \"function_call\": {\n                            \"id\": None,\n                            \"name\": \"get_capital\",\n                            \"args\": {\n                                \"country\": \"South Korea\",\n                            },\n                        },\n                        \"thought_signature\": \"2\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"function_response\": {\n                            \"id\": \"2\",\n                            \"name\": \"get_capital\",\n                            \"response\": {\n                                \"output\": \"- The capital of South Korea is \"\n                                \"Seoul.\\n- The returned image can \"\n                                \"be found at: ./image.png\\n- The \"\n                                \"returned audio can be found at: \"\n                                f\"{self.mock_audio_path}\",\n                            },\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"<history>assistant: \"\n                        \"The capital of South Korea is Seoul.\"\n                        \"\\n</history>\",\n                    },\n                ],\n            },\n        ]\n\n    @patch(\"agentscope.formatter._formatter_base._save_base64_data\")\n    async def test_chat_formatter(\n        self,\n        mock_save_base64_data: MagicMock,\n    ) -> None:\n        \"\"\"Test the gemini chat formatter.\"\"\"\n        mock_save_base64_data.return_value = self.mock_audio_path\n        formatter = GeminiChatFormatter()\n\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat,\n        )\n\n        # Without system message\n        res = await formatter.format(\n            [*self.msgs_conversation, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat[1:],\n        )\n\n        # Without conversation messages\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat[:1]\n            + self.ground_truth_chat[-len(self.msgs_tools) :],\n        )\n\n        # Without tools messages\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat[: -len(self.msgs_tools)],\n        )\n\n    @patch(\"agentscope.formatter._formatter_base._save_base64_data\")\n    async def test_chat_formatter_with_extract_image_blocks(\n        self,\n        mock_save_base64_data: MagicMock,\n    ) -> None:\n        \"\"\"Test the gemini chat formatter with\n        promote_tool_result_images=True.\"\"\"\n        mock_save_base64_data.return_value = self.mock_audio_path\n\n        formatter = GeminiChatFormatter(promote_tool_result_images=True)\n\n        # Test with tool result containing image blocks\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation, *self.msgs_tools],\n        )\n\n        # Expected result: image blocks should be extracted and inserted\n        # as a separate user message after the tool result message\n        expected_result = [\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"You're a helpful assistant.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"What is the capital of France?\",\n                    },\n                    {\n                        \"inline_data\": {\n                            \"data\": \"ZmFrZSBpbWFnZSBjb250ZW50\",\n                            \"mime_type\": \"image/png\",\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"model\",\n                \"parts\": [\n                    {\n                        \"text\": \"The capital of France is Paris.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"What is the capital of Germany?\",\n                    },\n                    {\n                        \"inline_data\": {\n                            \"data\": \"ZmFrZSBhdWRpbyBjb250ZW50\",\n                            \"mime_type\": \"audio/mp3\",\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"model\",\n                \"parts\": [\n                    {\n                        \"text\": \"The capital of Germany is Berlin.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"What is the capital of Japan?\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"model\",\n                \"parts\": [\n                    {\n                        \"function_call\": {\n                            \"id\": None,\n                            \"name\": \"get_capital\",\n                            \"args\": {\n                                \"country\": \"Japan\",\n                            },\n                        },\n                        \"thought_signature\": \"1\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"function_response\": {\n                            \"id\": \"1\",\n                            \"name\": \"get_capital\",\n                            \"response\": {\n                                \"output\": \"- The capital of Japan is Tokyo.\\n\"\n                                \"- The returned image can be found\"\n                                \" at: ./image.png\\n- The returned \"\n                                \"audio can be found at: \"\n                                f\"{self.mock_audio_path}\",\n                            },\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"<system-info>The following are \"\n                        \"the image contents from the tool \"\n                        \"result of 'get_capital':\",\n                    },\n                    {\n                        \"text\": \"\\n- The image from './image.png': \",\n                    },\n                    {\n                        \"inline_data\": {\n                            \"data\": \"ZmFrZSBpbWFnZSBjb250ZW50\",\n                            \"mime_type\": \"image/png\",\n                        },\n                    },\n                    {\n                        \"text\": \"</system-info>\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"model\",\n                \"parts\": [\n                    {\n                        \"text\": \"The capital of Japan is Tokyo.\",\n                    },\n                ],\n            },\n        ]\n\n        self.assertListEqual(expected_result, res)\n\n    @patch(\"agentscope.formatter._formatter_base._save_base64_data\")\n    async def test_multi_agent_formatter(\n        self,\n        mock_save_base64_data: MagicMock,\n    ) -> None:\n        \"\"\"Test the gemini multi-agent formatter.\"\"\"\n        mock_save_base64_data.return_value = self.mock_audio_path\n\n        formatter = GeminiMultiAgentFormatter()\n\n        # system + conversation + tools + conversation + tools\n        res = await formatter.format(\n            [\n                *self.msgs_system,\n                *self.msgs_conversation,\n                *self.msgs_tools,\n                *self.msgs_conversation_2,\n                *self.msgs_tools_2,\n            ],\n        )\n\n        self.assertListEqual(res, self.ground_truth_multiagent_2)\n\n        # system + conversation + tools + conversation\n        res = await formatter.format(\n            [\n                *self.msgs_system,\n                *self.msgs_conversation,\n                *self.msgs_tools,\n                *self.msgs_conversation_2,\n            ],\n        )\n\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent_2[: -len(self.msgs_tools_2)],\n        )\n\n        # system + conversation + tools\n        res = await formatter.format(\n            [\n                *self.msgs_system,\n                *self.msgs_conversation,\n                *self.msgs_tools,\n            ],\n        )\n\n        self.assertListEqual(res, self.ground_truth_multiagent)\n\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent,\n        )\n\n        res = await formatter.format(\n            [*self.msgs_conversation, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent[1:],\n        )\n\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent_without_first_conversation,\n        )\n\n        # Only system message\n        res = await formatter.format(self.msgs_system)\n        self.assertListEqual(res, self.ground_truth_multiagent[:1])\n\n        # Only conversation messages\n        res = await formatter.format(self.msgs_conversation)\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent[1 : -len(self.msgs_tools)],\n        )\n\n        # Only tools messages\n        res = await formatter.format(self.msgs_tools)\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent_without_first_conversation[1:],\n        )\n\n    @patch(\"agentscope.formatter._formatter_base._save_base64_data\")\n    async def test_multi_agent_formatter_with_promote_tool_result_images(\n        self,\n        mock_save_base64_data: MagicMock,\n    ) -> None:\n        \"\"\"Test the gemini multi-agent formatter with\n        promote_tool_result_images=True.\"\"\"\n        mock_save_base64_data.return_value = self.mock_audio_path\n\n        formatter = GeminiMultiAgentFormatter(\n            promote_tool_result_images=True,\n        )\n\n        # Test with tool result containing image blocks\n        res = await formatter.format(\n            [\n                *self.msgs_system,\n                *self.msgs_conversation,\n                *self.msgs_tools,\n            ],\n        )\n\n        # Expected result: image blocks should be promoted and inserted\n        # as a separate user message after the tool result message\n        expected_result = [\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"You're a helpful assistant.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"# Conversation History\\nThe content between\"\n                        \" <history></history> tags contains your\"\n                        \" conversation history\\n<history>user: What\"\n                        \" is the capital of France?\",\n                    },\n                    {\n                        \"inline_data\": {\n                            \"data\": \"ZmFrZSBpbWFnZSBjb250ZW50\",\n                            \"mime_type\": \"image/png\",\n                        },\n                    },\n                    {\n                        \"text\": \"assistant: The capital of France is Paris.\"\n                        \"\\nuser: What is the capital of Germany?\",\n                    },\n                    {\n                        \"inline_data\": {\n                            \"data\": \"ZmFrZSBhdWRpbyBjb250ZW50\",\n                            \"mime_type\": \"audio/mp3\",\n                        },\n                    },\n                    {\n                        \"text\": \"assistant: The capital of Germany is Berlin.\"\n                        \"\\nuser: What is the capital of Japan?\"\n                        \"\\n</history>\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"model\",\n                \"parts\": [\n                    {\n                        \"function_call\": {\n                            \"id\": None,\n                            \"name\": \"get_capital\",\n                            \"args\": {\n                                \"country\": \"Japan\",\n                            },\n                        },\n                        \"thought_signature\": \"1\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"function_response\": {\n                            \"id\": \"1\",\n                            \"name\": \"get_capital\",\n                            \"response\": {\n                                \"output\": \"- The capital of Japan is Tokyo.\"\n                                \"\\n- The returned image can be found\"\n                                \" at: ./image.png\\n- The returned\"\n                                \" audio can be found at: \"\n                                f\"{self.mock_audio_path}\",\n                            },\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"<system-info>The following are \"\n                        \"the image contents from the tool \"\n                        \"result of 'get_capital':\",\n                    },\n                    {\n                        \"text\": \"\\n- The image from './image.png': \",\n                    },\n                    {\n                        \"inline_data\": {\n                            \"data\": \"ZmFrZSBpbWFnZSBjb250ZW50\",\n                            \"mime_type\": \"image/png\",\n                        },\n                    },\n                    {\n                        \"text\": \"</system-info>\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"parts\": [\n                    {\n                        \"text\": \"<history>assistant:\"\n                        \" The capital of Japan is Tokyo.\\n</history>\",\n                    },\n                ],\n            },\n        ]\n\n        self.assertListEqual(expected_result, res)\n\n    async def asyncTearDown(self) -> None:\n        \"\"\"Clean up the test environment.\"\"\"\n        if os.path.exists(self.image_path):\n            os.remove(self.image_path)\n        if os.path.exists(self.audio_path):\n            os.remove(self.audio_path)\n"
  },
  {
    "path": "tests/formatter_ollama_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The Ollama formatter unittests.\"\"\"\nimport os\nimport unittest\nfrom unittest.async_case import IsolatedAsyncioTestCase\n\nfrom agentscope.formatter import OllamaChatFormatter, OllamaMultiAgentFormatter\nfrom agentscope.message import (\n    Msg,\n    URLSource,\n    TextBlock,\n    ImageBlock,\n    ToolUseBlock,\n    ToolResultBlock,\n)\n\n\nclass TestOllamaFormatter(IsolatedAsyncioTestCase):\n    \"\"\"Unittest for the Ollama formatter.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the test environment.\"\"\"\n        self.image_path = \"./image.png\"\n        with open(self.image_path, \"wb\") as f:\n            f.write(b\"fake image content\")\n\n        self.msgs_system = [\n            Msg(\n                \"system\",\n                \"You're a helpful assistant.\",\n                \"system\",\n            ),\n        ]\n        self.msgs_conversation = [\n            Msg(\n                \"user\",\n                [\n                    TextBlock(\n                        type=\"text\",\n                        text=\"What is the capital of France?\",\n                    ),\n                    ImageBlock(\n                        type=\"image\",\n                        source=URLSource(\n                            type=\"url\",\n                            url=self.image_path,\n                        ),\n                    ),\n                ],\n                \"user\",\n            ),\n            Msg(\n                \"assistant\",\n                \"The capital of France is Paris.\",\n                \"assistant\",\n            ),\n            Msg(\n                \"user\",\n                \"What is the capital of Japan?\",\n                \"user\",\n            ),\n        ]\n\n        self.msgs_tools = [\n            Msg(\n                \"assistant\",\n                [\n                    ToolUseBlock(\n                        type=\"tool_use\",\n                        id=\"1\",\n                        name=\"get_capital\",\n                        input={\"country\": \"Japan\"},\n                    ),\n                ],\n                \"assistant\",\n            ),\n            Msg(\n                \"system\",\n                [\n                    ToolResultBlock(\n                        type=\"tool_result\",\n                        id=\"1\",\n                        name=\"get_capital\",\n                        output=[\n                            TextBlock(\n                                type=\"text\",\n                                text=\"The capital of Japan is Tokyo.\",\n                            ),\n                            ImageBlock(\n                                type=\"image\",\n                                source=URLSource(\n                                    type=\"url\",\n                                    url=self.image_path,\n                                ),\n                            ),\n                        ],\n                    ),\n                ],\n                \"system\",\n            ),\n            Msg(\n                \"assistant\",\n                \"The capital of Japan is Tokyo.\",\n                \"assistant\",\n            ),\n        ]\n\n        self.msgs_conversation_2 = [\n            Msg(\n                \"user\",\n                \"What is the capital of South Korea?\",\n                \"user\",\n            ),\n        ]\n\n        self.msgs_tools_2 = [\n            Msg(\n                \"assistant\",\n                [\n                    ToolUseBlock(\n                        type=\"tool_use\",\n                        id=\"2\",\n                        name=\"get_capital\",\n                        input={\"country\": \"South Korea\"},\n                    ),\n                ],\n                \"assistant\",\n            ),\n            Msg(\n                \"system\",\n                [\n                    ToolResultBlock(\n                        type=\"tool_result\",\n                        id=\"2\",\n                        name=\"get_capital\",\n                        output=[\n                            TextBlock(\n                                type=\"text\",\n                                text=\"The capital of South Korea is Seoul.\",\n                            ),\n                            ImageBlock(\n                                type=\"image\",\n                                source=URLSource(\n                                    type=\"url\",\n                                    url=self.image_path,\n                                ),\n                            ),\n                        ],\n                    ),\n                ],\n                \"system\",\n            ),\n            Msg(\n                \"assistant\",\n                \"The capital of South Korea is Seoul.\",\n                \"assistant\",\n            ),\n        ]\n\n        self.ground_truth_chat = [\n            {\n                \"role\": \"system\",\n                \"content\": \"You're a helpful assistant.\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"What is the capital of France?\",\n                \"images\": [\n                    \"ZmFrZSBpbWFnZSBjb250ZW50\",\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": \"The capital of France is Paris.\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"What is the capital of Japan?\",\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": None,\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": {\n                                \"country\": \"Japan\",\n                            },\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"- The capital of Japan is Tokyo.\\n- The returned \"\n                \"image can be found at: ./image.png\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": \"The capital of Japan is Tokyo.\",\n            },\n        ]\n\n        self.ground_truth_multiagent = [\n            {\n                \"role\": \"system\",\n                \"content\": \"You're a helpful assistant.\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"# Conversation History\\nThe content between\"\n                \" <history></history> tags contains your\"\n                \" conversation history\\n<history>\\nuser: What is\"\n                \" the capital of France?\\n\\nassistant: The capital\"\n                \" of France is Paris.\\nuser: What is the capital\"\n                \" of Japan?\\n</history>\",\n                \"images\": [\n                    \"ZmFrZSBpbWFnZSBjb250ZW50\",\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": None,\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": {\n                                \"country\": \"Japan\",\n                            },\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"- The capital of Japan is Tokyo.\\n- The returned\"\n                \" image can be found at: ./image.png\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"<history>\\nassistant: The\"\n                \" capital of Japan is Tokyo.\\n</history>\",\n            },\n        ]\n\n        self.ground_truth_multiagent_without_first_conversation = [\n            {\n                \"role\": \"system\",\n                \"content\": \"You're a helpful assistant.\",\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": None,\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": {\n                                \"country\": \"Japan\",\n                            },\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"- The capital of Japan is Tokyo.\\n- The returned\"\n                \" image can be found at: ./image.png\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"# Conversation History\\nThe content between\"\n                \" <history></history> tags contains your\"\n                \" conversation history\\n<history>\\nassistant: The\"\n                \" capital of Japan is Tokyo.\\n</history>\",\n            },\n        ]\n\n        self.ground_truth_multiagent_2 = [\n            {\n                \"role\": \"system\",\n                \"content\": \"You're a helpful assistant.\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"# Conversation History\\nThe content between \"\n                \"<history></history> tags contains your \"\n                \"conversation history\\n<history>\\nuser: What is \"\n                \"the capital of France?\\n\\nassistant: The capital \"\n                \"of France is Paris.\\nuser: What is the capital of \"\n                \"Japan?\\n</history>\",\n                \"images\": [\n                    \"ZmFrZSBpbWFnZSBjb250ZW50\",\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": None,\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": {\n                                \"country\": \"Japan\",\n                            },\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"- The capital of Japan is Tokyo.\\n- The returned \"\n                \"image can be found at: ./image.png\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"<history>\\nassistant: The capital of Japan is \"\n                \"Tokyo.\\nuser: What is the capital of South Korea?\"\n                \"\\n</history>\",\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": None,\n                \"tool_calls\": [\n                    {\n                        \"id\": \"2\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": {\n                                \"country\": \"South Korea\",\n                            },\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"2\",\n                \"content\": \"- The capital of South Korea is Seoul.\\n- The \"\n                \"returned image can be found at: ./image.png\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"<history>\\nassistant: The capital of South Korea\"\n                \" is Seoul.\\n</history>\",\n            },\n        ]\n\n    async def test_chat_formatter(self) -> None:\n        \"\"\"Test the Ollama chat formatter.\"\"\"\n        formatter = OllamaChatFormatter()\n\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation, *self.msgs_tools],\n        )\n\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat,\n        )\n\n        # Without system message\n        res = await formatter.format(\n            [*self.msgs_conversation, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat[1:],\n        )\n\n        # Without conversation messages\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat[:1]\n            + self.ground_truth_chat[-len(self.msgs_tools) :],\n        )\n\n        # Without tools messages\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat[: -len(self.msgs_tools)],\n        )\n\n    async def test_chat_formatter_with_extract_image_blocks(\n        self,\n    ) -> None:\n        \"\"\"Test the Ollama chat formatter with\n        promote_tool_result_images=True.\"\"\"\n        formatter = OllamaChatFormatter(promote_tool_result_images=True)\n\n        # Test with tool result containing image blocks\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation, *self.msgs_tools],\n        )\n\n        # Expected result: image blocks should be extracted and inserted\n        # as a separate user message after the tool result message\n        expected_result = [\n            {\n                \"role\": \"system\",\n                \"content\": \"You're a helpful assistant.\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"What is the capital of France?\",\n                \"images\": [\n                    \"ZmFrZSBpbWFnZSBjb250ZW50\",\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": \"The capital of France is Paris.\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"What is the capital of Japan?\",\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": None,\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": {\n                                \"country\": \"Japan\",\n                            },\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"- The capital of Japan is Tokyo.\\n- The returned \"\n                \"image can be found at: ./image.png\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"<system-info>The following are \"\n                \"the image contents from the tool \"\n                \"result of 'get_capital':\\n\\n- The image from \"\n                \"'./image.png': \\n</system-info>\",\n                \"images\": [\n                    \"ZmFrZSBpbWFnZSBjb250ZW50\",\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": \"The capital of Japan is Tokyo.\",\n            },\n        ]\n\n        self.assertListEqual(expected_result, res)\n\n    async def test_multi_agent_formatter(\n        self,\n    ) -> None:\n        \"\"\"Test the Ollama multi-agent formatter.\"\"\"\n\n        formatter = OllamaMultiAgentFormatter()\n\n        # system + conversation + tools + conversation + tools\n        res = await formatter.format(\n            [\n                *self.msgs_system,\n                *self.msgs_conversation,\n                *self.msgs_tools,\n                *self.msgs_conversation_2,\n                *self.msgs_tools_2,\n            ],\n        )\n\n        self.assertListEqual(res, self.ground_truth_multiagent_2)\n\n        # system + conversation + tools + conversation\n        res = await formatter.format(\n            [\n                *self.msgs_system,\n                *self.msgs_conversation,\n                *self.msgs_tools,\n                *self.msgs_conversation_2,\n            ],\n        )\n\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent_2[: -len(self.msgs_tools_2)],\n        )\n\n        # system + conversation + tools\n        res = await formatter.format(\n            [\n                *self.msgs_system,\n                *self.msgs_conversation,\n                *self.msgs_tools,\n            ],\n        )\n\n        self.assertListEqual(res, self.ground_truth_multiagent)\n\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation, *self.msgs_tools],\n        )\n\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent,\n        )\n\n        res = await formatter.format(\n            [*self.msgs_conversation, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent[1:],\n        )\n\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent_without_first_conversation,\n        )\n\n        # Only system message\n        res = await formatter.format(self.msgs_system)\n        self.assertListEqual(res, self.ground_truth_multiagent[:1])\n\n        # Only conversation messages\n        res = await formatter.format(self.msgs_conversation)\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent[1 : -len(self.msgs_tools)],\n        )\n\n        # Only tools messages\n        res = await formatter.format(self.msgs_tools)\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent_without_first_conversation[1:],\n        )\n\n    async def test_multi_agent_formatter_with_promote_tool_result_images(\n        self,\n    ) -> None:\n        \"\"\"Test the Ollama multi-agent formatter with\n        promote_tool_result_images=True.\"\"\"\n        formatter = OllamaMultiAgentFormatter(\n            promote_tool_result_images=True,\n        )\n\n        # Test with tool result containing image blocks\n        res = await formatter.format(\n            [\n                *self.msgs_system,\n                *self.msgs_conversation,\n                *self.msgs_tools,\n            ],\n        )\n\n        # Expected result: image blocks should be promoted and inserted\n        # as a separate user message after the tool result message\n        expected_result = [\n            {\n                \"role\": \"system\",\n                \"content\": \"You're a helpful assistant.\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"# Conversation History\\nThe content between\"\n                \" <history></history> tags contains your\"\n                \" conversation history\\n<history>\\nuser: What is\"\n                \" the capital of France?\\n\\nassistant: The capital\"\n                \" of France is Paris.\\nuser: What is the capital\"\n                \" of Japan?\\n</history>\",\n                \"images\": [\n                    \"ZmFrZSBpbWFnZSBjb250ZW50\",\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": None,\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": {\n                                \"country\": \"Japan\",\n                            },\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"- The capital of Japan is Tokyo.\\n- The returned\"\n                \" image can be found at: ./image.png\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"<system-info>The following are \"\n                \"the image contents from the tool \"\n                \"result of 'get_capital':\\n\\n- The image from \"\n                \"'./image.png': \\n</system-info>\",\n                \"images\": [\n                    \"ZmFrZSBpbWFnZSBjb250ZW50\",\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": \"<history>\\nassistant: The\"\n                \" capital of Japan is Tokyo.\\n</history>\",\n            },\n        ]\n\n        self.assertListEqual(expected_result, res)\n\n    async def asyncTearDown(self) -> None:\n        \"\"\"Clean up the test environment.\"\"\"\n        if os.path.exists(self.image_path):\n            os.remove(self.image_path)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/formatter_openai_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The OpenAI formatter unittests.\"\"\"\nimport os\nfrom unittest.async_case import IsolatedAsyncioTestCase\nfrom unittest.mock import patch, MagicMock\n\nfrom agentscope.formatter import OpenAIChatFormatter\nfrom agentscope.formatter._openai_formatter import OpenAIMultiAgentFormatter\nfrom agentscope.message import (\n    Msg,\n    TextBlock,\n    ImageBlock,\n    AudioBlock,\n    URLSource,\n    ToolResultBlock,\n    ToolUseBlock,\n    Base64Source,\n)\n\n\nclass TestOpenAIFormatter(IsolatedAsyncioTestCase):\n    \"\"\"OpenAI formatter unittests.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the test environment.\"\"\"\n        self.image_path = os.path.abspath(\"./image.png\")\n        with open(self.image_path, \"wb\") as f:\n            f.write(b\"fake image content\")\n\n        self.mock_audio_path = (\n            \"/var/folders/gf/krg8x_ws409cpw_46b2s6rjc0000gn/T/tmpfymnv2w9.wav\"\n        )\n\n        self.audio_path = os.path.abspath(\"./audio.wav\")\n        with open(self.audio_path, \"wb\") as f:\n            f.write(b\"fake audio content\")\n\n        self.msgs_system = [\n            Msg(\n                \"system\",\n                \"You're a helpful assistant.\",\n                \"system\",\n            ),\n        ]\n        self.msgs_conversation = [\n            Msg(\n                \"user\",\n                [\n                    TextBlock(\n                        type=\"text\",\n                        text=\"What is the capital of France?\",\n                    ),\n                    ImageBlock(\n                        type=\"image\",\n                        source=URLSource(\n                            type=\"url\",\n                            url=self.image_path,\n                        ),\n                    ),\n                ],\n                \"user\",\n            ),\n            Msg(\n                \"assistant\",\n                \"The capital of France is Paris.\",\n                \"assistant\",\n            ),\n            Msg(\n                \"user\",\n                [\n                    TextBlock(\n                        type=\"text\",\n                        text=\"What is the capital of Germany?\",\n                    ),\n                    AudioBlock(\n                        type=\"audio\",\n                        source=URLSource(\n                            type=\"url\",\n                            url=self.audio_path,\n                        ),\n                    ),\n                ],\n                \"user\",\n            ),\n            Msg(\n                \"assistant\",\n                \"The capital of Germany is Berlin.\",\n                \"assistant\",\n            ),\n            Msg(\n                \"user\",\n                \"What is the capital of Japan?\",\n                \"user\",\n            ),\n        ]\n\n        self.msgs_tools = [\n            Msg(\n                \"assistant\",\n                [\n                    ToolUseBlock(\n                        type=\"tool_use\",\n                        id=\"1\",\n                        name=\"get_capital\",\n                        input={\"country\": \"Japan\"},\n                    ),\n                ],\n                \"assistant\",\n            ),\n            Msg(\n                \"system\",\n                [\n                    ToolResultBlock(\n                        type=\"tool_result\",\n                        id=\"1\",\n                        name=\"get_capital\",\n                        output=[\n                            TextBlock(\n                                type=\"text\",\n                                text=\"The capital of Japan is Tokyo.\",\n                            ),\n                            ImageBlock(\n                                type=\"image\",\n                                source=URLSource(\n                                    type=\"url\",\n                                    url=self.image_path,\n                                ),\n                            ),\n                            AudioBlock(\n                                type=\"audio\",\n                                source=Base64Source(\n                                    type=\"base64\",\n                                    media_type=\"audio/wav\",\n                                    data=\"ZmFrZSBhdWRpbyBjb250ZW50\",\n                                ),\n                            ),\n                        ],\n                    ),\n                ],\n                \"system\",\n            ),\n            Msg(\n                \"assistant\",\n                \"The capital of Japan is Tokyo.\",\n                \"assistant\",\n            ),\n        ]\n\n        self.ground_truth_chat = [\n            {\n                \"role\": \"system\",\n                \"name\": \"system\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"You're a helpful assistant.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"name\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"What is the capital of France?\",\n                    },\n                    {\n                        \"type\": \"image_url\",\n                        \"image_url\": {\n                            \"url\": \"data:image/png;\"\n                            \"base64,ZmFrZSBpbWFnZSBjb250ZW50\",\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"name\": \"assistant\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"The capital of France is Paris.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"name\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"What is the capital of Germany?\",\n                    },\n                    {\n                        \"type\": \"input_audio\",\n                        \"input_audio\": {\n                            \"data\": \"ZmFrZSBhdWRpbyBjb250ZW50\",\n                            \"format\": \"wav\",\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"name\": \"assistant\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"The capital of Germany is Berlin.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"name\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"What is the capital of Japan?\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"name\": \"assistant\",\n                \"content\": None,\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": '{\"country\": \"Japan\"}',\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"- The capital of Japan is Tokyo.\\n\"\n                \"- The returned image can be found at: \"\n                f\"{self.image_path}\\n\"\n                \"- The returned audio can be found at: \"\n                f\"{self.mock_audio_path}\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"assistant\",\n                \"name\": \"assistant\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"The capital of Japan is Tokyo.\",\n                    },\n                ],\n            },\n        ]\n\n        self.ground_truth_multiagent = [\n            {\n                \"role\": \"system\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"You're a helpful assistant.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"# Conversation History\\n\"\n                        \"The content between <history></history> tags contains\"\n                        \" your conversation history\\n\"\n                        \"<history>\\n\"\n                        \"user: What is the capital of France?\\n\"\n                        \"assistant: The capital of France is Paris.\\n\"\n                        \"user: What is the capital of Germany?\\n\"\n                        \"assistant: The capital of Germany is Berlin.\\n\"\n                        \"user: What is the capital of Japan?\\n\"\n                        \"</history>\",\n                    },\n                    {\n                        \"type\": \"image_url\",\n                        \"image_url\": {\n                            \"url\": \"data:image/png;base64,\"\n                            \"ZmFrZSBpbWFnZSBjb250ZW50\",\n                        },\n                    },\n                    {\n                        \"type\": \"input_audio\",\n                        \"input_audio\": {\n                            \"data\": \"ZmFrZSBhdWRpbyBjb250ZW50\",\n                            \"format\": \"wav\",\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"name\": \"assistant\",\n                \"content\": None,\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": '{\"country\": \"Japan\"}',\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"- The capital of Japan is Tokyo.\\n\"\n                \"- The returned image can be found at: \"\n                f\"{self.image_path}\\n\"\n                \"- The returned audio can be found at: \"\n                f\"{self.mock_audio_path}\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"<history>\\n\"\n                        \"assistant: The capital of Japan is Tokyo.\\n\"\n                        \"</history>\",\n                    },\n                ],\n            },\n        ]\n\n        self.ground_truth_multiagent_without_conversation = [\n            {\n                \"role\": \"system\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"You're a helpful assistant.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"name\": \"assistant\",\n                \"content\": None,\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": '{\"country\": \"Japan\"}',\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"- The capital of Japan is Tokyo.\\n\"\n                \"- The returned image can be found at: \"\n                f\"{self.image_path}\\n\"\n                \"- The returned audio can be found at: \"\n                f\"{self.mock_audio_path}\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"# Conversation History\\n\"\n                        \"The content between <history></history> tags contains\"\n                        \" your conversation history\\n<history>\\n\"\n                        \"assistant: The capital of Japan is Tokyo.\\n\"\n                        \"</history>\",\n                    },\n                ],\n            },\n        ]\n\n    @patch(\"agentscope.formatter._formatter_base._save_base64_data\")\n    async def test_formatter(self, mock_save_base64_data: MagicMock) -> None:\n        \"\"\"Test the chat formatter.\"\"\"\n        mock_save_base64_data.return_value = self.mock_audio_path\n\n        formatter = OpenAIChatFormatter()\n\n        # Full history\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat,\n        )\n\n        # Without system message\n        res = await formatter.format(\n            [*self.msgs_conversation, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat[1:],\n        )\n\n        # Without conversation messages\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat[:1]\n            + self.ground_truth_chat[-len(self.msgs_tools) :],\n        )\n\n        # Without tools messages\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_chat[: -len(self.msgs_tools)],\n        )\n\n    @patch(\"agentscope.formatter._formatter_base._save_base64_data\")\n    async def test_formatter_with_extract_image_blocks(\n        self,\n        mock_save_base64_data: MagicMock,\n    ) -> None:\n        \"\"\"Test the OpenAI chat formatter with\n        promote_tool_result_images=True.\"\"\"\n        mock_save_base64_data.return_value = self.mock_audio_path\n\n        formatter = OpenAIChatFormatter(promote_tool_result_images=True)\n\n        # Test with tool result containing image blocks\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation, *self.msgs_tools],\n        )\n\n        # Expected result: image blocks should be extracted and inserted\n        # as a separate user message after the tool result message\n        expected_result = [\n            {\n                \"role\": \"system\",\n                \"name\": \"system\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"You're a helpful assistant.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"name\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"What is the capital of France?\",\n                    },\n                    {\n                        \"type\": \"image_url\",\n                        \"image_url\": {\n                            \"url\": \"data:image/png;\"\n                            \"base64,ZmFrZSBpbWFnZSBjb250ZW50\",\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"name\": \"assistant\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"The capital of France is Paris.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"name\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"What is the capital of Germany?\",\n                    },\n                    {\n                        \"type\": \"input_audio\",\n                        \"input_audio\": {\n                            \"data\": \"ZmFrZSBhdWRpbyBjb250ZW50\",\n                            \"format\": \"wav\",\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"name\": \"assistant\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"The capital of Germany is Berlin.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"name\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"What is the capital of Japan?\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"name\": \"assistant\",\n                \"content\": None,\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": '{\"country\": \"Japan\"}',\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"- The capital of Japan is Tokyo.\\n\"\n                \"- The returned image can be found at: \"\n                f\"{self.image_path}\\n\"\n                \"- The returned audio can be found at: \"\n                f\"{self.mock_audio_path}\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"user\",\n                \"name\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"<system-info>The following are \"\n                        \"the image contents from the tool \"\n                        \"result of 'get_capital':\",\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"text\": f\"\\n- The image from '{self.image_path}': \",\n                    },\n                    {\n                        \"type\": \"image_url\",\n                        \"image_url\": {\n                            \"url\": \"data:image/png;\"\n                            \"base64,ZmFrZSBpbWFnZSBjb250ZW50\",\n                        },\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"</system-info>\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"name\": \"assistant\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"The capital of Japan is Tokyo.\",\n                    },\n                ],\n            },\n        ]\n\n        self.assertListEqual(expected_result, res)\n\n    @patch(\"agentscope.formatter._formatter_base._save_base64_data\")\n    async def test_multiagent_formatter(\n        self,\n        mock_save_base64_data: MagicMock,\n    ) -> None:\n        \"\"\"Test the OpenAI multi-agent formatter.\"\"\"\n        mock_save_base64_data.return_value = self.mock_audio_path\n\n        formatter = OpenAIMultiAgentFormatter()\n\n        # Full test: system + conversation + tools\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_conversation, *self.msgs_tools],\n        )\n\n        self.assertListEqual(res, self.ground_truth_multiagent)\n\n        # Without system message\n        res = await formatter.format(\n            [*self.msgs_conversation, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent[1:],\n        )\n\n        # Without conversation messages\n        res = await formatter.format(\n            [*self.msgs_system, *self.msgs_tools],\n        )\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent_without_conversation,\n        )\n\n        # Only system message\n        res = await formatter.format(self.msgs_system)\n        self.assertListEqual(res, self.ground_truth_multiagent[:1])\n\n        # Only tools messages\n        res = await formatter.format(self.msgs_tools)\n        self.assertListEqual(\n            res,\n            self.ground_truth_multiagent_without_conversation[1:],\n        )\n\n    @patch(\"agentscope.formatter._formatter_base._save_base64_data\")\n    async def test_multiagent_formatter_with_promote_tool_result_images(\n        self,\n        mock_save_base64_data: MagicMock,\n    ) -> None:\n        \"\"\"Test the OpenAI multi-agent formatter with\n        promote_tool_result_images=True.\"\"\"\n        mock_save_base64_data.return_value = self.mock_audio_path\n\n        formatter = OpenAIMultiAgentFormatter(\n            promote_tool_result_images=True,\n        )\n\n        # Test with tool result containing image blocks\n        res = await formatter.format(\n            [\n                *self.msgs_system,\n                *self.msgs_conversation,\n                *self.msgs_tools,\n            ],\n        )\n\n        # Expected result: image blocks should be promoted and inserted\n        # as a separate user message after the tool result message\n        expected_result = [\n            {\n                \"role\": \"system\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"You're a helpful assistant.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"# Conversation History\\n\"\n                        \"The content between <history></history> tags contains\"\n                        \" your conversation history\\n\"\n                        \"<history>\\n\"\n                        \"user: What is the capital of France?\\n\"\n                        \"assistant: The capital of France is Paris.\\n\"\n                        \"user: What is the capital of Germany?\\n\"\n                        \"assistant: The capital of Germany is Berlin.\\n\"\n                        \"user: What is the capital of Japan?\\n\"\n                        \"</history>\",\n                    },\n                    {\n                        \"type\": \"image_url\",\n                        \"image_url\": {\n                            \"url\": \"data:image/png;base64,\"\n                            \"ZmFrZSBpbWFnZSBjb250ZW50\",\n                        },\n                    },\n                    {\n                        \"type\": \"input_audio\",\n                        \"input_audio\": {\n                            \"data\": \"ZmFrZSBhdWRpbyBjb250ZW50\",\n                            \"format\": \"wav\",\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"name\": \"assistant\",\n                \"content\": None,\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": '{\"country\": \"Japan\"}',\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"- The capital of Japan is Tokyo.\\n\"\n                \"- The returned image can be found at: \"\n                f\"{self.image_path}\\n\"\n                \"- The returned audio can be found at: \"\n                f\"{self.mock_audio_path}\",\n                \"name\": \"get_capital\",\n            },\n            {\n                \"role\": \"user\",\n                \"name\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"<system-info>The following are \"\n                        \"the image contents from the tool \"\n                        \"result of 'get_capital':\",\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"text\": f\"\\n- The image from '{self.image_path}': \",\n                    },\n                    {\n                        \"type\": \"image_url\",\n                        \"image_url\": {\n                            \"url\": \"data:image/png;\"\n                            \"base64,ZmFrZSBpbWFnZSBjb250ZW50\",\n                        },\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"</system-info>\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"<history>\\n\"\n                        \"assistant: The capital of Japan is Tokyo.\\n\"\n                        \"</history>\",\n                    },\n                ],\n            },\n        ]\n\n        self.assertListEqual(expected_result, res)\n\n    async def asyncTearDown(self) -> None:\n        \"\"\"Clean up the test environment.\"\"\"\n        if os.path.exists(self.image_path):\n            os.remove(self.image_path)\n        if os.path.exists(self.audio_path):\n            os.remove(self.audio_path)\n"
  },
  {
    "path": "tests/hook_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Hook related tests in agentscope.\"\"\"\nfrom typing import Any\nfrom unittest.async_case import IsolatedAsyncioTestCase\n\nfrom agentscope.agent import AgentBase\nfrom agentscope.message import Msg, TextBlock\n\n\nclass MyAgent(AgentBase):\n    \"\"\"Test agent class for testing hooks.\"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"Initialize the test agent.\"\"\"\n        super().__init__()\n        self.records: list[str] = []\n        self.memory: list[Msg] = []\n\n    async def reply(self, msg: Msg) -> Msg:\n        \"\"\"Reply to the message.\"\"\"\n        await self.print(msg)\n        if isinstance(msg.content, list):\n            msg.content.append(\n                TextBlock(\n                    type=\"text\",\n                    text=\"mark\",\n                ),\n            )\n        return msg\n\n    async def observe(self, msg: Msg) -> None:\n        \"\"\"Observe the message without generating a reply.\"\"\"\n        self.memory.append(msg)\n\n    async def handle_interrupt(self, *args: Any, **kwargs: Any) -> Msg:\n        \"\"\"Handle the interrupt signal.\"\"\"\n        # This is a placeholder for handling interrupts.\n        return Msg(\"test\", \"Interrupt handled\", \"assistant\")\n\n\nclass ChildAgent(MyAgent):\n    \"\"\"Child agent for testing hook isolation.\"\"\"\n\n\nclass GrandChildAgent(ChildAgent):\n    \"\"\"Grandchild agent for testing deeper inheritance.\"\"\"\n\n\nclass AgentA(MyAgent):\n    \"\"\"First parent class.\"\"\"\n\n\nclass AgentB(MyAgent):\n    \"\"\"Second parent class.\"\"\"\n\n\nclass AgentC(AgentA, AgentB):\n    \"\"\"Multiple inheritance class.\"\"\"\n\n\nasync def async_pre_func_w_modifying(\n    self: MyAgent,\n    kwargs: dict[str, Any],\n) -> dict[str, Any]:\n    \"\"\"A pre-hook function that modifies the keyword arguments.\"\"\"\n\n    if isinstance(kwargs.get(\"msg\"), Msg):\n        kwargs[\"msg\"].content.append(\n            TextBlock(\n                type=\"text\",\n                text=\"pre_1\",\n            ),\n        )\n    self.records.append(\"pre_1\")\n    return kwargs\n\n\nasync def async_pre_func_wo_modifying(\n    self: MyAgent,\n    kwargs: dict[str, Any],\n) -> None:\n    \"\"\"A pre-hook function that does not modify the keyword arguments.\"\"\"\n    if isinstance(kwargs.get(\"msg\"), Msg):\n        kwargs[\"msg\"].content.append(\n            TextBlock(\n                type=\"text\",\n                text=\"pre_2\",\n            ),\n        )\n    self.records.append(\"pre_2\")\n\n\ndef sync_pre_func_w_modifying(\n    self: MyAgent,\n    kwargs: dict[str, Any],\n) -> dict[str, Any]:\n    \"\"\"A synchronous pre-hook function that does not modify the keyword\n    arguments.\"\"\"\n    if isinstance(kwargs.get(\"msg\"), Msg):\n        kwargs[\"msg\"].content.append(\n            TextBlock(\n                type=\"text\",\n                text=\"pre_3\",\n            ),\n        )\n    self.records.append(\"pre_3\")\n    return kwargs\n\n\ndef sync_pre_func_wo_modifying(\n    self: MyAgent,\n    kwargs: dict[str, Any],\n) -> None:\n    \"\"\"A synchronous pre-hook function that does not modify the keyword\n    arguments.\"\"\"\n    if isinstance(kwargs.get(\"msg\"), Msg):\n        kwargs[\"msg\"].content.append(\n            TextBlock(\n                type=\"text\",\n                text=\"pre_4\",\n            ),\n        )\n    self.records.append(\"pre_4\")\n\n\nasync def async_post_func_w_modifying(\n    self: MyAgent,\n    _kwargs: dict[str, Any],\n    output: Any,\n) -> Any:\n    \"\"\"A post-hook function that modifies the output.\"\"\"\n    if isinstance(output, Msg):\n        output.content.append(\n            TextBlock(\n                type=\"text\",\n                text=\"post_1\",\n            ),\n        )\n    self.records.append(\"post_1\")\n    return output\n\n\nasync def async_post_func_wo_modifying(\n    self: MyAgent,\n    _kwargs: dict[str, Any],\n    output: Any,\n) -> None:\n    \"\"\"A post-hook function that does not modify the output.\"\"\"\n    if isinstance(output, Msg):\n        output.content.append(\n            TextBlock(\n                type=\"text\",\n                text=\"post_2\",\n            ),\n        )\n    self.records.append(\"post_2\")\n\n\ndef sync_post_func_w_modifying(\n    self: MyAgent,\n    _kwargs: dict[str, Any],\n    output: Any,\n) -> Any:\n    \"\"\"A synchronous post-hook function that modifies the output.\"\"\"\n    if isinstance(output, Msg):\n        output.content.append(\n            TextBlock(\n                type=\"text\",\n                text=\"post_3\",\n            ),\n        )\n    self.records.append(\"post_3\")\n    return output\n\n\ndef sync_post_func_wo_modifying(\n    self: MyAgent,\n    _kwargs: dict[str, Any],\n    output: Any,\n) -> None:\n    \"\"\"A synchronous post-hook function that does not modify the output.\"\"\"\n    if isinstance(output, Msg):\n        output.content.append(\n            TextBlock(\n                type=\"text\",\n                text=\"post_4\",\n            ),\n        )\n    self.records.append(\"post_4\")\n\n\nclass HookTest(IsolatedAsyncioTestCase):\n    \"\"\"The hook test class.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the test environment.\"\"\"\n        self.agent = MyAgent()\n\n    @property\n    def msg(self) -> Msg:\n        \"\"\"Get the test message.\"\"\"\n        return Msg(\n            \"user\",\n            [TextBlock(type=\"text\", text=\"0\")],\n            \"user\",\n        )\n\n    async def test_reply_hooks(self) -> None:\n        \"\"\"Test the reply hooks.\"\"\"\n        res = await self.agent(self.msg)\n        self.assertListEqual(\n            res.content,\n            [\n                TextBlock(type=\"text\", text=\"0\"),\n                TextBlock(type=\"text\", text=\"mark\"),\n            ],\n        )\n\n        # Add pre 1\n        self.agent.register_instance_hook(\n            \"pre_reply\",\n            \"pre_1\",\n            async_pre_func_w_modifying,\n        )\n        res = await self.agent(self.msg)\n        self.assertListEqual(\n            res.content,\n            [\n                TextBlock(type=\"text\", text=\"0\"),\n                TextBlock(type=\"text\", text=\"pre_1\"),\n                TextBlock(type=\"text\", text=\"mark\"),\n            ],\n        )\n        self.assertListEqual(\n            self.agent.records,\n            [\"pre_1\"],\n        )\n\n        # Add pre 2\n        self.agent.register_instance_hook(\n            \"pre_reply\",\n            \"pre_2\",\n            async_pre_func_wo_modifying,\n        )\n        res = await self.agent(self.msg)\n        self.assertListEqual(\n            res.content,\n            [\n                TextBlock(type=\"text\", text=\"0\"),\n                TextBlock(type=\"text\", text=\"pre_1\"),\n                TextBlock(type=\"text\", text=\"mark\"),\n            ],\n        )\n        self.assertListEqual(\n            self.agent.records,\n            [\"pre_1\", \"pre_1\", \"pre_2\"],\n        )\n\n        # Add sync pre 3\n        self.agent.register_instance_hook(\n            \"pre_reply\",\n            \"pre_3\",\n            sync_pre_func_w_modifying,\n        )\n        res = await self.agent(self.msg)\n        self.assertListEqual(\n            res.content,\n            [\n                TextBlock(type=\"text\", text=\"0\"),\n                TextBlock(type=\"text\", text=\"pre_1\"),\n                TextBlock(type=\"text\", text=\"pre_3\"),\n                TextBlock(type=\"text\", text=\"mark\"),\n            ],\n        )\n        self.assertListEqual(\n            self.agent.records,\n            [\n                \"pre_1\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n            ],\n        )\n\n        # Add sync pre 4\n        self.agent.register_instance_hook(\n            \"pre_reply\",\n            \"pre_4\",\n            sync_pre_func_wo_modifying,\n        )\n        res = await self.agent(self.msg)\n        self.assertListEqual(\n            res.content,\n            [\n                TextBlock(type=\"text\", text=\"0\"),\n                TextBlock(type=\"text\", text=\"pre_1\"),\n                TextBlock(type=\"text\", text=\"pre_3\"),\n                TextBlock(type=\"text\", text=\"mark\"),\n            ],\n        )\n        self.assertListEqual(\n            self.agent.records,\n            [\n                \"pre_1\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_4\",\n            ],\n        )\n\n        # Add post 1\n        self.agent.register_instance_hook(\n            \"post_reply\",\n            \"post_1\",\n            async_post_func_w_modifying,\n        )\n        res = await self.agent(self.msg)\n        self.assertListEqual(\n            res.content,\n            [\n                TextBlock(type=\"text\", text=\"0\"),\n                TextBlock(type=\"text\", text=\"pre_1\"),\n                TextBlock(type=\"text\", text=\"pre_3\"),\n                TextBlock(type=\"text\", text=\"mark\"),\n                TextBlock(type=\"text\", text=\"post_1\"),\n            ],\n        )\n        self.assertListEqual(\n            self.agent.records,\n            [\n                \"pre_1\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_4\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_4\",\n                \"post_1\",\n            ],\n        )\n\n        # Add post 2\n        self.agent.register_instance_hook(\n            \"post_reply\",\n            \"post_2\",\n            async_post_func_wo_modifying,\n        )\n        res = await self.agent(self.msg)\n        self.assertListEqual(\n            res.content,\n            [\n                TextBlock(type=\"text\", text=\"0\"),\n                TextBlock(type=\"text\", text=\"pre_1\"),\n                TextBlock(type=\"text\", text=\"pre_3\"),\n                TextBlock(type=\"text\", text=\"mark\"),\n                TextBlock(type=\"text\", text=\"post_1\"),\n            ],\n        )\n        self.assertListEqual(\n            self.agent.records,\n            [\n                \"pre_1\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_4\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_4\",\n                \"post_1\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_4\",\n                \"post_1\",\n                \"post_2\",\n            ],\n        )\n\n        # Add sync post 3\n        self.agent.register_instance_hook(\n            \"post_reply\",\n            \"post_3\",\n            sync_post_func_w_modifying,\n        )\n        res = await self.agent(self.msg)\n        self.assertListEqual(\n            res.content,\n            [\n                TextBlock(type=\"text\", text=\"0\"),\n                TextBlock(type=\"text\", text=\"pre_1\"),\n                TextBlock(type=\"text\", text=\"pre_3\"),\n                TextBlock(type=\"text\", text=\"mark\"),\n                TextBlock(type=\"text\", text=\"post_1\"),\n                TextBlock(type=\"text\", text=\"post_3\"),\n            ],\n        )\n        self.assertListEqual(\n            self.agent.records,\n            [\n                \"pre_1\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_4\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_4\",\n                \"post_1\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_4\",\n                \"post_1\",\n                \"post_2\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_4\",\n                \"post_1\",\n                \"post_2\",\n                \"post_3\",\n            ],\n        )\n\n        # Add sync post 4\n        self.agent.register_instance_hook(\n            \"post_reply\",\n            \"post_4\",\n            sync_post_func_wo_modifying,\n        )\n        res = await self.agent(self.msg)\n        self.assertListEqual(\n            res.content,\n            [\n                TextBlock(type=\"text\", text=\"0\"),\n                TextBlock(type=\"text\", text=\"pre_1\"),\n                TextBlock(type=\"text\", text=\"pre_3\"),\n                TextBlock(type=\"text\", text=\"mark\"),\n                TextBlock(type=\"text\", text=\"post_1\"),\n                TextBlock(type=\"text\", text=\"post_3\"),\n            ],\n        )\n        self.assertListEqual(\n            self.agent.records,\n            [\n                \"pre_1\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_4\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_4\",\n                \"post_1\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_4\",\n                \"post_1\",\n                \"post_2\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_4\",\n                \"post_1\",\n                \"post_2\",\n                \"post_3\",\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_4\",\n                \"post_1\",\n                \"post_2\",\n                \"post_3\",\n                \"post_4\",\n            ],\n        )\n\n        self.agent.clear_instance_hooks()\n        self.agent.records.clear()\n        res = await self.agent(self.msg)\n        self.assertListEqual(\n            res.content,\n            [\n                TextBlock(type=\"text\", text=\"0\"),\n                TextBlock(type=\"text\", text=\"mark\"),\n            ],\n        )\n        self.assertListEqual(\n            self.agent.records,\n            [],\n        )\n\n    async def test_print_hooks(self) -> None:\n        \"\"\"Test the print hooks.\"\"\"\n        self.agent.register_instance_hook(\n            \"pre_print\",\n            \"pre_1\",\n            async_pre_func_w_modifying,\n        )\n        self.agent.register_instance_hook(\n            \"pre_print\",\n            \"pre_2\",\n            async_pre_func_wo_modifying,\n        )\n        self.agent.register_instance_hook(\n            \"pre_print\",\n            \"pre_3\",\n            sync_pre_func_w_modifying,\n        )\n        self.agent.register_instance_hook(\n            \"pre_print\",\n            \"pre_4\",\n            sync_pre_func_wo_modifying,\n        )\n        await self.agent(self.msg)\n        self.assertListEqual(\n            self.agent.records,\n            [\n                \"pre_1\",\n                \"pre_2\",\n                \"pre_3\",\n                \"pre_4\",\n            ],\n        )\n\n    async def test_observe_hooks(self) -> None:\n        \"\"\"Test the observe hooks.\"\"\"\n        self.agent.register_instance_hook(\n            \"pre_observe\",\n            \"pre_1\",\n            async_pre_func_w_modifying,\n        )\n        self.agent.register_instance_hook(\n            \"pre_observe\",\n            \"pre_2\",\n            async_pre_func_wo_modifying,\n        )\n        await self.agent.observe(self.msg)\n        self.assertEqual(len(self.agent.memory), 1)\n        self.assertListEqual(\n            self.agent.records,\n            [\n                \"pre_1\",\n                \"pre_2\",\n            ],\n        )\n        self.assertListEqual(\n            self.agent.memory[0].content,\n            [\n                TextBlock(type=\"text\", text=\"0\"),\n                TextBlock(type=\"text\", text=\"pre_1\"),\n            ],\n        )\n\n        self.agent.register_instance_hook(\n            \"post_observe\",\n            \"post_1\",\n            async_post_func_w_modifying,\n        )\n        self.agent.register_instance_hook(\n            \"post_observe\",\n            \"post_2\",\n            async_post_func_wo_modifying,\n        )\n        await self.agent.observe(self.msg)\n        self.assertEqual(\n            len(self.agent.memory),\n            2,\n        )\n        self.assertListEqual(\n            self.agent.records,\n            [\"pre_1\", \"pre_2\", \"pre_1\", \"pre_2\", \"post_1\", \"post_2\"],\n        )\n        self.assertListEqual(\n            self.agent.memory[1].content,\n            [\n                TextBlock(type=\"text\", text=\"0\"),\n                TextBlock(type=\"text\", text=\"pre_1\"),\n            ],\n        )\n\n    # TODO: The studio requires the hook inherited from AgentBase, we will\n    #  solving this problem later.\n    # async def test_instance_and_class_hooks(self) -> None:\n    #     \"\"\"Test instance and class hooks.\"\"\"\n    #     AgentBase.register_class_hook(\n    #         \"pre_reply\",\n    #         \"pre_3\",\n    #         sync_pre_func_w_modifying,\n    #     )\n    #     self.agent.register_instance_hook(\n    #         \"pre_reply\",\n    #         \"pre_1\",\n    #         async_pre_func_w_modifying,\n    #     )\n    #     res = await self.agent(self.msg)\n    #     self.assertListEqual(\n    #         res.content,\n    #         [\n    #             TextBlock(type=\"text\", text=\"0\"),\n    #             TextBlock(type=\"text\", text=\"pre_1\"),\n    #             TextBlock(type=\"text\", text=\"mark\"),\n    #         ],\n    #     )\n    #\n    #     # remove hook\n    #     AgentBase.remove_class_hook(\"pre_reply\", \"pre_3\")\n    #     res = await self.agent(self.msg)\n    #     self.assertListEqual(\n    #         res.content,\n    #         [\n    #             TextBlock(type=\"text\", text=\"0\"),\n    #             TextBlock(type=\"text\", text=\"pre_1\"),\n    #             TextBlock(type=\"text\", text=\"mark\"),\n    #         ],\n    #     )\n    #\n    # async def test_class_hook_inheritance_isolation(self) -> None:\n    #     \"\"\"Test that class hooks are isolated between parent and child\n    #     classes.\"\"\"\n    #\n    #     # Register different hooks on different classes\n    #     MyAgent.register_class_hook(\n    #         \"pre_reply\",\n    #         \"parent_hook\",\n    #         sync_pre_func_w_modifying,  # adds \"pre_3\" to content\n    #     )\n    #\n    #     ChildAgent.register_class_hook(\n    #         \"pre_reply\",\n    #         \"child_hook\",\n    #         async_pre_func_w_modifying,  # adds \"pre_1\" to content\n    #     )\n    #\n    #     GrandChildAgent.register_class_hook(\n    #         \"pre_reply\",\n    #         \"grandchild_hook\",\n    #         sync_pre_func_wo_modifying,  # adds \"pre_4\" to content\n    #     )\n    #\n    #     # Create instances of each class\n    #     parent_agent = MyAgent()\n    #     child_agent = ChildAgent()\n    #     grandchild_agent = GrandChildAgent()\n    #\n    #     # Test parent agent - should only execute parent hook\n    #     res = await parent_agent(self.msg)\n    #     self.assertListEqual(\n    #         res.content,\n    #         [\n    #             TextBlock(type=\"text\", text=\"0\"),\n    #             TextBlock(type=\"text\", text=\"pre_3\"),  # only parent hook\n    #             TextBlock(type=\"text\", text=\"mark\"),\n    #         ],\n    #     )\n    #     self.assertListEqual(parent_agent.records, [\"pre_3\"])\n    #\n    #     # Test child agent - should only execute child hook\n    #     res = await child_agent(self.msg)\n    #     self.assertListEqual(\n    #         res.content,\n    #         [\n    #             TextBlock(type=\"text\", text=\"0\"),\n    #             TextBlock(type=\"text\", text=\"pre_1\"),  # only child hook\n    #             TextBlock(type=\"text\", text=\"mark\"),\n    #         ],\n    #     )\n    #     self.assertListEqual(child_agent.records, [\"pre_1\"])\n    #\n    #     # Test grandchild agent - should only execute grandchild hook\n    #     res = await grandchild_agent(self.msg)\n    #     self.assertListEqual(\n    #         res.content,\n    #         [\n    #             TextBlock(type=\"text\", text=\"0\"),\n    #             TextBlock(type=\"text\", text=\"mark\"),\n    #             # pre_4 doesn't modify content\n    #         ],\n    #     )\n    #     self.assertListEqual(grandchild_agent.records, [\"pre_4\"])\n    #\n    # async def test_multiple_inheritance_hook_isolation(self) -> None:\n    #     \"\"\"Test hook isolation in multiple inheritance scenarios.\"\"\"\n    #\n    #     # Register hooks on different classes\n    #     AgentA.register_class_hook(\n    #         \"pre_reply\",\n    #         \"hook_a\",\n    #         sync_pre_func_w_modifying,  # adds \"pre_3\"\n    #     )\n    #\n    #     AgentB.register_class_hook(\n    #         \"pre_reply\",\n    #         \"hook_b\",\n    #         async_pre_func_w_modifying,  # adds \"pre_1\"\n    #     )\n    #\n    #     AgentC.register_class_hook(\n    #         \"pre_reply\",\n    #         \"hook_c\",\n    #         sync_pre_func_wo_modifying,  # adds \"pre_4\" (no content change)\n    #     )  # Create instances\n    #     agent_a = AgentA()\n    #     agent_b = AgentB()\n    #     agent_c = AgentC()\n    #\n    #     # Test AgentA - should only execute hook_a\n    #     res = await agent_a(self.msg)\n    #     self.assertListEqual(\n    #         res.content,\n    #         [\n    #             TextBlock(type=\"text\", text=\"0\"),\n    #             TextBlock(type=\"text\", text=\"pre_3\"),\n    #             TextBlock(type=\"text\", text=\"mark\"),\n    #         ],\n    #     )\n    #     self.assertListEqual(agent_a.records, [\"pre_3\"])\n    #\n    #     # Test AgentB - should only execute hook_b\n    #     res = await agent_b(self.msg)\n    #     self.assertListEqual(\n    #         res.content,\n    #         [\n    #             TextBlock(type=\"text\", text=\"0\"),\n    #             TextBlock(type=\"text\", text=\"pre_1\"),\n    #             TextBlock(type=\"text\", text=\"mark\"),\n    #         ],\n    #     )\n    #     self.assertListEqual(agent_b.records, [\"pre_1\"])\n    #\n    #     # Test AgentC - should only execute hook_c\n    #     res = await agent_c(self.msg)\n    #     self.assertListEqual(\n    #         res.content,\n    #         [\n    #             TextBlock(type=\"text\", text=\"0\"),\n    #             TextBlock(type=\"text\", text=\"mark\"),\n    #             # pre_4 doesn't modify content\n    #         ],\n    #     )\n    #     self.assertListEqual(agent_c.records, [\"pre_4\"])\n\n    async def asyncTearDown(self) -> None:\n        \"\"\"Tear down the test environment.\"\"\"\n        self.agent.clear_instance_hooks()\n        MyAgent.clear_class_hooks()\n\n        ChildAgent.clear_class_hooks()\n        GrandChildAgent.clear_class_hooks()\n\n        AgentA.clear_class_hooks()\n        AgentB.clear_class_hooks()\n        AgentC.clear_class_hooks()\n"
  },
  {
    "path": "tests/mcp_sse_client_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The MCP client test module in agentscope.\"\"\"\nimport asyncio\nfrom multiprocessing import Process\nfrom unittest.async_case import IsolatedAsyncioTestCase\n\nimport mcp.types\nfrom mcp.server import FastMCP\n\nfrom agentscope.mcp import HttpStatelessClient, HttpStatefulClient\nfrom agentscope.message import TextBlock, ToolUseBlock\nfrom agentscope.tool import ToolResponse, Toolkit\n\n\nasync def tool_1(arg1: str, arg2: list[int]) -> str:\n    \"\"\"A test tool function.\n\n    Args:\n        arg1 (`str`):\n            The first argument named arg1.\n        arg2 (`list[int]`):\n            The second argument named arg2.\n    \"\"\"\n    return f\"arg1: {arg1}, arg2: {arg2}\"\n\n\ndef setup_server() -> None:\n    \"\"\"Set up the streamable HTTP MCP server.\"\"\"\n    sse_server = FastMCP(\"SSE\", port=8003)\n    sse_server.tool(description=\"A test tool function.\")(tool_1)\n    sse_server.run(transport=\"sse\")\n\n\nclass SseMCPClientTest(IsolatedAsyncioTestCase):\n    \"\"\"Test class for MCP server functionality.\"\"\"\n\n    async def asyncTearDown(self) -> None:\n        \"\"\"Tear down the test environment.\"\"\"\n        del self.toolkit\n\n        while self.process.is_alive():\n            self.process.terminate()\n            await asyncio.sleep(5)\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the test environment.\"\"\"\n        self.port = 8003\n        self.process = Process(target=setup_server)\n        self.process.start()\n        await asyncio.sleep(10)\n\n        self.toolkit = Toolkit()\n        self.schemas_wo_arg1 = [\n            {\n                \"type\": \"function\",\n                \"function\": {\n                    \"name\": \"tool_1\",\n                    \"description\": \"A test tool function.\",\n                    \"parameters\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"arg2\": {\n                                \"items\": {\n                                    \"type\": \"integer\",\n                                },\n                                \"title\": \"Arg2\",\n                                \"type\": \"array\",\n                            },\n                        },\n                        \"required\": [\n                            \"arg2\",\n                        ],\n                    },\n                },\n            },\n        ]\n        self.schemas = [\n            {\n                \"type\": \"function\",\n                \"function\": {\n                    \"name\": \"tool_1\",\n                    \"description\": \"A test tool function.\",\n                    \"parameters\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"arg1\": {\n                                \"title\": \"Arg1\",\n                                \"type\": \"string\",\n                            },\n                            \"arg2\": {\n                                \"items\": {\n                                    \"type\": \"integer\",\n                                },\n                                \"title\": \"Arg2\",\n                                \"type\": \"array\",\n                            },\n                        },\n                        \"required\": [\n                            \"arg1\",\n                            \"arg2\",\n                        ],\n                    },\n                },\n            },\n        ]\n\n    async def test_stateless_client(self) -> None:\n        \"\"\"Test the stateless sse MCP client.\"\"\"\n        stateless_client = HttpStatelessClient(\n            name=\"test_sse_client\",\n            transport=\"sse\",\n            url=f\"http://127.0.0.1:{self.port}/sse\",\n        )\n\n        func_1 = await stateless_client.get_callable_function(\n            \"tool_1\",\n            wrap_tool_result=False,\n        )\n        res_1: mcp.types.CallToolResult = await func_1(\n            arg1=\"123\",\n            arg2=[1, 2, 3],\n        )\n        self.assertEqual(\n            res_1.content[0].text,\n            \"arg1: 123, arg2: [1, 2, 3]\",\n        )\n\n        func_2 = await stateless_client.get_callable_function(\n            \"tool_1\",\n            wrap_tool_result=True,\n        )\n        # Repeat to ensure idempotency\n        res_2: ToolResponse = await func_2(arg1=\"345\", arg2=[4, 5, 6])\n        res_3: ToolResponse = await func_2(arg1=\"345\", arg2=[4, 5, 6])\n        res_4: ToolResponse = await func_2(arg1=\"345\", arg2=[4, 5, 6])\n        self.assertEqual(\n            res_2,\n            ToolResponse(\n                id=res_2.id,\n                content=[\n                    TextBlock(\n                        text=\"arg1: 345, arg2: [4, 5, 6]\",\n                        type=\"text\",\n                    ),\n                ],\n            ),\n        )\n        self.assertEqual(\n            res_3,\n            ToolResponse(\n                id=res_3.id,\n                content=[\n                    TextBlock(\n                        text=\"arg1: 345, arg2: [4, 5, 6]\",\n                        type=\"text\",\n                    ),\n                ],\n            ),\n        )\n        self.assertEqual(\n            res_4,\n            ToolResponse(\n                id=res_4.id,\n                content=[\n                    TextBlock(\n                        text=\"arg1: 345, arg2: [4, 5, 6]\",\n                        type=\"text\",\n                    ),\n                ],\n            ),\n        )\n\n        self.toolkit.register_tool_function(\n            func_2,\n        )\n\n        schemas = self.toolkit.get_json_schemas()\n        self.assertListEqual(\n            schemas,\n            self.schemas,\n        )\n\n        res_gen = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                id=\"xx\",\n                type=\"tool_use\",\n                name=\"tool_1\",\n                input={\n                    \"arg1\": \"789\",\n                    \"arg2\": [7, 8, 9],\n                },\n            ),\n        )\n\n        async for chunk in res_gen:\n            self.assertEqual(\n                chunk,\n                ToolResponse(\n                    id=chunk.id,\n                    content=[\n                        TextBlock(\n                            text=\"arg1: 789, arg2: [7, 8, 9]\",\n                            type=\"text\",\n                        ),\n                    ],\n                ),\n            )\n\n        self.toolkit.clear()\n        self.assertDictEqual(self.toolkit.tools, {})\n\n        # Try to add the mcp client\n        await self.toolkit.register_mcp_client(stateless_client)\n        self.assertListEqual(\n            self.toolkit.get_json_schemas(),\n            self.schemas,\n        )\n\n        self.toolkit.clear()\n        await self.toolkit.register_mcp_client(\n            stateless_client,\n            preset_kwargs_mapping={\n                \"tool_1\": {\n                    \"arg1\": \"default_value\",\n                },\n            },\n        )\n        self.assertListEqual(\n            self.toolkit.get_json_schemas(),\n            self.schemas_wo_arg1,\n        )\n        res_gen = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                id=\"xx\",\n                type=\"tool_use\",\n                name=\"tool_1\",\n                input={\n                    \"arg2\": [11, 12],\n                },\n            ),\n        )\n        async for chunk in res_gen:\n            self.assertEqual(\n                chunk,\n                ToolResponse(\n                    id=chunk.id,\n                    content=[\n                        TextBlock(\n                            text=\"arg1: default_value, arg2: [11, 12]\",\n                            type=\"text\",\n                        ),\n                    ],\n                ),\n            )\n\n    async def test_stateful_client(self) -> None:\n        \"\"\"Test the stateful sse MCP client.\"\"\"\n\n        # Test stateful client\n        stateful_client = HttpStatefulClient(\n            name=\"test_sse_client_stateful\",\n            transport=\"sse\",\n            url=f\"http://127.0.0.1:{self.port}/sse\",\n        )\n\n        self.assertFalse(stateful_client.is_connected)\n        await stateful_client.connect()\n\n        self.assertTrue(stateful_client.is_connected)\n\n        func_1 = await stateful_client.get_callable_function(\n            \"tool_1\",\n            wrap_tool_result=False,\n        )\n        res_1: mcp.types.CallToolResult = await func_1(\n            arg1=\"12\",\n            arg2=[1, 2],\n        )\n        self.assertEqual(\n            res_1.content[0].text,\n            \"arg1: 12, arg2: [1, 2]\",\n        )\n\n        func_2 = await stateful_client.get_callable_function(\n            \"tool_1\",\n            wrap_tool_result=True,\n        )\n        res_2: ToolResponse = await func_2(arg1=\"34\", arg2=[4, 5])\n        res_3: ToolResponse = await func_2(arg1=\"34\", arg2=[4, 5])\n        res_4: ToolResponse = await func_2(arg1=\"34\", arg2=[4, 5])\n        self.assertEqual(\n            res_2,\n            ToolResponse(\n                id=res_2.id,\n                content=[\n                    TextBlock(\n                        text=\"arg1: 34, arg2: [4, 5]\",\n                        type=\"text\",\n                    ),\n                ],\n            ),\n        )\n        self.assertEqual(\n            res_3,\n            ToolResponse(\n                id=res_3.id,\n                content=[\n                    TextBlock(\n                        text=\"arg1: 34, arg2: [4, 5]\",\n                        type=\"text\",\n                    ),\n                ],\n            ),\n        )\n        self.assertEqual(\n            res_4,\n            ToolResponse(\n                id=res_4.id,\n                content=[\n                    TextBlock(\n                        text=\"arg1: 34, arg2: [4, 5]\",\n                        type=\"text\",\n                    ),\n                ],\n            ),\n        )\n\n        # with toolkit\n        self.toolkit.register_tool_function(func_2)\n        self.assertListEqual(\n            self.toolkit.get_json_schemas(),\n            self.schemas,\n        )\n\n        res_gen = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                id=\"xx\",\n                type=\"tool_use\",\n                name=\"tool_1\",\n                input={\n                    \"arg1\": \"56\",\n                    \"arg2\": [5, 6],\n                },\n            ),\n        )\n        async for chunk in res_gen:\n            self.assertEqual(\n                chunk,\n                ToolResponse(\n                    id=chunk.id,\n                    content=[\n                        TextBlock(\n                            text=\"arg1: 56, arg2: [5, 6]\",\n                            type=\"text\",\n                        ),\n                    ],\n                ),\n            )\n\n        # mcp client level test\n        self.toolkit.clear()\n        self.assertDictEqual(self.toolkit.tools, {})\n\n        await self.toolkit.register_mcp_client(stateful_client)\n        self.assertListEqual(\n            self.toolkit.get_json_schemas(),\n            self.schemas,\n        )\n\n        self.toolkit.clear()\n        await self.toolkit.register_mcp_client(\n            stateful_client,\n            preset_kwargs_mapping={\n                \"tool_1\": {\n                    \"arg1\": \"default_value\",\n                },\n            },\n        )\n        self.assertListEqual(\n            self.toolkit.get_json_schemas(),\n            self.schemas_wo_arg1,\n        )\n        res_gen = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                id=\"xx\",\n                type=\"tool_use\",\n                name=\"tool_1\",\n                input={\n                    \"arg2\": [11, 12],\n                },\n            ),\n        )\n        async for chunk in res_gen:\n            self.assertEqual(\n                chunk,\n                ToolResponse(\n                    id=chunk.id,\n                    content=[\n                        TextBlock(\n                            text=\"arg1: default_value, arg2: [11, 12]\",\n                            type=\"text\",\n                        ),\n                    ],\n                ),\n            )\n\n        await stateful_client.close()\n        self.assertFalse(stateful_client.is_connected)\n"
  },
  {
    "path": "tests/mcp_streamable_http_client_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The MCP client test module in agentscope.\"\"\"\nimport asyncio\nfrom multiprocessing import Process\nfrom unittest.async_case import IsolatedAsyncioTestCase\n\nimport mcp.types\nfrom mcp.server import FastMCP\nfrom mcp.types import EmbeddedResource, TextResourceContents\n\nfrom agentscope.mcp import HttpStatelessClient, HttpStatefulClient\nfrom agentscope.message import TextBlock\nfrom agentscope.tool import ToolResponse\n\n\nasync def tool_1(arg1: str, arg2: list[int]) -> str:\n    \"\"\"A test tool function.\n\n    Args:\n        arg1 (`str`):\n            The first argument named arg1.\n        arg2 (`list[int]`):\n            The second argument named arg2.\n    \"\"\"\n    return f\"arg1: {arg1}, arg2: {arg2}\"\n\n\nasync def tool_2() -> list:\n    \"\"\"\n    A test tool function return the EmbeddedResource type\n    \"\"\"\n    return [\n        EmbeddedResource(\n            type=\"resource\",\n            resource=TextResourceContents(\n                uri=\"file://tmp.txt\",\n                mimeType=\"text/plain\",\n                text=\"test content\",\n            ),\n        ),\n    ]\n\n\ndef setup_server() -> None:\n    \"\"\"Set up the streamable HTTP MCP server.\"\"\"\n    sse_server = FastMCP(\"StreamableHTTP\", port=8002)\n    sse_server.tool(description=\"A test tool function.\")(tool_1)\n    sse_server.tool(\n        description=\"A test tool function with embedded resource.\",\n    )(tool_2)\n    sse_server.run(transport=\"streamable-http\")\n\n\nclass StreamableHttpMCPClientTest(IsolatedAsyncioTestCase):\n    \"\"\"Test class for streamable HTTP MCP client.\"\"\"\n\n    async def asyncTearDown(self) -> None:\n        \"\"\"Tear down the test environment.\"\"\"\n        while self.process.is_alive():\n            self.process.terminate()\n            await asyncio.sleep(5)\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the test environment.\"\"\"\n        self.port = 8002\n        self.process = Process(target=setup_server)\n        self.process.start()\n        await asyncio.sleep(10)\n\n    async def test_streamable_http_stateless_client(self) -> None:\n        \"\"\"Test the MCP server connection functionality.\"\"\"\n\n        client = HttpStatelessClient(\n            name=\"test_streamable_http_stateless_client\",\n            transport=\"streamable_http\",\n            url=f\"http://127.0.0.1:{self.port}/mcp\",\n        )\n\n        func_1 = await client.get_callable_function(\n            \"tool_1\",\n            wrap_tool_result=False,\n        )\n        res_1: mcp.types.CallToolResult = await func_1(\n            arg1=\"123\",\n            arg2=[1, 2, 3],\n        )\n        self.assertEqual(\n            res_1.content[0].text,\n            \"arg1: 123, arg2: [1, 2, 3]\",\n        )\n\n        func_2 = await client.get_callable_function(\n            \"tool_1\",\n            wrap_tool_result=True,\n        )\n        res_2: ToolResponse = await func_2(arg1=\"345\", arg2=[4, 5, 6])\n        self.assertEqual(\n            res_2,\n            ToolResponse(\n                id=res_2.id,\n                content=[\n                    TextBlock(\n                        text=\"arg1: 345, arg2: [4, 5, 6]\",\n                        type=\"text\",\n                    ),\n                ],\n            ),\n        )\n\n        # Test stateful client connection\n        client = HttpStatefulClient(\n            name=\"test_streamable_http_stateless_client\",\n            transport=\"streamable_http\",\n            url=f\"http://127.0.0.1:{self.port}/mcp\",\n        )\n\n        self.assertFalse(client.is_connected)\n        await client.connect()\n\n        self.assertTrue(client.is_connected)\n\n        func_1 = await client.get_callable_function(\n            \"tool_1\",\n            wrap_tool_result=False,\n        )\n        res_3: mcp.types.CallToolResult = await func_1(\n            arg1=\"12\",\n            arg2=[1, 2],\n        )\n        self.assertEqual(\n            res_3.content[0].text,\n            \"arg1: 12, arg2: [1, 2]\",\n        )\n\n        func_2 = await client.get_callable_function(\n            \"tool_1\",\n            wrap_tool_result=True,\n        )\n        res_4: ToolResponse = await func_2(arg1=\"34\", arg2=[4, 5])\n        self.assertEqual(\n            res_4,\n            ToolResponse(\n                id=res_4.id,\n                content=[\n                    TextBlock(\n                        text=\"arg1: 34, arg2: [4, 5]\",\n                        type=\"text\",\n                    ),\n                ],\n            ),\n        )\n\n        await client.close()\n        self.assertFalse(client.is_connected)\n\n    async def test_embedded_content(self) -> None:\n        \"\"\"Test the EmbeddedContent functionality.\"\"\"\n        client = HttpStatelessClient(\n            name=\"test_embedded_content\",\n            transport=\"streamable_http\",\n            url=f\"http://127.0.0.1:{self.port}/mcp\",\n        )\n\n        func_3 = await client.get_callable_function(\n            \"tool_2\",\n            wrap_tool_result=True,\n        )\n        res: ToolResponse = await func_3()\n        self.assertEqual(\n            res,\n            ToolResponse(\n                id=res.id,\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=\"\"\"{\n  \"uri\": \"file://tmp.txt/\",\n  \"mimeType\": \"text/plain\",\n  \"meta\": null,\n  \"text\": \"test content\"\n}\"\"\",\n                    ),\n                ],\n            ),\n        )\n"
  },
  {
    "path": "tests/mem0_utils_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unit tests for AgentScopeLLM with Ollama using asyncio.gather() for\nparallel calls.\"\"\"\nimport asyncio\nfrom typing import Any\nfrom unittest.async_case import IsolatedAsyncioTestCase\nfrom unittest.mock import patch, AsyncMock, MagicMock\n\nfrom agentscope.memory._long_term_memory._mem0._mem0_utils import AgentScopeLLM\nfrom agentscope.model import OllamaChatModel\n\n# Try to import BaseLlmConfig, but handle ImportError gracefully\ntry:\n    from mem0.configs.llms.base import BaseLlmConfig\nexcept ImportError:\n    # If mem0 is not installed, create a mock class\n    BaseLlmConfig = MagicMock\n\n\nclass OllamaMessageMock:\n    \"\"\"Mock class for Ollama message objects.\"\"\"\n\n    def __init__(\n        self,\n        content: str = \"\",\n        thinking: str = \"\",\n        tool_calls: list = None,\n    ) -> None:\n        self.content = content\n        self.thinking = thinking\n        self.tool_calls = tool_calls or []\n\n\nclass OllamaResponseMock:\n    \"\"\"Mock class for Ollama response objects.\"\"\"\n\n    def __init__(\n        self,\n        content: str = \"\",\n        thinking: str = \"\",\n        tool_calls: list = None,\n        prompt_eval_count: int = 10,\n        eval_count: int = 20,\n    ) -> None:\n        self.message = OllamaMessageMock(\n            content=content,\n            thinking=thinking,\n            tool_calls=tool_calls or [],\n        )\n        self.prompt_eval_count = prompt_eval_count\n        self.eval_count = eval_count\n\n    def get(self, key: str, default: Any | None = None) -> Any:\n        \"\"\"Mock dict-like get method.\"\"\"\n        return getattr(self, key, default)\n\n    def __contains__(self, key: str) -> bool:\n        \"\"\"Mock dict-like contains method to support 'in' operator.\"\"\"\n        return hasattr(self, key)\n\n\nclass TestAgentScopeLLMWithOllama(IsolatedAsyncioTestCase):\n    \"\"\"Test cases for AgentScopeLLM with\n    OllamaChatModel using asyncio.gather().\"\"\"\n\n    def test_agentscope_llm_parallel_calls_with_asyncio_gather(self) -> None:\n        \"\"\"Test parallel calls using asyncio.gather() - original bug scenario.\n\n        This test reproduces the original bug where parallel calls using\n        asyncio.gather() would cause \"Event loop is closed\" errors.\n        The persistent event loop management fix should resolve this issue.\n        \"\"\"\n        with patch(\"ollama.AsyncClient\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            # Create OllamaChatModel instance\n            ollama_model = OllamaChatModel(\n                model_name=\"qwen3:14b\",\n                stream=False,\n                enable_thinking=False,\n            )\n            ollama_model.client = mock_client\n\n            # Mock Ollama chat response\n            mock_ollama_response = OllamaResponseMock(\n                content=\"Test response\",\n            )\n            mock_client.chat = AsyncMock(\n                return_value=mock_ollama_response,\n            )\n\n            # Create AgentScopeLLM config\n            # Directly set model attribute, simpler than using\n            # LlmConfig constructor\n            llm_config = BaseLlmConfig()\n            llm_config.model = ollama_model\n\n            llm = AgentScopeLLM(config=llm_config)\n\n            # Create multiple different messages for parallel calls\n            messages_list = [\n                [{\"role\": \"user\", \"content\": \"I like staying in homestays\"}],\n                [{\"role\": \"user\", \"content\": \"I prefer coffee over tea\"}],\n                [{\"role\": \"user\", \"content\": \"My favorite color is blue\"}],\n                [{\"role\": \"user\", \"content\": \"I work as a software engineer\"}],\n                [\n                    {\n                        \"role\": \"user\",\n                        \"content\": \"I enjoy reading science fiction\",\n                    },\n                ],\n            ]\n\n            # Define async function to call generate_response\n            async def call_llm(messages: list[dict[str, str]]) -> str | dict:\n                \"\"\"Call LLM generate_response in async context.\"\"\"\n                return llm.generate_response(messages)\n\n            # Use asyncio.gather() to make parallel calls\n            # Without the fix, this would fail with\n            # \"Event loop is closed\" error\n            loop = asyncio.new_event_loop()\n            asyncio.set_event_loop(loop)\n            try:\n                results = loop.run_until_complete(\n                    asyncio.gather(\n                        *[call_llm(msgs) for msgs in messages_list],\n                    ),\n                )\n            finally:\n                loop.close()\n\n            # Verify all parallel calls completed successfully\n            self.assertEqual(len(results), len(messages_list))\n            for result in results:\n                self.assertIsInstance(result, str)\n                self.assertGreater(len(result), 0)\n\n            # Verify Ollama client was called for each parallel request\n            self.assertEqual(\n                mock_client.chat.call_count,\n                len(messages_list),\n            )\n\n    async def test_agentscope_llm_async_gather_in_async_context(self) -> None:\n        \"\"\"Test asyncio.gather() in an async test context.\n\n        This test uses the async test framework to properly test\n        parallel calls using asyncio.gather().\n        \"\"\"\n        with patch(\"ollama.AsyncClient\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            # Create OllamaChatModel instance\n            ollama_model = OllamaChatModel(\n                model_name=\"qwen3:14b\",\n                stream=False,\n                enable_thinking=False,\n            )\n            ollama_model.client = mock_client\n\n            # Mock Ollama chat response\n            mock_ollama_response = OllamaResponseMock(\n                content=\"Test response\",\n            )\n            mock_client.chat = AsyncMock(\n                return_value=mock_ollama_response,\n            )\n\n            # Create AgentScopeLLM config\n            # Directly set model attribute, simpler than using\n            # LlmConfig constructor\n            llm_config = BaseLlmConfig()\n            llm_config.model = ollama_model\n\n            llm = AgentScopeLLM(config=llm_config)\n\n            messages_list = [\n                [{\"role\": \"user\", \"content\": \"First message\"}],\n                [{\"role\": \"user\", \"content\": \"Second message\"}],\n                [{\"role\": \"user\", \"content\": \"Third message\"}],\n            ]\n\n            # Define async function to call generate_response\n            async def call_llm(messages: list[dict[str, str]]) -> str | dict:\n                \"\"\"Call LLM generate_response.\"\"\"\n                return llm.generate_response(messages)\n\n            # Use asyncio.gather() to make parallel calls\n            # This is the exact scenario that was causing the bug\n            results = await asyncio.gather(\n                *[call_llm(msgs) for msgs in messages_list],\n            )\n\n            # Verify all parallel calls completed successfully\n            self.assertEqual(len(results), len(messages_list))\n            for result in results:\n                self.assertIsInstance(result, str)\n                self.assertGreater(len(result), 0)\n\n            # Verify Ollama client was called for each parallel request\n            self.assertEqual(\n                mock_client.chat.call_count,\n                len(messages_list),\n            )\n"
  },
  {
    "path": "tests/memory_compression_test.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=protected-access\n\"\"\"The unittest for memory compression.\"\"\"\nfrom typing import Any\nfrom unittest import IsolatedAsyncioTestCase\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import FormatterBase\nfrom agentscope.message import Msg, TextBlock\nfrom agentscope.model import ChatModelBase, ChatResponse\nfrom agentscope.token import CharTokenCounter\n\n\nclass MockChatModel(ChatModelBase):\n    \"\"\"A mock chat model for testing purposes.\"\"\"\n\n    def __init__(\n        self,\n        model_name: str,\n        stream: bool = False,\n    ) -> None:\n        \"\"\"Initialize the mock chat model.\n\n        Args:\n            model_name (`str`):\n                The name of the model.\n            stream (`bool`, optional):\n                Whether to use streaming mode.\n        \"\"\"\n        super().__init__(model_name=model_name, stream=stream)\n        self.call_count = 0\n        self.received_messages: list[list[dict]] = []\n\n    async def __call__(\n        self,\n        messages: list[dict],\n        **kwargs: Any,\n    ) -> ChatResponse:\n        \"\"\"Mock the model's response.\n\n        Args:\n            messages (`list[dict]`):\n                The messages to process.\n\n        Returns:\n            `ChatResponse`:\n                The mocked response.\n        \"\"\"\n        self.call_count += 1\n        self.received_messages.append(messages)\n\n        return ChatResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=\"This is a test response.\",\n                ),\n            ],\n            metadata={\n                \"task_overview\": \"This is a compressed summary.\",\n                \"current_state\": \"In progress\",\n                \"important_discoveries\": \"N/A\",\n                \"next_steps\": \"N/A\",\n                \"context_to_preserve\": \"N/A\",\n            },\n        )\n\n\nclass MockFormatter(FormatterBase):\n    \"\"\"A mock formatter for testing purposes.\"\"\"\n\n    async def format(self, msgs: list[Msg], **kwargs: Any) -> list[dict]:\n        \"\"\"Mock the formatting of messages.\n\n        Args:\n            msgs (`list[Msg]`):\n                The list of messages to format.\n\n        Returns:\n            `list[dict]`:\n                The formatted messages.\n        \"\"\"\n        return [{\"name\": _.name, \"content\": _.content} for _ in msgs]\n\n\nclass MemoryCompressionTest(IsolatedAsyncioTestCase):\n    \"\"\"The unittest for memory compression.\"\"\"\n\n    async def test_no_compression_below_threshold(self) -> None:\n        \"\"\"Test that compression is NOT triggered when memory is below\n        threshold.\n\n        This test verifies that:\n        1. When memory token count is below the trigger threshold, compression\n           is not activated\n        2. The agent's memory does not contain a compressed summary\n        3. The model receives the full, uncompressed conversation history\n        \"\"\"\n        model = MockChatModel(model_name=\"mock-model\", stream=False)\n        agent = ReActAgent(\n            name=\"Friday\",\n            sys_prompt=\"You are a helpful assistant.\",\n            model=model,\n            formatter=MockFormatter(),\n            compression_config=ReActAgent.CompressionConfig(\n                enable=True,\n                trigger_threshold=10000,  # High threshold to avoid compression\n                agent_token_counter=CharTokenCounter(),\n                keep_recent=1,\n            ),\n        )\n\n        # Create a user message that won't trigger compression\n        user_msg = Msg(\"user\", \"Hello, this is a short message.\", \"user\")\n\n        # Call the agent\n        await agent(user_msg)\n\n        # Verify that compression was NOT triggered (no compressed summary)\n        self.assertEqual(\n            agent.memory._compressed_summary,\n            \"\",\n        )\n\n        # Verify the exact messages received by the model\n        self.assertListEqual(\n            model.received_messages,\n            [\n                [\n                    {\n                        \"content\": \"You are a helpful assistant.\",\n                        \"name\": \"system\",\n                    },\n                    {\n                        \"content\": \"Hello, this is a short message.\",\n                        \"name\": \"user\",\n                    },\n                ],\n            ],\n        )\n\n    async def test_compression_above_threshold(self) -> None:\n        \"\"\"Test that compression IS triggered when memory exceeds threshold and\n        the model receives compressed prompts.\n\n        This test verifies that:\n        1. When memory token count exceeds the trigger threshold, compression\n           is activated\n        2. The agent's memory contains a properly formatted compressed summary\n        3. After compression, the model receives prompts that include the\n           compressed summary instead of the full conversation history\n        4. The compression summary follows the expected format and contains\n           the mock summary content\n\n        This is the key test ensuring that compression not only happens, but\n        also that the compressed format is actually used in subsequent model\n        calls.\n        \"\"\"\n        model = MockChatModel(model_name=\"mock-model\", stream=False)\n        agent = ReActAgent(\n            name=\"Friday\",\n            sys_prompt=\"You are a helpful assistant.\",\n            model=model,\n            formatter=MockFormatter(),\n            compression_config=ReActAgent.CompressionConfig(\n                enable=True,\n                trigger_threshold=100,  # Low threshold to trigger compression\n                agent_token_counter=CharTokenCounter(),\n                keep_recent=1,\n            ),\n        )\n\n        # Create messages that will trigger compression\n        # First message - should not trigger compression\n        msgs = [\n            Msg(\n                \"user\",\n                \"1\",\n                \"user\",\n            ),\n            Msg(\n                \"user\",\n                \"This is a long message \" * 100,  # Make it long\n                \"user\",\n            ),\n            Msg(\n                \"user\",\n                \"2\",\n                \"user\",\n            ),\n        ]\n        await agent(msgs)\n\n        # Verify that compression was triggered\n        summary = \"\"\"<system-info>Here is a summary of your previous work\n# Task Overview\nThis is a compressed summary.\n\n# Current State\nIn progress\n\n# Important Discoveries\nN/A\n\n# Next Steps\nN/A\n\n# Context to Preserve\nN/A</system-info>\"\"\"\n        self.assertEqual(\n            agent.memory._compressed_summary,\n            summary,\n        )\n\n        # Verify the exact messages received by the model after clearing\n        # First call: compression call\n        # Second call: agent response with compressed summary\n        expected_received_messages = [\n            [\n                {\"name\": \"system\", \"content\": \"You are a helpful assistant.\"},\n                {\"name\": \"user\", \"content\": \"1\"},\n                {\n                    \"name\": \"user\",\n                    \"content\": \"This is a long message \" * 100,\n                },\n                {\n                    \"name\": \"user\",\n                    \"content\": (\n                        \"<system-hint>You have been working on the task \"\n                        \"described above but have not yet completed it. \"\n                        \"Now write a continuation summary that will allow \"\n                        \"you to resume work efficiently in a future context \"\n                        \"window where the conversation history will be \"\n                        \"replaced with this summary. Your summary should \"\n                        \"be structured, concise, and actionable.\"\n                        \"</system-hint>\"\n                    ),\n                },\n            ],\n            [\n                {\"name\": \"system\", \"content\": \"You are a helpful assistant.\"},\n                {\n                    \"name\": \"user\",\n                    \"content\": summary,\n                },\n                {\"name\": \"user\", \"content\": \"2\"},\n            ],\n        ]\n\n        self.assertListEqual(\n            model.received_messages,\n            expected_received_messages,\n        )\n"
  },
  {
    "path": "tests/memory_reme_test.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa: E501\n# pylint: disable=C0301,W0212\n\"\"\"Unit tests for ReMeMemory classes (Personal, Tool, Task).\"\"\"\nimport os\nimport sys\nimport unittest\nfrom typing import Any\nfrom unittest.async_case import IsolatedAsyncioTestCase\nfrom unittest.mock import patch, AsyncMock, MagicMock\n\n# Check Python version before importing reme dependencies\nPYTHON_VERSION = sys.version_info\nSKIP_REME_TESTS = PYTHON_VERSION < (3, 12)\n\nif not SKIP_REME_TESTS:\n    from agentscope.embedding import DashScopeTextEmbedding\n    from agentscope.memory import (\n        ReMePersonalLongTermMemory,\n        ReMeToolLongTermMemory,\n        ReMeTaskLongTermMemory,\n    )\n    from agentscope.message import Msg\n    from agentscope.model import DashScopeChatModel\n    from agentscope.tool import ToolResponse\n\n# Get memory type from environment variable or command line argument\n# Options: \"personal\", \"tool\", \"task\"\nMEMORY_TYPE = os.environ.get(\"REME_MEMORY_TYPE\", \"personal\").lower()\nif not SKIP_REME_TESTS:\n    print(f\"MEMORY_TYPE: {MEMORY_TYPE}\")\nelse:\n    print(\n        f\"Skipping ReMeMemory tests: Python {PYTHON_VERSION.major}.{PYTHON_VERSION.minor} < 3.12\",\n    )\n\n\n@unittest.skipIf(\n    SKIP_REME_TESTS,\n    f\"ReMeMemory requires Python 3.12+, current version is {PYTHON_VERSION.major}.{PYTHON_VERSION.minor}\",\n)\nclass TestReMeMemory(IsolatedAsyncioTestCase):\n    \"\"\"Test cases for ReMeMemory (dynamically tests Personal, Tool, or Task memory).\"\"\"\n\n    def setUp(self) -> None:\n        \"\"\"Set up test fixtures.\"\"\"\n        # Mock the model and embedding model to pass isinstance checks\n        self.mock_model = MagicMock(spec=DashScopeChatModel)\n        self.mock_model.model_name = \"qwen3-max\"\n        self.mock_model.api_key = \"test_api_key\"\n\n        self.mock_embedding_model = MagicMock(spec=DashScopeTextEmbedding)\n        self.mock_embedding_model.model_name = \"text-embedding-v4\"\n        self.mock_embedding_model.api_key = \"test_embedding_key\"\n        self.mock_embedding_model.dimensions = 1024\n\n        # Set the memory class based on MEMORY_TYPE\n        self.memory_type = MEMORY_TYPE\n        if self.memory_type == \"tool\":\n            self.memory_class = ReMeToolLongTermMemory\n            self.summary_operation = \"add_tool_call_result\"\n        elif self.memory_type == \"task\":\n            self.memory_class = ReMeTaskLongTermMemory\n            self.summary_operation = \"summary_task_memory\"\n        else:  # default to personal\n            self.memory_class = ReMePersonalLongTermMemory\n            self.summary_operation = \"summary_personal_memory\"\n\n        print(f\"\\n=== Testing {self.memory_class.__name__} ===\")\n\n    def _create_memory_instance(self) -> Any:\n        \"\"\"Create a ReMeMemory instance with mocked dependencies.\"\"\"\n        with patch(\"reme_ai.ReMeApp\"):\n            memory = self.memory_class(\n                agent_name=\"TestAgent\",\n                user_name=\"test_user\",\n                model=self.mock_model,\n                embedding_model=self.mock_embedding_model,\n            )\n            # Mock the app attribute\n            memory.app = AsyncMock()\n            memory._app_started = True\n            memory.workspace_id = \"test_workspace_123\"\n            return memory\n\n    async def test_init_with_default_params(self) -> None:\n        \"\"\"Test initialization with default parameters.\"\"\"\n        with patch(\"reme_ai.ReMeApp\"):\n            memory = self.memory_class(\n                agent_name=\"Friday\",\n                user_name=\"user_123\",\n                model=self.mock_model,\n                embedding_model=self.mock_embedding_model,\n            )\n            self.assertEqual(memory.agent_name, \"Friday\")\n            self.assertEqual(memory.workspace_id, \"user_123\")\n            self.assertIsNotNone(memory.app)\n\n    async def test_record_to_memory_success(self) -> None:\n        \"\"\"Test successful memory recording via record_to_memory tool.\"\"\"\n        memory = self._create_memory_instance()\n\n        # Prepare test data based on memory type\n        mock_result: dict = {}\n        if self.memory_type == \"tool\":\n            # Tool memory expects JSON strings with tool_call_result format\n            import json\n\n            content = [\n                json.dumps(\n                    {\n                        \"create_time\": \"2025-01-01T12:00:00\",\n                        \"tool_name\": \"search_web\",\n                        \"input\": {\"query\": \"Hangzhou travel\"},\n                        \"output\": \"Found 10 results\",\n                        \"token_cost\": 100,\n                        \"success\": True,\n                        \"time_cost\": 1.5,\n                    },\n                ),\n                json.dumps(\n                    {\n                        \"create_time\": \"2025-01-01T12:01:00\",\n                        \"tool_name\": \"book_hotel\",\n                        \"input\": {\"location\": \"Hangzhou\"},\n                        \"output\": \"Booking confirmed\",\n                        \"token_cost\": 150,\n                        \"success\": True,\n                        \"time_cost\": 2.0,\n                    },\n                ),\n            ]\n            expected_count = 2\n            mock_result = {\"status\": \"success\"}\n        elif self.memory_type == \"task\":\n            # Task memory expects task execution information\n            content = [\n                \"Task: Plan Hangzhou trip\",\n                \"Step 1: Research destinations\",\n                \"Step 2: Book accommodations\",\n            ]\n            expected_count = 3\n            mock_result = {\"status\": \"success\"}\n        else:  # personal\n            # Personal memory expects natural language content\n            content = [\n                \"I prefer to stay in homestays when traveling to Hangzhou\",\n                \"I like to visit the West Lake in the morning\",\n                \"I enjoy drinking Longjing tea\",\n            ]\n            expected_count = 3\n            mock_result = {\n                \"metadata\": {\n                    \"memory_list\": [\n                        {\"content\": \"Prefer homestays in Hangzhou\"},\n                        {\"content\": \"Visit West Lake in morning\"},\n                        {\"content\": \"Enjoy Longjing tea\"},\n                    ],\n                },\n            }\n\n        memory.app.async_execute = AsyncMock(return_value=mock_result)\n\n        # Test recording\n        result = await memory.record_to_memory(\n            thinking=\"Recording important information\",\n            content=content,\n        )\n\n        # Verify result\n        self.assertIsInstance(result, ToolResponse)\n        self.assertGreater(len(result.content), 0)\n        text_content = result.content[0].get(\"text\", \"\")\n\n        # Verify success message contains the expected count\n        if self.memory_type == \"tool\":\n            self.assertIn(\n                \"Successfully recorded 2 tool execution\",\n                text_content,\n            )\n        else:\n            self.assertIn(\n                f\"Successfully recorded {expected_count}\",\n                text_content,\n            )\n\n        # Verify app.async_execute was called\n        memory.app.async_execute.assert_called()\n        self.assertEqual(\n            memory.app.async_execute.call_args[1][\"workspace_id\"],\n            \"test_workspace_123\",\n        )\n\n    async def test_record_to_memory_app_not_started(self) -> None:\n        \"\"\"Test record_to_memory when app context is not started.\"\"\"\n        memory = self._create_memory_instance()\n        memory._app_started = False\n\n        # Should raise RuntimeError when app is not started\n        with self.assertRaises(RuntimeError) as context:\n            await memory.record_to_memory(\n                thinking=\"Test thinking\",\n                content=[\"Test content\"],\n            )\n\n        self.assertIn(\"ReMeApp context not started\", str(context.exception))\n\n    async def test_record_to_memory_error_handling(self) -> None:\n        \"\"\"Test error handling in record_to_memory.\"\"\"\n        memory = self._create_memory_instance()\n\n        # Tool memory has different behavior - it validates JSON first\n        if self.memory_type == \"tool\":\n            # For tool memory, test with invalid JSON that triggers the \"No valid tool call results\" path\n            result = await memory.record_to_memory(\n                thinking=\"Test thinking\",\n                content=[\"Test content\"],  # Invalid JSON\n            )\n\n            self.assertIsInstance(result, ToolResponse)\n            text_content = result.content[0].get(\"text\", \"\")\n            self.assertIn(\"No valid tool call results to record\", text_content)\n        else:\n            # For task and personal memory, test with connection error\n            memory.app.async_execute = AsyncMock(\n                side_effect=Exception(\"Connection error\"),\n            )\n\n            result = await memory.record_to_memory(\n                thinking=\"Test thinking\",\n                content=[\"Test content\"],\n            )\n\n            self.assertIsInstance(result, ToolResponse)\n            text_content = result.content[0].get(\"text\", \"\")\n\n            # Different memory types have different error messages\n            if self.memory_type == \"task\":\n                self.assertIn(\"Error recording task memory\", text_content)\n            else:  # personal\n                self.assertIn(\"Error recording memory\", text_content)\n\n            self.assertIn(\"Connection error\", text_content)\n\n    async def test_retrieve_from_memory_success(self) -> None:\n        \"\"\"Test successful memory retrieval via retrieve_from_memory tool.\"\"\"\n        memory = self._create_memory_instance()\n\n        # Mock the app.async_execute response based on memory type\n        if self.memory_type == \"tool\":\n            # Tool memory expects tool_names parameter and returns tool guidelines\n            def mock_retrieve(**kwargs: Any) -> dict:\n                tool_names = kwargs.get(\"tool_names\", \"\")\n                if \"search_web\" in tool_names or \"book_hotel\" in tool_names:\n                    return {\n                        \"answer\": \"Tool usage guidelines for search_web and book_hotel.\",\n                    }\n                return {\"answer\": \"\"}\n\n            memory.app.async_execute = AsyncMock(side_effect=mock_retrieve)\n\n            # Test retrieval with tool names\n            result = await memory.retrieve_from_memory(\n                keywords=[\"search_web\", \"book_hotel\"],\n            )\n\n            # Verify result\n            self.assertIsInstance(result, ToolResponse)\n            text_content = result.content[0].get(\"text\", \"\")\n            self.assertIn(\"Tool usage guidelines\", text_content)\n\n            # Tool memory combines all keywords into one call\n            self.assertEqual(memory.app.async_execute.call_count, 1)\n\n        elif self.memory_type == \"task\":\n            # Task memory expects query parameter and returns task experiences\n            def mock_retrieve(**kwargs: Any) -> dict:\n                query = kwargs.get(\"query\", \"\")\n                if \"Hangzhou\" in query:\n                    return {\n                        \"answer\": \"Task experience: Planning a trip to Hangzhou requires research and booking.\",\n                    }\n                elif \"travel\" in query:\n                    return {\n                        \"answer\": \"Task experience: Travel planning involves multiple steps.\",\n                    }\n                return {\"answer\": \"\"}\n\n            memory.app.async_execute = AsyncMock(side_effect=mock_retrieve)\n\n            # Test retrieval\n            result = await memory.retrieve_from_memory(\n                keywords=[\"Hangzhou trip\", \"travel planning\"],\n            )\n\n            # Verify result\n            self.assertIsInstance(result, ToolResponse)\n            text_content = result.content[0].get(\"text\", \"\")\n            self.assertIn(\"Keyword 'Hangzhou trip'\", text_content)\n            self.assertIn(\"Task experience\", text_content)\n\n            # Task memory calls once per keyword\n            self.assertEqual(memory.app.async_execute.call_count, 2)\n\n        else:  # personal\n            # Personal memory expects query parameter and returns personal preferences\n            def mock_retrieve(**kwargs: Any) -> dict:\n                keyword = kwargs.get(\"query\", \"\")\n                if \"Hangzhou\" in keyword:\n                    return {\n                        \"answer\": \"User prefers homestays in Hangzhou and visits West Lake in the morning.\",\n                    }\n                elif \"tea\" in keyword:\n                    return {\n                        \"answer\": \"User enjoys drinking Longjing tea.\",\n                    }\n                return {\"answer\": \"\"}\n\n            memory.app.async_execute = AsyncMock(side_effect=mock_retrieve)\n\n            # Test retrieval\n            result = await memory.retrieve_from_memory(\n                keywords=[\"Hangzhou travel\", \"tea preference\"],\n            )\n\n            # Verify result\n            self.assertIsInstance(result, ToolResponse)\n            text_content = result.content[0].get(\"text\", \"\")\n            self.assertIn(\"Keyword 'Hangzhou travel'\", text_content)\n            self.assertIn(\"homestays\", text_content)\n            self.assertIn(\"Keyword 'tea preference'\", text_content)\n            self.assertIn(\"Longjing tea\", text_content)\n\n            # Personal memory calls once per keyword\n            self.assertEqual(memory.app.async_execute.call_count, 2)\n\n    async def test_retrieve_from_memory_no_results(self) -> None:\n        \"\"\"Test retrieve_from_memory when no memories are found.\"\"\"\n        memory = self._create_memory_instance()\n\n        # Mock empty response\n        memory.app.async_execute = AsyncMock(return_value={\"answer\": \"\"})\n\n        result = await memory.retrieve_from_memory(\n            keywords=[\"nonexistent keyword\"],\n        )\n\n        self.assertIsInstance(result, ToolResponse)\n        text_content = result.content[0].get(\"text\", \"\")\n\n        # Different memory types have different \"not found\" messages\n        if self.memory_type == \"tool\":\n            self.assertIn(\"No tool guidelines found\", text_content)\n        elif self.memory_type == \"task\":\n            self.assertIn(\"No task experiences found\", text_content)\n        else:  # personal\n            self.assertIn(\"No memories found\", text_content)\n\n    async def test_retrieve_from_memory_app_not_started(self) -> None:\n        \"\"\"Test retrieve_from_memory when app context is not started.\"\"\"\n        memory = self._create_memory_instance()\n        memory._app_started = False\n\n        # Should raise RuntimeError when app is not started\n        with self.assertRaises(RuntimeError) as context:\n            await memory.retrieve_from_memory(\n                keywords=[\"test\"],\n            )\n\n        self.assertIn(\"ReMeApp context not started\", str(context.exception))\n\n    async def test_record_direct_method_success(self) -> None:\n        \"\"\"Test direct record method with message list.\"\"\"\n        memory = self._create_memory_instance()\n\n        # Mock successful recording\n        memory.app.async_execute = AsyncMock(\n            return_value={\"status\": \"success\"},\n        )\n\n        # Prepare messages based on memory type\n        if self.memory_type == \"tool\":\n            # Tool memory expects JSON strings with tool call results\n            import json\n\n            msgs = [\n                Msg(\n                    role=\"user\",\n                    content=json.dumps(\n                        {\n                            \"create_time\": \"2025-01-01T12:00:00\",\n                            \"tool_name\": \"search\",\n                            \"input\": {\"query\": \"test\"},\n                            \"output\": \"result\",\n                            \"token_cost\": 100,\n                            \"success\": True,\n                            \"time_cost\": 1.0,\n                        },\n                    ),\n                    name=\"user\",\n                ),\n            ]\n        else:\n            # Task and Personal memory work with regular messages\n            msgs = [\n                Msg(\n                    role=\"user\",\n                    content=\"I work as a software engineer\",\n                    name=\"user\",\n                ),\n                Msg(\n                    role=\"assistant\",\n                    content=\"Understood!\",\n                    name=\"assistant\",\n                ),\n                Msg(\n                    role=\"user\",\n                    content=\"I prefer remote work\",\n                    name=\"user\",\n                ),\n            ]\n\n        # Should not raise any exception\n        await memory.record(msgs)\n\n        # Verify app.async_execute was called\n        memory.app.async_execute.assert_called()\n        call_args = memory.app.async_execute.call_args[1]\n        self.assertEqual(call_args[\"workspace_id\"], \"test_workspace_123\")\n\n    async def test_record_direct_with_single_message(self) -> None:\n        \"\"\"Test direct record method with a single message.\"\"\"\n        memory = self._create_memory_instance()\n\n        memory.app.async_execute = AsyncMock(\n            return_value={\"status\": \"success\"},\n        )\n\n        # Tool memory requires JSON-formatted tool call results\n        if self.memory_type == \"tool\":\n            import json\n\n            msg = Msg(\n                role=\"user\",\n                content=json.dumps(\n                    {\n                        \"create_time\": \"2025-01-01T12:00:00\",\n                        \"tool_name\": \"test_tool\",\n                        \"input\": {\"param\": \"value\"},\n                        \"output\": \"result\",\n                        \"token_cost\": 10,\n                        \"success\": True,\n                        \"time_cost\": 0.1,\n                    },\n                ),\n                name=\"user\",\n            )\n        else:\n            msg = Msg(\n                role=\"user\",\n                content=\"Single message test\",\n                name=\"user\",\n            )\n\n        # Should handle single message\n        await memory.record(msg)\n\n        # Tool memory calls async_execute twice (add + summarize)\n        if self.memory_type == \"tool\":\n            self.assertEqual(memory.app.async_execute.call_count, 2)\n        else:\n            memory.app.async_execute.assert_called_once()\n\n    async def test_record_direct_with_empty_list(self) -> None:\n        \"\"\"Test direct record method with empty message list.\"\"\"\n        memory = self._create_memory_instance()\n\n        memory.app.async_execute = AsyncMock()\n\n        # Should return early without calling app\n        await memory.record([])\n\n        memory.app.async_execute.assert_not_called()\n\n    async def test_record_direct_filters_none_messages(self) -> None:\n        \"\"\"Test that record method filters out None messages.\"\"\"\n        memory = self._create_memory_instance()\n\n        memory.app.async_execute = AsyncMock(\n            return_value={\"status\": \"success\"},\n        )\n\n        # Tool memory requires JSON-formatted tool call results\n        if self.memory_type == \"tool\":\n            import json\n\n            msgs = [\n                Msg(\n                    role=\"user\",\n                    content=json.dumps(\n                        {\n                            \"create_time\": \"2025-01-01T12:00:00\",\n                            \"tool_name\": \"tool1\",\n                            \"input\": {},\n                            \"output\": \"result1\",\n                            \"token_cost\": 10,\n                            \"success\": True,\n                            \"time_cost\": 0.1,\n                        },\n                    ),\n                    name=\"user\",\n                ),\n                None,\n                Msg(\n                    role=\"assistant\",\n                    content=json.dumps(\n                        {\n                            \"create_time\": \"2025-01-01T12:01:00\",\n                            \"tool_name\": \"tool2\",\n                            \"input\": {},\n                            \"output\": \"result2\",\n                            \"token_cost\": 20,\n                            \"success\": True,\n                            \"time_cost\": 0.2,\n                        },\n                    ),\n                    name=\"assistant\",\n                ),\n                None,\n            ]\n        else:\n            msgs = [\n                Msg(role=\"user\", content=\"Valid message\", name=\"user\"),\n                None,\n                Msg(\n                    role=\"assistant\",\n                    content=\"Another valid\",\n                    name=\"assistant\",\n                ),\n                None,\n            ]\n\n        await memory.record(msgs)\n\n        # Tool memory calls async_execute twice (add + summarize)\n        if self.memory_type == \"tool\":\n            self.assertEqual(memory.app.async_execute.call_count, 2)\n        else:\n            # Should still be called with filtered messages\n            memory.app.async_execute.assert_called_once()\n\n    async def test_record_direct_app_not_started(self) -> None:\n        \"\"\"Test record method when app is not started.\"\"\"\n        memory = self._create_memory_instance()\n        memory._app_started = False\n\n        msgs = [Msg(role=\"user\", content=\"Test\", name=\"user\")]\n\n        # Should raise RuntimeError when app is not started\n        with self.assertRaises(RuntimeError) as context:\n            await memory.record(msgs)\n\n        self.assertIn(\"ReMeApp context not started\", str(context.exception))\n\n    async def test_retrieve_direct_method_success(self) -> None:\n        \"\"\"Test direct retrieve method with message.\"\"\"\n        memory = self._create_memory_instance()\n\n        # Prepare test data based on memory type\n        if self.memory_type == \"tool\":\n            mock_response = {\n                \"answer\": \"Tool guidelines for search and analysis tools.\",\n            }\n            expected_content = \"Tool guidelines\"\n            expected_operation = \"retrieve_tool_memory\"\n        elif self.memory_type == \"task\":\n            mock_response = {\n                \"answer\": \"Task experience with work-related projects.\",\n            }\n            expected_content = \"Task experience\"\n            expected_operation = \"retrieve_task_memory\"\n        else:  # personal\n            mock_response = {\n                \"answer\": \"You are a software engineer who prefers remote work.\",\n            }\n            expected_content = \"software engineer\"\n            expected_operation = \"retrieve_personal_memory\"\n\n        # Mock the retrieval response\n        memory.app.async_execute = AsyncMock(return_value=mock_response)\n\n        msg = Msg(\n            role=\"user\",\n            content=\"What do you know about my work preferences?\",\n            name=\"user\",\n        )\n\n        result = await memory.retrieve(msg)\n\n        # Verify result\n        self.assertIsInstance(result, str)\n        self.assertIn(expected_content, result)\n\n        # Verify app.async_execute was called\n        memory.app.async_execute.assert_called_once()\n        call_args = memory.app.async_execute.call_args[1]\n        self.assertEqual(call_args[\"name\"], expected_operation)\n\n    async def test_retrieve_direct_with_message_list(self) -> None:\n        \"\"\"Test direct retrieve method with list of messages.\"\"\"\n        memory = self._create_memory_instance()\n\n        memory.app.async_execute = AsyncMock(\n            return_value={\"answer\": \"Test answer\"},\n        )\n\n        msgs = [\n            Msg(role=\"user\", content=\"First message\", name=\"user\"),\n            Msg(role=\"user\", content=\"Last message for query\", name=\"user\"),\n        ]\n\n        result = await memory.retrieve(msgs)\n\n        self.assertIsInstance(result, str)\n        # Should use the last message's content\n        call_args = memory.app.async_execute.call_args[1]\n\n        # Tool memory uses tool_names parameter, others use query\n        if self.memory_type == \"tool\":\n            # Tool memory extracts tool names from content\n            self.assertIn(\"tool_names\", call_args)\n        else:\n            self.assertIn(\"Last message for query\", call_args[\"query\"])\n\n    async def test_retrieve_direct_with_none_message(self) -> None:\n        \"\"\"Test direct retrieve method with None message.\"\"\"\n        memory = self._create_memory_instance()\n\n        result = await memory.retrieve(None)\n\n        # Should return empty string\n        self.assertEqual(result, \"\")\n\n    async def test_retrieve_direct_invalid_input(self) -> None:\n        \"\"\"Test direct retrieve method with invalid input.\"\"\"\n        memory = self._create_memory_instance()\n\n        # Should raise TypeError for invalid input\n        with self.assertRaises(TypeError) as context:\n            await memory.retrieve(\"invalid string input\")\n\n        self.assertIn(\"must be a Msg or a list of Msg\", str(context.exception))\n\n    async def test_retrieve_direct_app_not_started(self) -> None:\n        \"\"\"Test retrieve method when app is not started.\"\"\"\n        memory = self._create_memory_instance()\n        memory._app_started = False\n\n        msg = Msg(role=\"user\", content=\"Test\", name=\"user\")\n\n        # Should raise RuntimeError\n        with self.assertRaises(RuntimeError) as context:\n            await memory.retrieve(msg)\n\n        self.assertIn(\"ReMeApp context not started\", str(context.exception))\n\n    async def test_context_manager_usage(self) -> None:\n        \"\"\"Test using ReMeMemory as async context manager.\"\"\"\n        with patch(\"reme_ai.ReMeApp\") as MockReMeApp:\n            mock_app = AsyncMock()\n            mock_app.__aenter__ = AsyncMock(return_value=mock_app)\n            mock_app.__aexit__ = AsyncMock(return_value=None)\n            MockReMeApp.return_value = mock_app\n\n            memory = self.memory_class(\n                agent_name=\"TestAgent\",\n                user_name=\"test_user\",\n                model=self.mock_model,\n                embedding_model=self.mock_embedding_model,\n            )\n\n            # Use as context manager\n            async with memory as mem:\n                self.assertIsNotNone(mem)\n                # The app should be started\n                self.assertTrue(hasattr(mem, \"app\"))\n\n    async def test_integration_record_and_retrieve(self) -> None:\n        \"\"\"Test integration of recording and retrieving memories.\"\"\"\n        memory = self._create_memory_instance()\n\n        # Prepare test data based on memory type\n        if self.memory_type == \"tool\":\n            import json\n\n            content = [\n                json.dumps(\n                    {\n                        \"create_time\": \"2025-01-01T12:00:00\",\n                        \"tool_name\": \"python_executor\",\n                        \"input\": {\"code\": \"print('hello')\"},\n                        \"output\": \"hello\",\n                        \"token_cost\": 50,\n                        \"success\": True,\n                        \"time_cost\": 0.5,\n                    },\n                ),\n            ]\n            keywords = [\"python_executor\"]\n            expected_text = \"Tool usage guidelines\"\n        elif self.memory_type == \"task\":\n            content = [\"Task: Execute Python code successfully\"]\n            keywords = [\"Python execution\"]\n            expected_text = \"Task experience\"\n        else:  # personal\n            content = [\"I like Python programming\"]\n            keywords = [\"programming preferences\"]\n            expected_text = \"Python programming\"\n\n        # Mock record response\n        memory.app.async_execute = AsyncMock(\n            return_value={\n                \"metadata\": {\"memory_list\": [{\"content\": \"test\"}]},\n            },\n        )\n\n        # Record some memories\n        record_result = await memory.record_to_memory(\n            thinking=\"Recording preferences\",\n            content=content,\n        )\n\n        self.assertIn(\n            \"Successfully recorded\",\n            record_result.content[0][\"text\"],\n        )\n\n        # Mock retrieve response\n        if self.memory_type == \"tool\":\n            memory.app.async_execute = AsyncMock(\n                return_value={\n                    \"answer\": \"Tool usage guidelines for python_executor.\",\n                },\n            )\n        elif self.memory_type == \"task\":\n            memory.app.async_execute = AsyncMock(\n                return_value={\n                    \"answer\": \"Task experience: Execute Python code successfully.\",\n                },\n            )\n        else:  # personal\n            memory.app.async_execute = AsyncMock(\n                return_value={\n                    \"answer\": \"You like Python programming.\",\n                },\n            )\n\n        # Retrieve the memories\n        retrieve_result = await memory.retrieve_from_memory(\n            keywords=keywords,\n        )\n\n        self.assertIn(expected_text, retrieve_result.content[0][\"text\"])\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/memory_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The short-term memory tests.\"\"\"\nimport asyncio\nfrom unittest.async_case import IsolatedAsyncioTestCase\n\nfrom sqlalchemy.ext.asyncio import create_async_engine\n\nfrom agentscope.memory import (\n    MemoryBase,\n    InMemoryMemory,\n    AsyncSQLAlchemyMemory,\n    RedisMemory,\n)\nfrom agentscope.message import Msg\n\n\nclass ShortTermMemoryTest(IsolatedAsyncioTestCase):\n    \"\"\"The short-term memory tests.\"\"\"\n\n    memory: MemoryBase\n    \"\"\"The test memory instance.\"\"\"\n\n    memory_session: MemoryBase\n    \"\"\"The test memory instance for different session.\"\"\"\n\n    memory_user: MemoryBase\n    \"\"\"The test memory instance for different user.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the memory instance for testing.\"\"\"\n        self.msgs = [\n            Msg(\"user\", \"0\", \"user\"),\n            Msg(\"user\", \"1\", \"user\"),\n            Msg(\"assistant\", \"2\", \"assistant\"),\n            Msg(\"system\", \"3\", \"system\"),\n            Msg(\"user\", \"4\", \"user\"),\n            Msg(\"assistant\", \"5\", \"assistant\"),\n            Msg(\"system\", \"6\", \"system\"),\n            Msg(\"user\", \"7\", \"user\"),\n            Msg(\"assistant\", \"8\", \"assistant\"),\n            Msg(\"system\", \"9\", \"system\"),\n        ]\n        for i, msg in enumerate(self.msgs):\n            msg.id = str(i)\n\n    async def _basic_tests(self) -> None:\n        \"\"\"Test the basic functionalities of the short-term memory.\"\"\"\n        # test at the beginning\n        self.assertIsInstance(await self.memory.get_memory(), list)\n        self.assertEqual(\n            len(await self.memory.get_memory()),\n            0,\n        )\n        self.assertEqual(\n            await self.memory.size(),\n            0,\n        )\n\n        await self.memory.update_compressed_summary(\"abc\")\n        self.assertEqual(\n            len(await self.memory.get_memory()),\n            1,\n        )\n\n        await self.memory.update_compressed_summary(\"\")\n        self.assertEqual(\n            len(await self.memory.get_memory()),\n            0,\n        )\n\n        # test adding messages\n        await self.memory.add(self.msgs[:5])\n        msgs = await self.memory.get_memory()\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in range(5)],\n        )\n\n        # test deleting messages by id\n        await self.memory.delete(msg_ids=[\"2\", \"4\"])\n        msgs = await self.memory.get_memory()\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [\"0\", \"1\", \"3\"],\n        )\n        self.assertEqual(\n            await self.memory.size(),\n            3,\n        )\n\n        # test adding more messages\n        await self.memory.add(self.msgs[5:])\n        msgs = await self.memory.get_memory()\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in [0, 1, 3, 5, 6, 7, 8, 9]],\n        )\n\n        # test clearing memory\n        await self.memory.clear()\n        self.assertEqual(\n            await self.memory.size(),\n            0,\n        )\n\n    async def _mark_tests(self) -> None:\n        \"\"\"Test the mark-related functionalities of the short-term memory.\"\"\"\n        # test getting messages by nonexistent mark\n        await self.memory.add(self.msgs[:5])\n        self.assertListEqual(\n            [_.id for _ in await self.memory.get_memory()],\n            [str(_) for _ in range(5)],\n        )\n        self.assertEqual(\n            len(await self.memory.get_memory(mark=\"nonexistent\")),\n            0,\n        )\n\n        # test adding marked messages\n        await self.memory.add(\n            self.msgs[5:7],\n            marks=[\"important\", \"todo\"],\n        )\n        await self.memory.add(self.msgs[7:], marks=\"important\")\n\n        # Test get messages by \"important\" mark\n        msgs = await self.memory.get_memory(mark=\"important\")\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in range(5, 10)],\n        )\n\n        # Test get messages by \"todo\" mark\n        msgs = await self.memory.get_memory(mark=\"todo\")\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in range(5, 7)],\n        )\n\n        # Test get messages excluding \"todo\" mark\n        msgs = await self.memory.get_memory(exclude_mark=\"todo\")\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in [0, 1, 2, 3, 4, 7, 8, 9]],\n        )\n\n        msgs = await self.memory.get_memory(exclude_mark=\"important\")\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in [0, 1, 2, 3, 4]],\n        )\n\n        # add unmarked messages\n        msgs = [\n            Msg(\"user\", \"10\", \"user\"),\n            Msg(\"user\", \"11\", \"user\"),\n        ]\n        msgs[0].id = \"10\"\n        msgs[1].id = \"11\"\n        await self.memory.add(msgs)\n        msgs = await self.memory.get_memory()\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in range(12)],\n        )\n\n        # test marking messages\n        await self.memory.update_messages_mark(\n            msg_ids=[\"0\", \"1\", \"2\"],\n            new_mark=\"review\",\n        )\n        msgs = await self.memory.get_memory(mark=\"review\")\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [\"0\", \"1\", \"2\"],\n        )\n\n        # test adding multiple marks to messages\n        await self.memory.update_messages_mark(\n            msg_ids=[\"6\", \"7\", \"9\"],\n            new_mark=\"unread\",\n        )\n        msgs = await self.memory.get_memory(mark=\"unread\")\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in [6, 7, 9]],\n        )\n        msgs = await self.memory.get_memory(mark=\"important\")\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in [5, 6, 7, 8, 9]],\n        )\n\n        # test unmarking messages\n        await self.memory.update_messages_mark(\n            msg_ids=[\"5\", \"7\"],\n            old_mark=\"important\",\n            new_mark=None,\n        )\n        self.assertListEqual(\n            [_.id for _ in await self.memory.get_memory(mark=\"important\")],\n            [str(_) for _ in [6, 8, 9]],\n        )\n\n        # test updating marks\n        await self.memory.update_messages_mark(\n            msg_ids=[\"6\", \"8\"],\n            old_mark=\"important\",\n            new_mark=\"archived\",\n        )\n        self.assertListEqual(\n            [_.id for _ in await self.memory.get_memory(mark=\"important\")],\n            [\"9\"],\n        )\n        self.assertListEqual(\n            [_.id for _ in await self.memory.get_memory(mark=\"archived\")],\n            [str(_) for _ in [6, 8]],\n        )\n\n        # test deleting messages by mark\n        await self.memory.delete_by_mark(\"important\")\n        msgs = await self.memory.get_memory(mark=\"important\")\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [],\n        )\n        msgs = await self.memory.get_memory()\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11]],\n        )\n\n        await self.memory.delete_by_mark([\"review\", \"archived\"])\n        msgs = await self.memory.get_memory()\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in [3, 4, 5, 7, 10, 11]],\n        )\n\n        await self.memory.clear()\n        msgs = await self.memory.get_memory()\n        self.assertEqual(\n            len(msgs),\n            0,\n        )\n\n    async def _multi_tenant_tests(self) -> None:\n        \"\"\"Test the multi-tenant functionalities of the short-term memory.\"\"\"\n        await self.memory.add(self.msgs[:8])\n        msgs = await self.memory.get_memory()\n        self.assertEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in range(8)],\n        )\n\n        # Add some msgs with overlapping ids to different users' memory\n        await self.memory_user.add(self.msgs[3:])\n        self.assertEqual(\n            [_.id for _ in await self.memory_user.get_memory()],\n            [str(_) for _ in [3, 4, 5, 6, 7, 8, 9]],\n        )\n\n        # Mark messages\n        await self.memory.update_messages_mark(\n            new_mark=\"shared\",\n            msg_ids=[\"5\", \"6\", \"7\"],\n        )\n\n        # mark messages with same ids with different mark for different users\n        await self.memory_user.update_messages_mark(\n            new_mark=\"shared_user\",\n            msg_ids=[\"6\", \"7\", \"8\", \"9\"],\n        )\n\n        # Test if the marks are isolated between different users\n        msgs = await self.memory.get_memory(\n            mark=\"shared\",\n        )\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in [5, 6, 7]],\n        )\n        msgs = await self.memory.get_memory(\n            mark=\"shared_user\",\n        )\n        self.assertEqual(\n            len(msgs),\n            0,\n        )\n\n        msgs = await self.memory_user.get_memory(\n            mark=\"shared_user\",\n        )\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in [6, 7, 8, 9]],\n        )\n        msgs = await self.memory_user.get_memory(\n            mark=\"shared\",\n        )\n        self.assertEqual(\n            len(msgs),\n            0,\n        )\n\n        # Test delete operation is isolated between different sessions\n        await self.memory.delete(\n            msg_ids=[\"6\", \"7\"],\n        )\n        msgs = await self.memory.get_memory()\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in range(6)],\n        )\n        msgs = await self.memory_user.get_memory()\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in range(3, 10)],\n        )\n\n        # Test delete operation by mark is isolated between different sessions\n        await self.memory_user.delete_by_mark(\"shared\")\n        msgs = await self.memory_user.get_memory()\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in range(3, 10)],\n        )\n        msgs = await self.memory.get_memory()\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in range(6)],\n        )\n\n        # Clean up\n        await self.memory.clear()\n        await self.memory_user.clear()\n\n    async def _multi_session_tests(self) -> None:\n        \"\"\"Test the multi-session functionalities of the short-term memory.\"\"\"\n        await self.memory.add(self.msgs[:8])\n        msgs = await self.memory.get_memory()\n        self.assertEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in range(8)],\n        )\n\n        # Add some msgs with overlapping ids to different session's memory\n        await self.memory_session.add(self.msgs[3:])\n        self.assertEqual(\n            [_.id for _ in await self.memory_session.get_memory()],\n            [str(_) for _ in [3, 4, 5, 6, 7, 8, 9]],\n        )\n\n        # Mark messages in first session\n        await self.memory.update_messages_mark(\n            new_mark=\"session1_mark\",\n            msg_ids=[\"5\", \"6\", \"7\"],\n        )\n\n        # mark messages with same ids with different mark for different session\n        await self.memory_session.update_messages_mark(\n            new_mark=\"session2_mark\",\n            msg_ids=[\"6\", \"7\", \"8\", \"9\"],\n        )\n\n        # Test if the marks are isolated between different sessions\n        msgs = await self.memory.get_memory(\n            mark=\"session1_mark\",\n        )\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in [5, 6, 7]],\n        )\n        msgs = await self.memory.get_memory(\n            mark=\"session2_mark\",\n        )\n        self.assertEqual(\n            len(msgs),\n            0,\n        )\n\n        msgs = await self.memory_session.get_memory(\n            mark=\"session2_mark\",\n        )\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in [6, 7, 8, 9]],\n        )\n        msgs = await self.memory_session.get_memory(\n            mark=\"session1_mark\",\n        )\n        self.assertEqual(\n            len(msgs),\n            0,\n        )\n\n        # Test delete operation is isolated between different sessions\n        await self.memory.delete(\n            msg_ids=[\"6\", \"7\"],\n        )\n        msgs = await self.memory.get_memory()\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in range(6)],\n        )\n        msgs = await self.memory_session.get_memory()\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in range(3, 10)],\n        )\n\n        # Test delete operation by mark is isolated between different sessions\n        await self.memory_session.delete_by_mark(\"session1_mark\")\n        msgs = await self.memory_session.get_memory()\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in range(3, 10)],\n        )\n        msgs = await self.memory.get_memory()\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in range(6)],\n        )\n\n        # Clean up\n        await self.memory.clear()\n        await self.memory_session.clear()\n\n    async def _test_add_duplicated_msgs(self) -> None:\n        \"\"\"Test adding duplicated messages to the memory.\"\"\"\n        await self.memory.add(self.msgs[:8])\n        await self.memory.add(self.msgs[5:])\n\n        msgs = await self.memory.get_memory()\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in range(10)],\n        )\n\n        await self.memory.clear()\n\n    async def _test_delete_nonexistent_msg(self) -> None:\n        \"\"\"Test deleting nonexistent messages from the memory.\"\"\"\n        await self.memory.add(self.msgs[:5])\n        await self.memory.delete(msg_ids=[\"nonexistent_id\"])\n        msgs = await self.memory.get_memory()\n        self.assertListEqual(\n            [_.id for _ in msgs],\n            [str(_) for _ in range(5)],\n        )\n\n        await self.memory.clear()\n\n    async def _test_serialization(self) -> None:\n        \"\"\"Test the serialization and deserialization of RedisMemory.\"\"\"\n\n        # Test the state dict before any updates\n        self.assertDictEqual(\n            self.memory.state_dict(),\n            {\n                \"_compressed_summary\": \"\",\n            },\n        )\n\n        # Update compressed summary and test state dict\n        await self.memory.update_compressed_summary(\"Hi there!\")\n        state_dict = self.memory.state_dict()\n\n        # Verify the state dict content\n        self.assertDictEqual(\n            state_dict,\n            {\n                \"_compressed_summary\": \"Hi there!\",\n            },\n        )\n\n        # Clear the compressed summary and verify state dict\n        await self.memory.update_compressed_summary(\"\")\n        self.assertDictEqual(\n            self.memory.state_dict(),\n            {\n                \"_compressed_summary\": \"\",\n            },\n        )\n\n        # Load the previous state dict and verify restoration\n        self.memory.load_state_dict(state_dict)\n        self.assertDictEqual(\n            self.memory.state_dict(),\n            {\n                \"_compressed_summary\": \"Hi there!\",\n            },\n        )\n\n    async def asyncTearDown(self) -> None:\n        \"\"\"Clean up after unittests\"\"\"\n        await self.memory.clear()\n        # Close the session or connection if applicable\n        if hasattr(self.memory, \"close\"):\n            await self.memory.close()\n\n\nclass InMemoryMemoryTest(ShortTermMemoryTest):\n    \"\"\"The in-memory short-term memory tests.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the in-memory memory instance for testing.\"\"\"\n        await super().asyncSetUp()\n        self.memory = InMemoryMemory()\n\n    async def test_memory(self) -> None:\n        \"\"\"Test the in-memory memory functionalities.\"\"\"\n        await self._basic_tests()\n        await self._mark_tests()\n        await self._test_add_duplicated_msgs()\n        await self._test_delete_nonexistent_msg()\n\n    async def test_serialization(self) -> None:\n        \"\"\"Test the serialization and deserialization of InMemoryMemory.\"\"\"\n\n        msg = Msg(\"user\", \"1\", \"user\")\n        await self.memory.add(msg)\n\n        # Test the state dict before any updates\n        self.assertDictEqual(\n            self.memory.state_dict(),\n            {\n                \"_compressed_summary\": \"\",\n                \"content\": [\n                    [\n                        {\n                            \"id\": msg.id,\n                            \"name\": msg.name,\n                            \"role\": msg.role,\n                            \"content\": msg.content,\n                            \"metadata\": msg.metadata,\n                            \"timestamp\": msg.timestamp,\n                        },\n                        [],\n                    ],\n                ],\n            },\n        )\n\n        # Update compressed summary and test state dict\n        await self.memory.update_compressed_summary(\"Hello World!\")\n        state_dict = self.memory.state_dict()\n\n        # Verify the state dict content\n        self.assertDictEqual(\n            state_dict,\n            {\n                \"_compressed_summary\": \"Hello World!\",\n                \"content\": [\n                    [\n                        {\n                            \"id\": msg.id,\n                            \"name\": msg.name,\n                            \"role\": msg.role,\n                            \"content\": msg.content,\n                            \"metadata\": msg.metadata,\n                            \"timestamp\": msg.timestamp,\n                        },\n                        [],\n                    ],\n                ],\n            },\n        )\n\n        # Clear the compressed summary and verify state dict\n        await self.memory.update_compressed_summary(\"\")\n        await self.memory.clear()\n        self.assertDictEqual(\n            self.memory.state_dict(),\n            {\n                \"_compressed_summary\": \"\",\n                \"content\": [],\n            },\n        )\n\n        # Load the previous state dict and verify restoration\n        self.memory.load_state_dict(state_dict)\n        self.assertDictEqual(\n            self.memory.state_dict(),\n            {\n                \"_compressed_summary\": \"Hello World!\",\n                \"content\": [\n                    [\n                        {\n                            \"id\": msg.id,\n                            \"name\": msg.name,\n                            \"role\": msg.role,\n                            \"content\": msg.content,\n                            \"metadata\": msg.metadata,\n                            \"timestamp\": msg.timestamp,\n                        },\n                        [],\n                    ],\n                ],\n            },\n        )\n\n\nclass AsyncSQLAlchemyMemoryTest(ShortTermMemoryTest):\n    \"\"\"The SQLAlchemy short-term memory tests.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the SQLAlchemy memory instance for testing.\"\"\"\n        await super().asyncSetUp()\n        self.engine = create_async_engine(\n            # in-memory SQLite database for testing\n            url=\"sqlite+aiosqlite:///:memory:\",\n        )\n        self.memory = AsyncSQLAlchemyMemory(\n            session_id=\"session_1\",\n            user_id=\"user_1\",\n            engine_or_session=self.engine,\n        )\n\n        self.memory_session = AsyncSQLAlchemyMemory(\n            session_id=\"session_2\",\n            user_id=\"user_1\",\n            engine_or_session=self.engine,\n        )\n\n        self.memory_user = AsyncSQLAlchemyMemory(\n            session_id=\"session_2\",\n            user_id=\"user_2\",\n            engine_or_session=self.engine,\n        )\n\n    async def test_memory(self) -> None:\n        \"\"\"Test the SQLAlchemy memory functionalities.\"\"\"\n        await self._basic_tests()\n        await self._test_add_duplicated_msgs()\n        await self._test_delete_nonexistent_msg()\n        await self._mark_tests()\n        await self._multi_tenant_tests()\n        await self._multi_session_tests()\n        await self._test_serialization()\n\n    async def asyncTearDown(self) -> None:\n        \"\"\"Clean up after unittests\"\"\"\n        await super().asyncTearDown()\n        await self.engine.dispose()\n\n\nclass RedisMemoryTest(ShortTermMemoryTest):\n    \"\"\"The Redis short-term memory tests.\"\"\"\n\n    memory: RedisMemory\n    \"\"\"The Redis memory instance.\"\"\"\n\n    memory_session: RedisMemory\n    \"\"\"The Redis memory instance for different session.\"\"\"\n\n    memory_user: RedisMemory\n    \"\"\"The Redis memory instance for different user.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the Redis memory instance for testing.\"\"\"\n        await super().asyncSetUp()\n        try:\n            import fakeredis.aioredis\n        except ImportError:\n            self.skipTest(\n                \"fakeredis is not installed. Install it via \"\n                \"'pip install fakeredis' to run this test.\",\n            )\n\n        # Use fakeredis for in-memory testing without a real Redis server\n        fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True)\n        self.memory = RedisMemory(\n            user_id=\"user_1\",\n            session_id=\"session_1\",\n            connection_pool=fake_redis.connection_pool,\n        )\n\n        self.memory_session = RedisMemory(\n            user_id=\"user_1\",\n            session_id=\"session_2\",\n            connection_pool=fake_redis.connection_pool,\n        )\n\n        self.memory_user = RedisMemory(\n            user_id=\"user_2\",\n            session_id=\"session_2\",\n            connection_pool=fake_redis.connection_pool,\n        )\n\n    async def test_memory(self) -> None:\n        \"\"\"Test the Redis memory functionalities.\"\"\"\n        await self._basic_tests()\n        await self._mark_tests()\n        await self._test_add_duplicated_msgs()\n        await self._test_delete_nonexistent_msg()\n        await self._multi_tenant_tests()\n        await self._multi_session_tests()\n        await self._test_serialization()\n\n    async def test_ttl(self) -> None:\n        \"\"\"Test the TTL functionality of the Redis memory.\"\"\"\n        # Set a short TTL for testing\n        self.memory.key_ttl = 2  # 2 seconds\n\n        # Add messages and verify they exist\n        await self.memory.add(self.msgs[:5])\n        msgs = await self.memory.get_memory()\n        self.assertEqual(\n            len(msgs),\n            5,\n        )\n\n        # Wait for TTL to expire\n        await asyncio.sleep(3)\n\n        msgs = await self.memory.get_memory()\n        self.assertEqual(\n            len(msgs),\n            0,\n        )\n\n\nclass RedisMemoryTestWithBytes(ShortTermMemoryTest):\n    \"\"\"The Redis short-term memory tests with decode_responses=False.\"\"\"\n\n    memory: RedisMemory\n    \"\"\"The Redis memory instance.\"\"\"\n\n    memory_session: RedisMemory\n    \"\"\"The Redis memory instance for different session.\"\"\"\n\n    memory_user: RedisMemory\n    \"\"\"The Redis memory instance for different user.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the Redis memory instance for testing.\"\"\"\n        await super().asyncSetUp()\n        try:\n            import fakeredis.aioredis\n        except ImportError:\n            self.skipTest(\n                \"fakeredis is not installed. Install it via \"\n                \"'pip install fakeredis' to run this test.\",\n            )\n\n        # Use fakeredis with decode_responses=False to test bytes handling\n        fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=False)\n        self.memory = RedisMemory(\n            user_id=\"user_1\",\n            session_id=\"session_1\",\n            connection_pool=fake_redis.connection_pool,\n        )\n\n        self.memory_session = RedisMemory(\n            user_id=\"user_1\",\n            session_id=\"session_2\",\n            connection_pool=fake_redis.connection_pool,\n        )\n\n        self.memory_user = RedisMemory(\n            user_id=\"user_2\",\n            session_id=\"session_2\",\n            connection_pool=fake_redis.connection_pool,\n        )\n\n    async def test_memory(self) -> None:\n        \"\"\"Test the Redis memory functionalities.\"\"\"\n        await self._basic_tests()\n        await self._mark_tests()\n        await self._test_add_duplicated_msgs()\n        await self._test_delete_nonexistent_msg()\n        await self._multi_tenant_tests()\n        await self._multi_session_tests()\n\n    async def test_ttl(self) -> None:\n        \"\"\"Test the TTL functionality of the Redis memory.\"\"\"\n        # Set a short TTL for testing\n        self.memory.key_ttl = 2  # 2 seconds\n\n        # Add messages and verify they exist\n        await self.memory.add(self.msgs[:5])\n        msgs = await self.memory.get_memory()\n        self.assertEqual(\n            len(msgs),\n            5,\n        )\n\n        # Wait for TTL to expire\n        await asyncio.sleep(3)\n\n        msgs = await self.memory.get_memory()\n        self.assertEqual(\n            len(msgs),\n            0,\n        )\n"
  },
  {
    "path": "tests/model_anthropic_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unit tests for Anthropic API model class.\"\"\"\nfrom typing import Any, AsyncGenerator\nfrom unittest.async_case import IsolatedAsyncioTestCase\nfrom unittest.mock import Mock, patch, AsyncMock\nfrom pydantic import BaseModel\n\nfrom agentscope.model import AnthropicChatModel, ChatResponse\nfrom agentscope.message import TextBlock, ToolUseBlock, ThinkingBlock\n\n\nclass SampleModel(BaseModel):\n    \"\"\"Sample Pydantic model for testing structured output.\"\"\"\n\n    name: str\n    age: int\n\n\nclass AnthropicMessageMock:\n    \"\"\"Mock class for Anthropic message objects.\"\"\"\n\n    def __init__(self, content: list = None, usage: dict = None):\n        self.content = content or []\n        self.usage = self._create_usage_mock(usage) if usage else None\n\n    def _create_usage_mock(self, usage_data: dict) -> Mock:\n        usage_mock = Mock()\n        usage_mock.input_tokens = usage_data.get(\"input_tokens\", 0)\n        usage_mock.output_tokens = usage_data.get(\"output_tokens\", 0)\n        return usage_mock\n\n\nclass AnthropicContentBlockMock:\n    \"\"\"Mock class for Anthropic content blocks.\"\"\"\n\n    def __init__(self, block_type: str, **kwargs: Any) -> None:\n        self.type = block_type\n        for key, value in kwargs.items():\n            setattr(self, key, value)\n\n\nclass AnthropicEventMock:\n    \"\"\"Mock class for Anthropic streaming events.\"\"\"\n\n    def __init__(self, event_type: str, **kwargs: Any) -> None:\n        self.type = event_type\n        for key, value in kwargs.items():\n            setattr(self, key, value)\n\n\nclass TestAnthropicChatModel(IsolatedAsyncioTestCase):\n    \"\"\"Test cases for AnthropicChatModel.\"\"\"\n\n    def test_init_default_params(self) -> None:\n        \"\"\"Test initialization with default parameters.\"\"\"\n        with patch(\"anthropic.AsyncAnthropic\") as mock_client:\n            model = AnthropicChatModel(\n                model_name=\"claude-3-sonnet-20240229\",\n                api_key=\"test_key\",\n            )\n            self.assertEqual(model.model_name, \"claude-3-sonnet-20240229\")\n            self.assertEqual(model.max_tokens, 2048)\n            self.assertTrue(model.stream)\n            self.assertIsNone(model.thinking)\n            self.assertEqual(model.generate_kwargs, {})\n            mock_client.assert_called_once_with(api_key=\"test_key\")\n\n    def test_init_with_custom_params(self) -> None:\n        \"\"\"Test initialization with custom parameters.\"\"\"\n        thinking_config = {\"type\": \"enabled\", \"budget_tokens\": 1024}\n        generate_kwargs = {\"temperature\": 0.7, \"top_p\": 0.9}\n        client_kwargs = {\"timeout\": 30}\n\n        with patch(\"anthropic.AsyncAnthropic\") as mock_client:\n            model = AnthropicChatModel(\n                model_name=\"claude-3-opus-20240229\",\n                api_key=\"test_key\",\n                max_tokens=4096,\n                stream=False,\n                thinking=thinking_config,\n                client_kwargs=client_kwargs,\n                generate_kwargs=generate_kwargs,\n            )\n            self.assertEqual(model.model_name, \"claude-3-opus-20240229\")\n            self.assertEqual(model.max_tokens, 4096)\n            self.assertFalse(model.stream)\n            self.assertEqual(model.thinking, thinking_config)\n            self.assertEqual(model.generate_kwargs, generate_kwargs)\n            mock_client.assert_called_once_with(api_key=\"test_key\", timeout=30)\n\n    async def test_call_with_regular_messages(self) -> None:\n        \"\"\"Test calling with regular messages.\"\"\"\n        with patch(\"anthropic.AsyncAnthropic\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            model = AnthropicChatModel(\n                model_name=\"claude-3-sonnet-20240229\",\n                api_key=\"test_key\",\n                stream=False,\n            )\n            model.client = mock_client\n\n            messages = [{\"role\": \"user\", \"content\": \"Hello\"}]\n            mock_response = AnthropicMessageMock(\n                content=[\n                    AnthropicContentBlockMock(\n                        \"text\",\n                        text=\"Hello! How can I help you?\",\n                    ),\n                ],\n                usage={\"input_tokens\": 10, \"output_tokens\": 20},\n            )\n            mock_client.messages.create = AsyncMock(return_value=mock_response)\n\n            result = await model(messages)\n            call_args = mock_client.messages.create.call_args[1]\n            self.assertEqual(call_args[\"model\"], \"claude-3-sonnet-20240229\")\n            self.assertEqual(call_args[\"max_tokens\"], 2048)\n            self.assertFalse(call_args[\"stream\"])\n            self.assertEqual(call_args[\"messages\"], messages)\n            self.assertIsInstance(result, ChatResponse)\n            expected_content = [\n                TextBlock(type=\"text\", text=\"Hello! How can I help you?\"),\n            ]\n            self.assertEqual(result.content, expected_content)\n            self.assertEqual(result.usage.input_tokens, 10)\n            self.assertEqual(result.usage.output_tokens, 20)\n\n    async def test_call_with_system_message(self) -> None:\n        \"\"\"Test calling with system message extraction.\"\"\"\n        with patch(\"anthropic.AsyncAnthropic\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            model = AnthropicChatModel(\n                model_name=\"claude-3-sonnet-20240229\",\n                api_key=\"test_key\",\n                stream=False,\n            )\n            model.client = mock_client\n\n            messages = [\n                {\"role\": \"system\", \"content\": \"You are a helpful assistant\"},\n                {\"role\": \"user\", \"content\": \"Hello\"},\n            ]\n            mock_response = AnthropicMessageMock(\n                content=[AnthropicContentBlockMock(\"text\", text=\"Hi there!\")],\n                usage={\"input_tokens\": 15, \"output_tokens\": 5},\n            )\n            mock_client.messages.create = AsyncMock(return_value=mock_response)\n            await model(messages)\n\n            call_args = mock_client.messages.create.call_args[1]\n            self.assertEqual(\n                call_args[\"system\"],\n                \"You are a helpful assistant\",\n            )\n            self.assertEqual(\n                call_args[\"messages\"],\n                [\n                    {\"role\": \"user\", \"content\": \"Hello\"},\n                ],\n            )\n\n    async def test_call_with_thinking_enabled(self) -> None:\n        \"\"\"Test calling with thinking functionality enabled.\"\"\"\n        with patch(\"anthropic.AsyncAnthropic\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            thinking_config = {\"type\": \"enabled\", \"budget_tokens\": 1024}\n            model = AnthropicChatModel(\n                model_name=\"claude-3-sonnet-20240229\",\n                api_key=\"test_key\",\n                stream=False,\n                thinking=thinking_config,\n            )\n            model.client = mock_client\n\n            messages = [\n                {\"role\": \"user\", \"content\": \"Think about this problem\"},\n            ]\n            thinking_block = AnthropicContentBlockMock(\n                \"thinking\",\n                thinking=\"Let me analyze this step by step...\",\n                signature=\"thinking_signature_123\",\n            )\n            text_block = AnthropicContentBlockMock(\n                \"text\",\n                text=\"Here's my analysis\",\n            )\n            mock_response = AnthropicMessageMock(\n                content=[thinking_block, text_block],\n                usage={\"input_tokens\": 20, \"output_tokens\": 40},\n            )\n            mock_client.messages.create = AsyncMock(return_value=mock_response)\n            result = await model(messages)\n\n            call_args = mock_client.messages.create.call_args[1]\n            self.assertEqual(call_args[\"thinking\"], thinking_config)\n            expected_thinking_block = ThinkingBlock(\n                type=\"thinking\",\n                thinking=\"Let me analyze this step by step...\",\n            )\n            expected_thinking_block[\"signature\"] = \"thinking_signature_123\"\n            expected_content = [\n                expected_thinking_block,\n                TextBlock(type=\"text\", text=\"Here's my analysis\"),\n            ]\n            self.assertEqual(result.content, expected_content)\n\n    async def test_call_with_tools_integration(self) -> None:\n        \"\"\"Test full integration of tool calls.\"\"\"\n        with patch(\"anthropic.AsyncAnthropic\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            model = AnthropicChatModel(\n                model_name=\"claude-3-sonnet-20240229\",\n                api_key=\"test_key\",\n                stream=False,\n            )\n            model.client = mock_client\n\n            messages = [{\"role\": \"user\", \"content\": \"What's the weather?\"}]\n            tools = [\n                {\n                    \"type\": \"function\",\n                    \"function\": {\n                        \"name\": \"get_weather\",\n                        \"description\": \"Get weather info\",\n                        \"parameters\": {\"type\": \"object\"},\n                    },\n                },\n            ]\n            text_block = AnthropicContentBlockMock(\n                \"text\",\n                text=\"I'll check the weather\",\n            )\n            tool_block = AnthropicContentBlockMock(\n                \"tool_use\",\n                id=\"tool_123\",\n                name=\"get_weather\",\n                input={\"location\": \"Beijing\"},\n            )\n\n            mock_response = AnthropicMessageMock(\n                content=[text_block, tool_block],\n                usage={\"input_tokens\": 25, \"output_tokens\": 15},\n            )\n            mock_client.messages.create = AsyncMock(return_value=mock_response)\n            result = await model(messages, tools=tools, tool_choice=\"auto\")\n            # Verify tool formatting\n            call_args = mock_client.messages.create.call_args[1]\n            expected_tools = [\n                {\n                    \"name\": \"get_weather\",\n                    \"description\": \"Get weather info\",\n                    \"input_schema\": {\"type\": \"object\"},\n                },\n            ]\n            self.assertEqual(call_args[\"tools\"], expected_tools)\n            self.assertEqual(call_args[\"tool_choice\"], {\"type\": \"auto\"})\n            expected_content = [\n                TextBlock(type=\"text\", text=\"I'll check the weather\"),\n                ToolUseBlock(\n                    type=\"tool_use\",\n                    id=\"tool_123\",\n                    name=\"get_weather\",\n                    input={\"location\": \"Beijing\"},\n                ),\n            ]\n            self.assertEqual(result.content, expected_content)\n\n    async def test_streaming_response_processing(self) -> None:\n        \"\"\"Test processing of streaming response.\"\"\"\n        with patch(\"anthropic.AsyncAnthropic\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            model = AnthropicChatModel(\n                model_name=\"claude-3-sonnet-20240229\",\n                api_key=\"test_key\",\n                stream=True,\n            )\n            model.client = mock_client\n\n            messages = [{\"role\": \"user\", \"content\": \"Hello\"}]\n            events = [\n                AnthropicEventMock(\n                    \"message_start\",\n                    message=Mock(usage=Mock(input_tokens=10, output_tokens=0)),\n                ),\n                AnthropicEventMock(\n                    \"content_block_delta\",\n                    index=0,\n                    delta=Mock(type=\"text_delta\", text=\"Hello\"),\n                ),\n                AnthropicEventMock(\n                    \"content_block_delta\",\n                    index=0,\n                    delta=Mock(type=\"text_delta\", text=\" there!\"),\n                ),\n                AnthropicEventMock(\n                    \"message_delta\",\n                    usage=Mock(output_tokens=5),\n                ),\n            ]\n\n            async def mock_stream() -> AsyncGenerator:\n                for event in events:\n                    yield event\n\n            mock_client.messages.create = AsyncMock(return_value=mock_stream())\n            result = await model(messages)\n            responses = []\n            async for response in result:\n                responses.append(response)\n\n            self.assertEqual(len(responses), 2)\n            final_response = responses[-1]\n            self.assertIsInstance(final_response, ChatResponse)\n            expected_content = [\n                TextBlock(type=\"text\", text=\"Hello there!\"),\n            ]\n            self.assertEqual(final_response.content, expected_content)\n\n    async def test_generate_kwargs_integration(self) -> None:\n        \"\"\"Test integration of generate_kwargs.\"\"\"\n        with patch(\"anthropic.AsyncAnthropic\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            generate_kwargs = {\"temperature\": 0.7, \"top_p\": 0.9}\n            model = AnthropicChatModel(\n                model_name=\"claude-3-sonnet-20240229\",\n                api_key=\"test_key\",\n                stream=False,\n                generate_kwargs=generate_kwargs,\n            )\n            model.client = mock_client\n\n            messages = [{\"role\": \"user\", \"content\": \"Test\"}]\n            mock_response = AnthropicMessageMock(\n                content=[\n                    AnthropicContentBlockMock(\"text\", text=\"Test response\"),\n                ],\n                usage={\"input_tokens\": 5, \"output_tokens\": 10},\n            )\n            mock_client.messages.create = AsyncMock(return_value=mock_response)\n            await model(messages, top_k=40)\n            call_args = mock_client.messages.create.call_args[1]\n            self.assertEqual(call_args[\"temperature\"], 0.7)\n            self.assertEqual(call_args[\"top_p\"], 0.9)\n            self.assertEqual(call_args[\"top_k\"], 40)\n\n    async def test_call_with_structured_model_integration(self) -> None:\n        \"\"\"Test full integration of structured model.\"\"\"\n        with patch(\"anthropic.AsyncAnthropic\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            model = AnthropicChatModel(\n                model_name=\"claude-3-sonnet-20240229\",\n                api_key=\"test_key\",\n                stream=False,\n            )\n            model.client = mock_client\n\n            messages = [{\"role\": \"user\", \"content\": \"Generate a person\"}]\n\n            text_block = AnthropicContentBlockMock(\n                \"text\",\n                text=\"Here's a person\",\n            )\n            tool_block = AnthropicContentBlockMock(\n                \"tool_use\",\n                id=\"tool_123\",\n                name=\"generate_structured_output\",\n                input={\"name\": \"John\", \"age\": 30},\n            )\n\n            mock_response = AnthropicMessageMock(\n                content=[text_block, tool_block],\n                usage={\"input_tokens\": 20, \"output_tokens\": 15},\n            )\n\n            mock_client.messages.create = AsyncMock(return_value=mock_response)\n            result = await model(messages, structured_model=SampleModel)\n\n            call_args = mock_client.messages.create.call_args[1]\n            self.assertIn(\"tools\", call_args)\n            self.assertIn(\"tool_choice\", call_args)\n            expected_tools = [\n                {\n                    \"name\": \"generate_structured_output\",\n                    \"description\": \"Generate the required structured output\"\n                    \" with this function\",\n                    \"input_schema\": {\n                        \"description\": \"Sample Pydantic model for testing \"\n                        \"structured output.\",\n                        \"properties\": {\n                            \"name\": {\"type\": \"string\"},\n                            \"age\": {\"type\": \"integer\"},\n                        },\n                        \"required\": [\"name\", \"age\"],\n                        \"type\": \"object\",\n                    },\n                },\n            ]\n            self.assertEqual(call_args[\"tools\"], expected_tools)\n            self.assertEqual(\n                call_args[\"tool_choice\"],\n                {\n                    \"type\": \"tool\",\n                    \"name\": \"generate_structured_output\",\n                },\n            )\n            self.assertIsInstance(result, ChatResponse)\n            expected_content = [\n                TextBlock(type=\"text\", text=\"Here's a person\"),\n                ToolUseBlock(\n                    type=\"tool_use\",\n                    id=\"tool_123\",\n                    name=\"generate_structured_output\",\n                    input={\"name\": \"John\", \"age\": 30},\n                ),\n            ]\n            self.assertEqual(result.content, expected_content)\n            self.assertEqual(result.metadata, {\"name\": \"John\", \"age\": 30})\n"
  },
  {
    "path": "tests/model_dashscope_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unit tests for DashScope API model class.\"\"\"\nfrom typing import Any, AsyncGenerator\nfrom unittest.async_case import IsolatedAsyncioTestCase\nfrom unittest.mock import AsyncMock, Mock, patch\nfrom http import HTTPStatus\nfrom pydantic import BaseModel\n\nfrom agentscope.model import DashScopeChatModel, ChatResponse\nfrom agentscope.message import TextBlock, ToolUseBlock, ThinkingBlock\n\n\nclass MessageMock(dict):\n    \"\"\"Mock class for message objects, supports both dictionary and\n    attribute access.\"\"\"\n\n    def __init__(self, data: dict[str, Any]):\n        super().__init__(data)\n        for key, value in data.items():\n            setattr(self, key, value)\n\n\nclass SampleModel(BaseModel):\n    \"\"\"Sample Pydantic model for testing structured output.\"\"\"\n\n    name: str\n    age: int\n\n\nclass TestDashScopeChatModel(IsolatedAsyncioTestCase):\n    \"\"\"Test cases for DashScopeChatModel.\"\"\"\n\n    def test_init_default_params(self) -> None:\n        \"\"\"Test initialization with default parameters.\"\"\"\n        model = DashScopeChatModel(\n            model_name=\"qwen-turbo\",\n            api_key=\"test_key\",\n        )\n        self.assertEqual(model.model_name, \"qwen-turbo\")\n        self.assertEqual(model.api_key, \"test_key\")\n        self.assertTrue(model.stream)\n        self.assertIsNone(model.enable_thinking)\n        self.assertEqual(model.generate_kwargs, {})\n\n    def test_init_with_enable_thinking_forces_stream(self) -> None:\n        \"\"\"Test that enable_thinking=True forces stream=True.\"\"\"\n        with patch(\"agentscope.model._dashscope_model.logger\") as mock_logger:\n            model = DashScopeChatModel(\n                model_name=\"qwen-turbo\",\n                api_key=\"test_key\",\n                stream=False,\n                enable_thinking=True,\n            )\n            self.assertTrue(model.stream)\n            self.assertTrue(model.enable_thinking)\n            mock_logger.info.assert_called_once()\n\n    def test_init_with_custom_params(self) -> None:\n        \"\"\"Test initialization with custom parameters.\"\"\"\n        generate_kwargs = {\"temperature\": 0.7, \"max_tokens\": 1000}\n        model = DashScopeChatModel(\n            model_name=\"qwen-max\",\n            api_key=\"test_key\",\n            stream=False,\n            enable_thinking=False,\n            generate_kwargs=generate_kwargs,\n        )\n        self.assertEqual(model.model_name, \"qwen-max\")\n        self.assertFalse(model.stream)\n        self.assertFalse(model.enable_thinking)\n        self.assertEqual(model.generate_kwargs, generate_kwargs)\n\n    async def test_call_with_regular_model(self) -> None:\n        \"\"\"Test calling a regular model.\"\"\"\n        model = DashScopeChatModel(\n            model_name=\"qwen-turbo\",\n            api_key=\"test_key\",\n            stream=False,\n        )\n        messages = [{\"role\": \"user\", \"content\": \"Hello\"}]\n\n        mock_response = self._create_mock_response(\n            \"Hello! How can I help you?\",\n        )\n        with patch(\n            \"dashscope.aigc.generation.AioGeneration.call\",\n        ) as mock_call:\n            mock_call.return_value = mock_response\n            result = await model(messages)\n            call_args = mock_call.call_args[1]\n            self.assertEqual(call_args[\"messages\"], messages)\n            self.assertEqual(call_args[\"model\"], \"qwen-turbo\")\n            self.assertFalse(call_args[\"stream\"])\n            self.assertIsInstance(result, ChatResponse)\n            expected_content = [\n                TextBlock(type=\"text\", text=\"Hello! How can I help you?\"),\n            ]\n            self.assertEqual(result.content, expected_content)\n\n    async def test_call_with_tools_integration(self) -> None:\n        \"\"\"Test full integration of tool calls.\"\"\"\n        model = DashScopeChatModel(\n            model_name=\"qwen-turbo\",\n            api_key=\"test_key\",\n            stream=False,\n        )\n        messages = [{\"role\": \"user\", \"content\": \"What's the weather?\"}]\n        tools = [\n            {\n                \"type\": \"function\",\n                \"function\": {\n                    \"name\": \"get_weather\",\n                    \"description\": \"Get weather info\",\n                    \"parameters\": {\"type\": \"object\"},\n                },\n            },\n        ]\n\n        mock_response = self._create_mock_response_with_tools(\n            \"I'll check the weather for you.\",\n            [\n                {\n                    \"id\": \"call_123\",\n                    \"function\": {\n                        \"name\": \"get_weather\",\n                        \"arguments\": '{\"location\": \"Beijing\"}',\n                    },\n                },\n            ],\n        )\n\n        with patch(\n            \"dashscope.aigc.generation.AioGeneration.call\",\n        ) as mock_call:\n            mock_call.return_value = mock_response\n            result = await model(messages, tools=tools, tool_choice=\"auto\")\n            call_args = mock_call.call_args[1]\n            self.assertIn(\"tools\", call_args)\n            self.assertIn(\"tool_choice\", call_args)\n            self.assertEqual(call_args[\"tool_choice\"], \"auto\")\n\n            expected_content = [\n                TextBlock(type=\"text\", text=\"I'll check the weather for you.\"),\n                ToolUseBlock(\n                    type=\"tool_use\",\n                    id=\"call_123\",\n                    name=\"get_weather\",\n                    input={\"location\": \"Beijing\"},\n                ),\n            ]\n            self.assertEqual(result.content, expected_content)\n\n    async def test_call_with_enable_thinking_streaming(self) -> None:\n        \"\"\"Test streaming response with thinking mode enabled.\"\"\"\n        model = DashScopeChatModel(\n            model_name=\"qwen-turbo\",\n            api_key=\"test_key\",\n            enable_thinking=True,\n        )\n        messages = [{\"role\": \"user\", \"content\": \"Solve this problem\"}]\n\n        chunks = [\n            self._create_mock_chunk(\n                content=\"Solution\",\n                reasoning_content=\"Let me think...\",\n            ),\n        ]\n\n        with patch(\n            \"dashscope.aigc.generation.AioGeneration.call\",\n        ) as mock_call:\n            mock_call.return_value = self._create_async_generator(chunks)\n            result = await model(messages)\n\n            call_args = mock_call.call_args[1]\n            self.assertTrue(call_args[\"enable_thinking\"])\n            self.assertTrue(call_args[\"stream\"])\n            responses = []\n            async for response in result:\n                responses.append(response)\n            self.assertGreater(len(responses), 0)\n            self.assertIsInstance(responses[0], ChatResponse)\n\n            expected_content = [\n                ThinkingBlock(type=\"thinking\", thinking=\"Let me think...\"),\n                TextBlock(type=\"text\", text=\"Solution\"),\n            ]\n            self.assertEqual(responses[0].content, expected_content)\n\n    async def test_call_with_structured_model_integration(self) -> None:\n        \"\"\"Test full integration of a structured model.\"\"\"\n        model = DashScopeChatModel(\n            model_name=\"qwen-turbo\",\n            api_key=\"test_key\",\n            stream=False,\n        )\n        messages = [{\"role\": \"user\", \"content\": \"Generate a person\"}]\n\n        mock_response = self._create_mock_response_with_tools(\n            \"Here's a person\",\n            [\n                {\n                    \"id\": \"call_123\",\n                    \"function\": {\n                        \"name\": \"generate_structured_output\",\n                        \"arguments\": '{\"name\": \"John\", \"age\": 30}',\n                    },\n                },\n            ],\n        )\n\n        with patch(\n            \"dashscope.aigc.generation.AioGeneration.call\",\n        ) as mock_call:\n            mock_call.return_value = mock_response\n\n            result = await model(messages, structured_model=SampleModel)\n            call_args = mock_call.call_args[1]\n\n            expected_tools = [\n                {\n                    \"type\": \"function\",\n                    \"function\": {\n                        \"name\": \"generate_structured_output\",\n                        \"description\": \"Generate the required structured\"\n                        \" output with this function\",\n                        \"parameters\": {\n                            \"description\": \"Sample Pydantic model for \"\n                            \"testing structured output.\",\n                            \"properties\": {\n                                \"name\": {\n                                    \"type\": \"string\",\n                                },\n                                \"age\": {\n                                    \"type\": \"integer\",\n                                },\n                            },\n                            \"required\": [\n                                \"name\",\n                                \"age\",\n                            ],\n                            \"type\": \"object\",\n                        },\n                    },\n                },\n            ]\n            self.assertEqual(call_args[\"tools\"], expected_tools)\n            self.assertEqual(\n                call_args[\"tool_choice\"],\n                {\n                    \"type\": \"function\",\n                    \"function\": {\n                        \"name\": \"generate_structured_output\",\n                    },\n                },\n            )\n\n            self.assertIsInstance(result, ChatResponse)\n            self.assertEqual(result.metadata, {\"name\": \"John\", \"age\": 30})\n            expected_content = [\n                TextBlock(type=\"text\", text=\"Here's a person\"),\n                ToolUseBlock(\n                    type=\"tool_use\",\n                    id=\"call_123\",\n                    name=\"generate_structured_output\",\n                    input={\"name\": \"John\", \"age\": 30},\n                ),\n            ]\n            self.assertEqual(result.content, expected_content)\n\n    async def test_streaming_response_processing(self) -> None:\n        \"\"\"Test processing of streaming response.\"\"\"\n        model = DashScopeChatModel(\n            model_name=\"qwen-turbo\",\n            api_key=\"test_key\",\n            stream=True,\n        )\n        messages = [{\"role\": \"user\", \"content\": \"Hello\"}]\n\n        chunks = [\n            self._create_mock_chunk(\n                content=\"Hello\",\n                reasoning_content=\"I should greet\",\n                tool_calls=[],\n            ),\n            self._create_mock_chunk(\n                content=\" there\",\n                reasoning_content=\" the\",\n                tool_calls=[\n                    {\n                        \"index\": 0,\n                        \"id\": \"call_123\",\n                        \"function\": {\n                            \"name\": \"greet\",\n                            \"arguments\": '{\"name\": ',\n                        },\n                    },\n                ],\n            ),\n            self._create_mock_chunk(\n                content=\"!\",\n                reasoning_content=\" user\",\n                tool_calls=[\n                    {\n                        \"index\": 0,\n                        \"id\": \"call_123\",\n                        \"function\": {\n                            \"arguments\": '\"user\"}',\n                        },\n                    },\n                ],\n            ),\n        ]\n\n        with patch(\n            \"dashscope.aigc.generation.AioGeneration.call\",\n        ) as mock_call:\n            mock_call.return_value = self._create_async_generator(chunks)\n            result = await model(messages)\n\n            responses = []\n            async for response in result:\n                responses.append(response)\n            self.assertEqual(len(responses), 3)\n            final_response = responses[-1]\n\n            expected_content = [\n                ThinkingBlock(\n                    type=\"thinking\",\n                    thinking=\"I should greet the user\",\n                ),\n                TextBlock(type=\"text\", text=\"Hello there!\"),\n                ToolUseBlock(\n                    id=\"call_123\",\n                    name=\"greet\",\n                    input={\"name\": \"user\"},\n                    type=\"tool_use\",\n                    raw_input='{\"name\": \"user\"}',\n                ),\n            ]\n            self.assertEqual(final_response.content, expected_content)\n\n    def test_tools_schema_validation_through_api(self) -> None:\n        \"\"\"Test tools schema validation through API call.\"\"\"\n        model = DashScopeChatModel(\n            model_name=\"qwen-turbo\",\n            api_key=\"test_key\",\n            stream=False,\n        )\n        # Test valid tools schema\n        valid_tools = [\n            {\n                \"type\": \"function\",\n                \"function\": {\n                    \"name\": \"get_weather\",\n                    \"description\": \"Get weather info\",\n                },\n            },\n        ]\n\n        # This test validates the format of the tools schema via an actual\n        # API call\n        messages = [{\"role\": \"user\", \"content\": \"Test\"}]\n        mock_response = self._create_mock_response(\"Test\")\n\n        with patch(\n            \"dashscope.aigc.generation.AioGeneration.call\",\n        ) as mock_call:\n            mock_call.return_value = mock_response\n\n            # Should not throw an exception\n            try:\n                import asyncio\n\n                loop = asyncio.get_event_loop()\n                if loop.is_running():\n                    # If event loop is already running, create a task\n                    loop.create_task(model(messages, tools=valid_tools))\n                else:\n                    loop.run_until_complete(model(messages, tools=valid_tools))\n            except Exception as e:\n                if \"schema must be a dict\" in str(e):\n                    self.fail(\"Valid tools schema was rejected\")\n\n    async def test_call_with_multimodal_model(self) -> None:\n        \"\"\"Test multimodal model uses AioMultiModalConversation (async).\"\"\"\n        model = DashScopeChatModel(\n            model_name=\"qwen-vl-plus\",\n            api_key=\"test_key\",\n            stream=False,\n            multimodality=True,\n        )\n        messages = [{\"role\": \"user\", \"content\": \"Describe this image.\"}]\n        mock_response = self._create_mock_response(\"This is a test image.\")\n        with patch(\n            \"dashscope.AioMultiModalConversation.call\",\n            new_callable=AsyncMock,\n        ) as mock_call:\n            mock_call.return_value = mock_response\n            result = await model(messages)\n            mock_call.assert_called_once()\n            call_kwargs = mock_call.call_args[1]\n            self.assertEqual(call_kwargs[\"messages\"], messages)\n            self.assertEqual(call_kwargs[\"model\"], \"qwen-vl-plus\")\n            self.assertIsInstance(result, ChatResponse)\n            self.assertEqual(\n                result.content,\n                [TextBlock(type=\"text\", text=\"This is a test image.\")],\n            )\n\n    async def test_error_handling_scenarios(self) -> None:\n        \"\"\"Test various error handling scenarios.\"\"\"\n        model = DashScopeChatModel(\n            model_name=\"qwen-turbo\",\n            api_key=\"test_key\",\n            stream=False,\n        )\n        messages = [{\"role\": \"user\", \"content\": \"Hello\"}]\n\n        # Test failure of non-streaming API call\n        mock_response = Mock()\n        mock_response.status_code = 400\n        with patch(\n            \"dashscope.aigc.generation.AioGeneration.call\",\n        ) as mock_call:\n            mock_call.return_value = mock_response\n            with self.assertRaises(RuntimeError):\n                await model(messages)\n\n    # Auxiliary methods\n    def _create_mock_response(self, content: str) -> Mock:\n        \"\"\"Create a standard mock response.\"\"\"\n        mock_response = Mock()\n        mock_response.status_code = 200\n        mock_response.output.choices = [Mock()]\n        mock_response.output.choices[0].message = MessageMock(\n            {\"content\": content},\n        )\n        mock_response.usage = Mock()\n        mock_response.usage.input_tokens = 10\n        mock_response.usage.output_tokens = 20\n        return mock_response\n\n    def _create_mock_response_with_tools(\n        self,\n        content: str,\n        tool_calls: list,\n    ) -> Mock:\n        \"\"\"Create a mock response containing tool calls.\"\"\"\n        mock_response = Mock()\n        mock_response.status_code = 200\n        mock_response.output.choices = [Mock()]\n        mock_response.output.choices[0].message = MessageMock(\n            {\n                \"content\": content,\n                \"tool_calls\": tool_calls,\n            },\n        )\n        mock_response.usage = Mock()\n        mock_response.usage.input_tokens = 20\n        mock_response.usage.output_tokens = 30\n        return mock_response\n\n    def _create_mock_chunk(\n        self,\n        content: str = \"\",\n        reasoning_content: str = \"\",\n        tool_calls: list = None,\n    ) -> Mock:\n        \"\"\"Create a mock chunk for streaming responses.\"\"\"\n        chunk = Mock()\n        chunk.status_code = HTTPStatus.OK\n        chunk.output.choices = [Mock()]\n        chunk.output.choices[0].message = MessageMock(\n            {\n                \"content\": content,\n                \"reasoning_content\": reasoning_content,\n                \"tool_calls\": tool_calls or [],\n            },\n        )\n        chunk.usage = Mock()\n        chunk.usage.input_tokens = 5\n        chunk.usage.output_tokens = 10\n        return chunk\n\n    async def _create_async_generator(self, items: list) -> AsyncGenerator:\n        \"\"\"Create an asynchronous generator.\"\"\"\n        for item in items:\n            yield item\n"
  },
  {
    "path": "tests/model_gemini_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unit tests for Google Gemini API model class.\"\"\"\nimport json\nfrom typing import AsyncGenerator\nfrom unittest.async_case import IsolatedAsyncioTestCase\nfrom unittest.mock import Mock, patch, AsyncMock\nfrom pydantic import BaseModel\n\nfrom agentscope.model import GeminiChatModel, ChatResponse\nfrom agentscope.message import TextBlock, ToolUseBlock, ThinkingBlock\n\n\nclass GeminiResponseMock:\n    \"\"\"Mock class for Gemini response objects.\"\"\"\n\n    def __init__(\n        self,\n        text: str = \"\",\n        function_calls: list = None,\n        usage_metadata: dict = None,\n        candidates: list = None,\n    ):\n        self.text = text\n        self.function_calls = function_calls or []\n        self.usage_metadata = (\n            self._create_usage_mock(usage_metadata) if usage_metadata else None\n        )\n\n        if candidates:\n            # Use provided candidates structure\n            self.candidates = candidates\n        else:\n            # Build default candidate structure\n            part = Mock()\n            part.text = text\n            part.thought = False\n            part.function_call = None\n\n            first_candidate = Mock()\n            first_candidate.content = Mock()\n            first_candidate.content.parts = [part]\n\n            for function_call in function_calls or []:\n                part = Mock()\n                part.text = None\n                part.thought = False\n                part.function_call = function_call\n                part.thought_signature = None\n                first_candidate.content.parts.append(part)\n\n            self.candidates = [first_candidate]\n\n    def _create_usage_mock(self, usage_data: dict) -> Mock:\n        usage_mock = Mock()\n        usage_mock.prompt_token_count = usage_data.get(\"prompt_token_count\", 0)\n        usage_mock.total_token_count = usage_data.get(\"total_token_count\", 0)\n        return usage_mock\n\n\nclass GeminiFunctionCallMock:\n    \"\"\"Mock class for Gemini function calls.\"\"\"\n\n    def __init__(self, call_id: str, name: str, args: dict = None):\n        self.id = call_id\n        self.name = name\n        self.args = args or {}\n\n\nclass GeminiPartMock:\n    \"\"\"Mock class for Gemini content parts.\"\"\"\n\n    def __init__(self, text: str = \"\", thought: bool = False):\n        self.text = text\n        self.thought = thought\n        self.function_call = None\n        self.thought_signature = None\n\n\nclass GeminiCandidateMock:\n    \"\"\"Mock class for Gemini candidates.\"\"\"\n\n    def __init__(self, parts: list = None):\n        self.content = Mock()\n        self.content.parts = parts or []\n\n\nclass SampleModel(BaseModel):\n    \"\"\"Sample Pydantic model for testing structured output.\"\"\"\n\n    name: str\n    age: int\n\n\nclass TestGeminiChatModel(IsolatedAsyncioTestCase):\n    \"\"\"Test cases for GeminiChatModel.\"\"\"\n\n    def test_init_default_params(self) -> None:\n        \"\"\"Test initialization with default parameters.\"\"\"\n        with patch(\"google.genai.Client\") as mock_client:\n            model = GeminiChatModel(\n                model_name=\"gemini-2.5-flash\",\n                api_key=\"test_key\",\n            )\n            self.assertEqual(model.model_name, \"gemini-2.5-flash\")\n            self.assertTrue(model.stream)\n            self.assertIsNone(model.thinking_config)\n            self.assertEqual(model.generate_kwargs, {})\n            mock_client.assert_called_once_with(api_key=\"test_key\")\n\n    def test_init_with_custom_params(self) -> None:\n        \"\"\"Test initialization with custom parameters.\"\"\"\n        thinking_config = {\"include_thoughts\": True, \"thinking_budget\": 1024}\n        generate_kwargs = {\"temperature\": 0.7, \"top_p\": 0.9}\n        client_kwargs = {\"timeout\": 30}\n\n        with patch(\"google.genai.Client\") as mock_client:\n            model = GeminiChatModel(\n                model_name=\"gemini-2.5-pro\",\n                api_key=\"test_key\",\n                stream=False,\n                thinking_config=thinking_config,\n                client_kwargs=client_kwargs,\n                generate_kwargs=generate_kwargs,\n            )\n            self.assertEqual(model.model_name, \"gemini-2.5-pro\")\n            self.assertFalse(model.stream)\n            self.assertEqual(model.thinking_config, thinking_config)\n            self.assertEqual(model.generate_kwargs, generate_kwargs)\n            mock_client.assert_called_once_with(api_key=\"test_key\", timeout=30)\n\n    async def test_call_with_regular_model(self) -> None:\n        \"\"\"Test calling a regular model.\"\"\"\n        with patch(\"google.genai.Client\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            model = GeminiChatModel(\n                model_name=\"gemini-2.5-flash\",\n                api_key=\"test_key\",\n                stream=False,\n            )\n            model.client = mock_client\n            messages = [{\"role\": \"user\", \"content\": \"Hello\"}]\n            mock_response = self._create_mock_response(\n                \"Hello! How can I help you?\",\n            )\n            mock_client.aio.models.generate_content = AsyncMock(\n                return_value=mock_response,\n            )\n\n            result = await model(messages)\n            call_args = mock_client.aio.models.generate_content.call_args[1]\n            self.assertEqual(call_args[\"model\"], \"gemini-2.5-flash\")\n            self.assertEqual(call_args[\"contents\"], messages)\n            self.assertIn(\"config\", call_args)\n            self.assertIsInstance(result, ChatResponse)\n            expected_content = [\n                TextBlock(type=\"text\", text=\"Hello! How can I help you?\"),\n            ]\n            self.assertEqual(result.content, expected_content)\n\n    async def test_call_with_tools_integration(self) -> None:\n        \"\"\"Test full integration of tool calls.\"\"\"\n        with patch(\"google.genai.Client\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            model = GeminiChatModel(\n                model_name=\"gemini-2.5-flash\",\n                api_key=\"test_key\",\n                stream=False,\n            )\n            model.client = mock_client\n\n            messages = [{\"role\": \"user\", \"content\": \"What's the weather?\"}]\n            tools = [\n                {\n                    \"type\": \"function\",\n                    \"function\": {\n                        \"name\": \"get_weather\",\n                        \"description\": \"Get weather info\",\n                        \"parameters\": {\"type\": \"object\"},\n                    },\n                },\n            ]\n\n            mock_response = self._create_mock_response_with_tools(\n                \"I'll check the weather for you.\",\n                [\n                    GeminiFunctionCallMock(\n                        call_id=\"call_123\",\n                        name=\"get_weather\",\n                        args={\"location\": \"Beijing\"},\n                    ),\n                ],\n            )\n\n            mock_client.aio.models.generate_content = AsyncMock(\n                return_value=mock_response,\n            )\n            result = await model(messages, tools=tools, tool_choice=\"auto\")\n\n            call_args = mock_client.aio.models.generate_content.call_args[1]\n            self.assertIn(\"tools\", call_args[\"config\"])\n            self.assertIn(\"tool_config\", call_args[\"config\"])\n            expected_tools = [\n                {\n                    \"function_declarations\": [\n                        {\n                            \"name\": \"get_weather\",\n                            \"description\": \"Get weather info\",\n                            \"parameters\": {\"type\": \"object\"},\n                        },\n                    ],\n                },\n            ]\n            self.assertEqual(call_args[\"config\"][\"tools\"], expected_tools)\n            self.assertEqual(\n                call_args[\"config\"][\"tool_config\"],\n                {\n                    \"function_calling_config\": {\"mode\": \"AUTO\"},\n                },\n            )\n            expected_content = [\n                TextBlock(type=\"text\", text=\"I'll check the weather for you.\"),\n                ToolUseBlock(\n                    type=\"tool_use\",\n                    id=\"call_123\",\n                    name=\"get_weather\",\n                    input={\"location\": \"Beijing\"},\n                    raw_input=json.dumps({\"location\": \"Beijing\"}),\n                ),\n            ]\n            self.assertEqual(result.content, expected_content)\n\n    async def test_call_with_thinking_enabled(self) -> None:\n        \"\"\"Test calling with thinking functionality enabled.\"\"\"\n        with patch(\"google.genai.Client\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            thinking_config = {\n                \"include_thoughts\": True,\n                \"thinking_budget\": 1024,\n            }\n            model = GeminiChatModel(\n                model_name=\"gemini-2.5-pro\",\n                api_key=\"test_key\",\n                stream=False,\n                thinking_config=thinking_config,\n            )\n            model.client = mock_client\n\n            messages = [\n                {\"role\": \"user\", \"content\": \"Think about this problem\"},\n            ]\n            thinking_part = GeminiPartMock(\n                text=\"Let me analyze this step by step...\",\n                thought=True,\n            )\n            text_part = GeminiPartMock(\n                text=\"Here's my analysis\",\n                thought=False,\n            )\n            candidate = GeminiCandidateMock(parts=[thinking_part, text_part])\n            mock_response = self._create_mock_response_with_thinking(\n                \"Here's my analysis\",\n                candidates=[candidate],\n            )\n            mock_client.aio.models.generate_content = AsyncMock(\n                return_value=mock_response,\n            )\n            result = await model(messages)\n\n            call_args = mock_client.aio.models.generate_content.call_args[1]\n            self.assertEqual(\n                call_args[\"config\"][\"thinking_config\"],\n                thinking_config,\n            )\n            expected_content = [\n                ThinkingBlock(\n                    type=\"thinking\",\n                    thinking=\"Let me analyze this step by step...\",\n                ),\n                TextBlock(type=\"text\", text=\"Here's my analysis\"),\n            ]\n            self.assertEqual(result.content, expected_content)\n\n    async def test_call_with_structured_model_integration(self) -> None:\n        \"\"\"Test full integration of a structured model.\"\"\"\n        with patch(\"google.genai.Client\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            model = GeminiChatModel(\n                model_name=\"gemini-2.5-flash\",\n                api_key=\"test_key\",\n                stream=False,\n            )\n            model.client = mock_client\n            messages = [{\"role\": \"user\", \"content\": \"Generate a person\"}]\n            mock_response = self._create_mock_response(\n                '{\"name\": \"John\", \"age\": 30}',\n            )\n            mock_client.aio.models.generate_content = AsyncMock(\n                return_value=mock_response,\n            )\n\n            result = await model(messages, structured_model=SampleModel)\n            call_args = mock_client.aio.models.generate_content.call_args[1]\n            self.assertEqual(\n                call_args[\"config\"][\"response_mime_type\"],\n                \"application/json\",\n            )\n            self.assertEqual(\n                call_args[\"config\"][\"response_schema\"],\n                SampleModel,\n            )\n            self.assertNotIn(\"tools\", call_args[\"config\"])\n            self.assertNotIn(\"tool_config\", call_args[\"config\"])\n\n            self.assertIsInstance(result, ChatResponse)\n            self.assertEqual(result.metadata, {\"name\": \"John\", \"age\": 30})\n            expected_content = [\n                TextBlock(type=\"text\", text='{\"name\": \"John\", \"age\": 30}'),\n            ]\n            self.assertEqual(result.content, expected_content)\n\n    async def test_streaming_response_processing(self) -> None:\n        \"\"\"Test processing of streaming response.\"\"\"\n        with patch(\"google.genai.Client\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            model = GeminiChatModel(\n                model_name=\"gemini-2.5-flash\",\n                api_key=\"test_key\",\n                stream=True,\n            )\n            model.client = mock_client\n\n            messages = [{\"role\": \"user\", \"content\": \"Hello\"}]\n            chunks = [\n                self._create_mock_chunk(text=\"Hello\"),\n                self._create_mock_chunk(text=\" there!\"),\n            ]\n\n            mock_client.aio.models.generate_content_stream = AsyncMock(\n                return_value=self._create_async_generator(chunks),\n            )\n            result = await model(messages)\n            responses = []\n            async for response in result:\n                responses.append(response)\n\n            self.assertEqual(len(responses), 2)\n            final_response = responses[-1]\n            expected_content = [\n                TextBlock(type=\"text\", text=\"Hello there!\"),\n            ]\n            self.assertEqual(final_response.content, expected_content)\n\n    async def test_generate_kwargs_integration(self) -> None:\n        \"\"\"Test integration of generate_kwargs.\"\"\"\n        with patch(\"google.genai.Client\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            generate_kwargs = {\"temperature\": 0.7, \"top_p\": 0.9}\n            model = GeminiChatModel(\n                model_name=\"gemini-2.5-flash\",\n                api_key=\"test_key\",\n                stream=False,\n                generate_kwargs=generate_kwargs,\n            )\n            model.client = mock_client\n\n            messages = [{\"role\": \"user\", \"content\": \"Test\"}]\n            mock_response = self._create_mock_response(\"Test response\")\n            mock_client.aio.models.generate_content = AsyncMock(\n                return_value=mock_response,\n            )\n\n            await model(messages, top_k=40)\n\n            call_args = mock_client.aio.models.generate_content.call_args[1]\n            self.assertEqual(call_args[\"config\"][\"temperature\"], 0.7)\n            self.assertEqual(call_args[\"config\"][\"top_p\"], 0.9)\n            self.assertEqual(call_args[\"config\"][\"top_k\"], 40)\n\n    def test_format_tools_with_nested_schema(self) -> None:\n        \"\"\"Test formatting tools with nested JSON schema ($defs and $ref).\"\"\"\n        model = GeminiChatModel(\n            model_name=\"gemini-2.5-flash\",\n            api_key=\"test_key\",\n        )\n\n        nested_schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"person\": {\"$ref\": \"#/$defs/Person\"},\n                \"location\": {\"type\": \"string\"},\n            },\n            \"required\": [\"person\"],\n            \"$defs\": {\n                \"Person\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"name\": {\"type\": \"string\"},\n                        \"age\": {\"type\": \"integer\"},\n                    },\n                    \"required\": [\"name\"],\n                },\n            },\n        }\n\n        tools = [\n            {\n                \"type\": \"function\",\n                \"function\": {\n                    \"name\": \"process_person\",\n                    \"description\": \"Process person info\",\n                    \"parameters\": nested_schema,\n                },\n            },\n        ]\n\n        # pylint: disable=protected-access\n        formatted_tools = model._format_tools_json_schemas(tools)\n\n        # Check if $ref is resolved\n        params = formatted_tools[0][\"function_declarations\"][0][\"parameters\"]\n        expected_params = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"person\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"name\": {\"type\": \"string\"},\n                        \"age\": {\"type\": \"integer\"},\n                    },\n                    \"required\": [\"name\"],\n                },\n                \"location\": {\"type\": \"string\"},\n            },\n            \"required\": [\"person\"],\n        }\n        self.assertEqual(params, expected_params)\n\n    # Auxiliary methods\n    def _create_mock_response(\n        self,\n        text: str = \"\",\n        usage_metadata: dict = None,\n    ) -> GeminiResponseMock:\n        \"\"\"Create a standard mock response.\"\"\"\n        return GeminiResponseMock(\n            text=text,\n            usage_metadata=usage_metadata\n            or {\"prompt_token_count\": 10, \"total_token_count\": 30},\n        )\n\n    def _create_mock_response_with_tools(\n        self,\n        text: str,\n        function_calls: list,\n        usage_metadata: dict = None,\n    ) -> GeminiResponseMock:\n        \"\"\"Create a mock response containing tool calls.\"\"\"\n        return GeminiResponseMock(\n            text=text,\n            function_calls=function_calls,\n            usage_metadata=usage_metadata\n            or {\"prompt_token_count\": 20, \"total_token_count\": 50},\n        )\n\n    def _create_mock_response_with_thinking(\n        self,\n        text: str,\n        candidates: list = None,\n        usage_metadata: dict = None,\n    ) -> GeminiResponseMock:\n        \"\"\"Create a mock response with thinking parts.\"\"\"\n        return GeminiResponseMock(\n            text=text,\n            candidates=candidates or [],\n            usage_metadata=usage_metadata\n            or {\"prompt_token_count\": 15, \"total_token_count\": 35},\n        )\n\n    def _create_mock_chunk(\n        self,\n        text: str = \"\",\n        function_calls: list = None,\n        candidates: list = None,\n        usage_metadata: dict = None,\n    ) -> GeminiResponseMock:\n        \"\"\"Create a mock chunk for streaming responses.\"\"\"\n        return GeminiResponseMock(\n            text=text,\n            function_calls=function_calls or [],\n            candidates=candidates or [],\n            usage_metadata=usage_metadata\n            or {\n                \"prompt_token_count\": 5,\n                \"total_token_count\": 15,\n            },\n        )\n\n    async def _create_async_generator(self, items: list) -> AsyncGenerator:\n        \"\"\"Create an asynchronous generator.\"\"\"\n        for item in items:\n            yield item\n"
  },
  {
    "path": "tests/model_ollama_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unit tests for Ollama API model class.\"\"\"\nimport json\nfrom typing import AsyncGenerator, Any\nfrom unittest.async_case import IsolatedAsyncioTestCase\nfrom unittest.mock import patch, AsyncMock\nfrom pydantic import BaseModel\n\nfrom agentscope.model import OllamaChatModel, ChatResponse\nfrom agentscope.message import TextBlock, ToolUseBlock, ThinkingBlock\n\n\nclass OllamaMessageMock:\n    \"\"\"Mock class for Ollama message objects.\"\"\"\n\n    def __init__(\n        self,\n        content: str = \"\",\n        thinking: str = \"\",\n        tool_calls: list = None,\n    ):\n        self.content = content\n        self.thinking = thinking\n        self.tool_calls = tool_calls or []\n\n\nclass OllamaFunctionMock:\n    \"\"\"Mock class for Ollama function objects.\"\"\"\n\n    def __init__(self, name: str, arguments: dict = None):\n        self.name = name\n        self.arguments = arguments or {}\n\n\nclass OllamaToolCallMock:\n    \"\"\"Mock class for Ollama tool call objects.\"\"\"\n\n    def __init__(\n        self,\n        call_id: str = None,\n        function: OllamaFunctionMock = None,\n    ):\n        self.id = call_id\n        self.function = function\n\n\nclass OllamaResponseMock:\n    \"\"\"Mock class for Ollama response objects.\"\"\"\n\n    def __init__(\n        self,\n        message: OllamaMessageMock = None,\n        done: bool = True,\n        prompt_eval_count: int = 0,\n        eval_count: int = 0,\n    ) -> None:\n        self.message = message or OllamaMessageMock()\n        self.done = done\n        self.prompt_eval_count = prompt_eval_count\n        self.eval_count = eval_count\n\n    def get(self, key: str, default: Any = None) -> Any:\n        \"\"\"Mock dict-like get method.\"\"\"\n        return getattr(self, key, default)\n\n    def __contains__(self, key: str) -> bool:\n        \"\"\"Mock dict-like contains method.\"\"\"\n        return hasattr(self, key)\n\n\nclass SampleModel(BaseModel):\n    \"\"\"Sample Pydantic model for testing structured output.\"\"\"\n\n    name: str\n    age: int\n\n\nclass TestOllamaChatModel(IsolatedAsyncioTestCase):\n    \"\"\"Test cases for OllamaChatModel.\"\"\"\n\n    def test_init_default_params(self) -> None:\n        \"\"\"Test initialization with default parameters.\"\"\"\n        with patch(\"ollama.AsyncClient\") as mock_client:\n            model = OllamaChatModel(model_name=\"llama3.2\")\n            self.assertEqual(model.model_name, \"llama3.2\")\n            self.assertFalse(model.stream)\n            self.assertIsNone(model.options)\n            self.assertEqual(model.keep_alive, \"5m\")\n            self.assertIsNone(model.think)\n            mock_client.assert_called_once_with(host=None)\n\n    def test_init_with_custom_params(self) -> None:\n        \"\"\"Test initialization with custom parameters.\"\"\"\n        options = {\"temperature\": 0.7, \"top_p\": 0.9}\n        with patch(\"ollama.AsyncClient\") as mock_client:\n            model = OllamaChatModel(\n                model_name=\"qwen2.5\",\n                stream=True,\n                options=options,\n                keep_alive=\"10m\",\n                enable_thinking=True,\n                host=\"http://localhost:11434\",\n                timeout=30,\n            )\n            self.assertEqual(model.model_name, \"qwen2.5\")\n            self.assertTrue(model.stream)\n            self.assertEqual(model.options, options)\n            self.assertEqual(model.keep_alive, \"10m\")\n            self.assertTrue(model.think)\n            mock_client.assert_called_once_with(\n                host=\"http://localhost:11434\",\n                timeout=30,\n            )\n\n    async def test_call_with_regular_model(self) -> None:\n        \"\"\"Test calling a regular model.\"\"\"\n        with patch(\"ollama.AsyncClient\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            model = OllamaChatModel(model_name=\"llama3.2\", stream=False)\n            model.client = mock_client\n\n            messages = [{\"role\": \"user\", \"content\": \"Hello\"}]\n            mock_response = self._create_mock_response(\n                \"Hello! How can I help you?\",\n            )\n            mock_client.chat = AsyncMock(return_value=mock_response)\n\n            result = await model(messages)\n            call_args = mock_client.chat.call_args[1]\n            self.assertEqual(call_args[\"model\"], \"llama3.2\")\n            self.assertEqual(call_args[\"messages\"], messages)\n            self.assertFalse(call_args[\"stream\"])\n            self.assertIsInstance(result, ChatResponse)\n            expected_content = [\n                TextBlock(type=\"text\", text=\"Hello! How can I help you?\"),\n            ]\n            self.assertEqual(result.content, expected_content)\n\n    async def test_call_with_tools_integration(self) -> None:\n        \"\"\"Test full integration of tool calls.\"\"\"\n        with patch(\"ollama.AsyncClient\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            model = OllamaChatModel(model_name=\"llama3.2\", stream=False)\n            model.client = mock_client\n\n            messages = [{\"role\": \"user\", \"content\": \"What's the weather?\"}]\n            tools = [\n                {\n                    \"type\": \"function\",\n                    \"function\": {\n                        \"name\": \"get_weather\",\n                        \"description\": \"Get weather info\",\n                        \"parameters\": {\"type\": \"object\"},\n                    },\n                },\n            ]\n\n            function_mock = OllamaFunctionMock(\n                name=\"get_weather\",\n                arguments={\"location\": \"Beijing\"},\n            )\n            tool_call_mock = OllamaToolCallMock(\n                call_id=\"call_123\",\n                function=function_mock,\n            )\n            message_mock = OllamaMessageMock(\n                content=\"I'll check the weather for you.\",\n                tool_calls=[tool_call_mock],\n            )\n            mock_response = self._create_mock_response_with_message(\n                message_mock,\n            )\n\n            mock_client.chat = AsyncMock(return_value=mock_response)\n            result = await model(messages, tools=tools)\n\n            call_args = mock_client.chat.call_args[1]\n            self.assertIn(\"tools\", call_args)\n            self.assertEqual(call_args[\"tools\"], tools)\n            expected_content = [\n                TextBlock(type=\"text\", text=\"I'll check the weather for you.\"),\n                ToolUseBlock(\n                    type=\"tool_use\",\n                    id=\"0_get_weather\",\n                    name=\"get_weather\",\n                    input={\"location\": \"Beijing\"},\n                    raw_input=json.dumps({\"location\": \"Beijing\"}),\n                ),\n            ]\n            self.assertEqual(result.content, expected_content)\n\n    async def test_call_with_thinking_enabled(self) -> None:\n        \"\"\"Test calling with thinking functionality enabled.\"\"\"\n        with patch(\"ollama.AsyncClient\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            model = OllamaChatModel(\n                model_name=\"qwen2.5\",\n                stream=False,\n                enable_thinking=True,\n            )\n            model.client = mock_client\n\n            messages = [\n                {\"role\": \"user\", \"content\": \"Think about this problem\"},\n            ]\n            message_mock = OllamaMessageMock(\n                content=\"Here's my analysis\",\n                thinking=\"Let me analyze this step by step...\",\n            )\n            mock_response = self._create_mock_response_with_message(\n                message_mock,\n            )\n\n            mock_client.chat = AsyncMock(return_value=mock_response)\n            result = await model(messages)\n\n            call_args = mock_client.chat.call_args[1]\n            self.assertTrue(call_args[\"think\"])\n            expected_content = [\n                ThinkingBlock(\n                    type=\"thinking\",\n                    thinking=\"Let me analyze this step by step...\",\n                ),\n                TextBlock(type=\"text\", text=\"Here's my analysis\"),\n            ]\n            self.assertEqual(result.content, expected_content)\n\n    async def test_call_with_structured_model_integration(self) -> None:\n        \"\"\"Test full integration of a structured model.\"\"\"\n        with patch(\"ollama.AsyncClient\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            model = OllamaChatModel(model_name=\"llama3.2\", stream=False)\n            model.client = mock_client\n\n            messages = [{\"role\": \"user\", \"content\": \"Generate a person\"}]\n            mock_response = self._create_mock_response(\n                '{\"name\": \"John\", \"age\": 30}',\n            )\n            mock_client.chat = AsyncMock(return_value=mock_response)\n\n            result = await model(messages, structured_model=SampleModel)\n            call_args = mock_client.chat.call_args[1]\n            self.assertIn(\"format\", call_args)\n            self.assertEqual(\n                call_args[\"format\"],\n                SampleModel.model_json_schema(),\n            )\n            self.assertIsInstance(result, ChatResponse)\n            self.assertEqual(result.metadata, {\"name\": \"John\", \"age\": 30})\n            expected_content = [\n                TextBlock(type=\"text\", text='{\"name\": \"John\", \"age\": 30}'),\n            ]\n            self.assertEqual(result.content, expected_content)\n\n    async def test_streaming_response_processing(self) -> None:\n        \"\"\"Test processing of streaming response.\"\"\"\n        with patch(\"ollama.AsyncClient\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            model = OllamaChatModel(model_name=\"llama3.2\", stream=True)\n            model.client = mock_client\n\n            messages = [{\"role\": \"user\", \"content\": \"Hello\"}]\n            chunks = [\n                self._create_mock_chunk(content=\"Hello\", done=False),\n                self._create_mock_chunk(content=\" there!\", done=True),\n            ]\n\n            mock_client.chat = AsyncMock(\n                return_value=self._create_async_generator(chunks),\n            )\n            result = await model(messages)\n            responses = []\n            async for response in result:\n                responses.append(response)\n\n            self.assertEqual(len(responses), 2)\n            final_response = responses[-1]\n            expected_content = [TextBlock(type=\"text\", text=\"Hello there!\")]\n            self.assertEqual(final_response.content, expected_content)\n\n    async def test_options_integration(self) -> None:\n        \"\"\"Test integration of options parameter.\"\"\"\n        with patch(\"ollama.AsyncClient\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            options = {\"temperature\": 0.7, \"top_p\": 0.9}\n            model = OllamaChatModel(\n                model_name=\"llama3.2\",\n                stream=False,\n                options=options,\n            )\n            model.client = mock_client\n\n            messages = [{\"role\": \"user\", \"content\": \"Test\"}]\n            mock_response = self._create_mock_response(\"Test response\")\n            mock_client.chat = AsyncMock(return_value=mock_response)\n\n            await model(messages, top_k=40)\n\n            call_args = mock_client.chat.call_args[1]\n            self.assertEqual(call_args[\"options\"], options)\n            self.assertEqual(call_args[\"keep_alive\"], \"5m\")\n            self.assertEqual(call_args[\"top_k\"], 40)\n\n    # Auxiliary methods\n    def _create_mock_response(\n        self,\n        content: str = \"\",\n        prompt_eval_count: int = 10,\n        eval_count: int = 20,\n    ) -> OllamaResponseMock:\n        \"\"\"Create a standard mock response.\"\"\"\n        message = OllamaMessageMock(content=content)\n        return OllamaResponseMock(\n            message=message,\n            prompt_eval_count=prompt_eval_count,\n            eval_count=eval_count,\n        )\n\n    def _create_mock_response_with_message(\n        self,\n        message: OllamaMessageMock,\n        prompt_eval_count: int = 10,\n        eval_count: int = 20,\n    ) -> OllamaResponseMock:\n        \"\"\"Create a mock response with specific message.\"\"\"\n        return OllamaResponseMock(\n            message=message,\n            prompt_eval_count=prompt_eval_count,\n            eval_count=eval_count,\n        )\n\n    def _create_mock_chunk(\n        self,\n        content: str = \"\",\n        thinking: str = \"\",\n        tool_calls: list = None,\n        done: bool = True,\n        prompt_eval_count: int = 5,\n        eval_count: int = 10,\n    ) -> OllamaResponseMock:\n        \"\"\"Create a mock chunk for streaming responses.\"\"\"\n        message = OllamaMessageMock(\n            content=content,\n            thinking=thinking,\n            tool_calls=tool_calls or [],\n        )\n        return OllamaResponseMock(\n            message=message,\n            done=done,\n            prompt_eval_count=prompt_eval_count,\n            eval_count=eval_count,\n        )\n\n    async def _create_async_generator(self, items: list) -> AsyncGenerator:\n        \"\"\"Create an asynchronous generator.\"\"\"\n        for item in items:\n            yield item\n"
  },
  {
    "path": "tests/model_openai_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unit tests for OpenAI API model class.\"\"\"\nfrom typing import AsyncGenerator, Any\nfrom unittest.async_case import IsolatedAsyncioTestCase\nfrom unittest.mock import Mock, patch, AsyncMock\nfrom pydantic import BaseModel\n\nfrom agentscope.model import OpenAIChatModel, ChatResponse\nfrom agentscope.message import TextBlock, ToolUseBlock, ThinkingBlock\n\n\nclass SampleModel(BaseModel):\n    \"\"\"Sample Pydantic model for testing structured output.\"\"\"\n\n    name: str\n    age: int\n\n\nclass TestOpenAIChatModel(IsolatedAsyncioTestCase):\n    \"\"\"Test cases for OpenAIChatModel.\"\"\"\n\n    def test_init_default_params(self) -> None:\n        \"\"\"Test initialization with default parameters.\"\"\"\n        with patch(\"openai.AsyncClient\") as mock_client:\n            model = OpenAIChatModel(model_name=\"gpt-4\", api_key=\"test_key\")\n            self.assertEqual(model.model_name, \"gpt-4\")\n            self.assertTrue(model.stream)\n            self.assertIsNone(model.reasoning_effort)\n            self.assertEqual(model.generate_kwargs, {})\n            mock_client.assert_called_once_with(\n                api_key=\"test_key\",\n                organization=None,\n            )\n\n    def test_init_with_custom_params(self) -> None:\n        \"\"\"Test initialization with custom parameters.\"\"\"\n        generate_kwargs = {\"temperature\": 0.7, \"max_tokens\": 1000}\n        client_kwargs = {\"timeout\": 30}\n        with patch(\"openai.AsyncClient\") as mock_client:\n            model = OpenAIChatModel(\n                model_name=\"gpt-4o\",\n                api_key=\"test_key\",\n                stream=False,\n                reasoning_effort=\"high\",\n                organization=\"org-123\",\n                client_kwargs=client_kwargs,\n                generate_kwargs=generate_kwargs,\n            )\n            self.assertEqual(model.model_name, \"gpt-4o\")\n            self.assertFalse(model.stream)\n            self.assertEqual(model.reasoning_effort, \"high\")\n            self.assertEqual(model.generate_kwargs, generate_kwargs)\n            mock_client.assert_called_once_with(\n                api_key=\"test_key\",\n                organization=\"org-123\",\n                timeout=30,\n            )\n\n    async def test_call_with_regular_model(self) -> None:\n        \"\"\"Test calling a regular model.\"\"\"\n        with patch(\"openai.AsyncClient\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            model = OpenAIChatModel(\n                model_name=\"gpt-4\",\n                api_key=\"test_key\",\n                stream=False,\n            )\n            model.client = mock_client\n\n            messages = [{\"role\": \"user\", \"content\": \"Hello\"}]\n            mock_response = self._create_mock_response(\n                \"Hello! How can I help you?\",\n            )\n            mock_client.chat.completions.create = AsyncMock(\n                return_value=mock_response,\n            )\n\n            result = await model(messages)\n            call_args = mock_client.chat.completions.create.call_args[1]\n            self.assertEqual(call_args[\"model\"], \"gpt-4\")\n            self.assertEqual(call_args[\"messages\"], messages)\n            self.assertFalse(call_args[\"stream\"])\n            self.assertIsInstance(result, ChatResponse)\n            expected_content = [\n                TextBlock(type=\"text\", text=\"Hello! How can I help you?\"),\n            ]\n            self.assertEqual(result.content, expected_content)\n\n    async def test_call_with_tools_integration(self) -> None:\n        \"\"\"Test full integration of tool calls.\"\"\"\n        with patch(\"openai.AsyncClient\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            model = OpenAIChatModel(\n                model_name=\"gpt-4\",\n                api_key=\"test_key\",\n                stream=False,\n            )\n            model.client = mock_client\n\n            messages = [{\"role\": \"user\", \"content\": \"What's the weather?\"}]\n            tools = [\n                {\n                    \"type\": \"function\",\n                    \"function\": {\n                        \"name\": \"get_weather\",\n                        \"description\": \"Get weather info\",\n                        \"parameters\": {\"type\": \"object\"},\n                    },\n                },\n            ]\n\n            mock_response = self._create_mock_response_with_tools(\n                \"I'll check the weather for you.\",\n                [\n                    {\n                        \"id\": \"call_123\",\n                        \"name\": \"get_weather\",\n                        \"arguments\": '{\"location\": \"Beijing\"}',\n                    },\n                ],\n            )\n            mock_client.chat.completions.create = AsyncMock(\n                return_value=mock_response,\n            )\n            result = await model(messages, tools=tools, tool_choice=\"auto\")\n            call_args = mock_client.chat.completions.create.call_args[1]\n            self.assertIn(\"tools\", call_args)\n            self.assertEqual(call_args[\"tools\"], tools)\n            self.assertEqual(call_args[\"tool_choice\"], \"auto\")\n            expected_content = [\n                TextBlock(type=\"text\", text=\"I'll check the weather for you.\"),\n                ToolUseBlock(\n                    type=\"tool_use\",\n                    id=\"call_123\",\n                    name=\"get_weather\",\n                    input={\"location\": \"Beijing\"},\n                ),\n            ]\n            self.assertEqual(result.content, expected_content)\n\n    async def test_call_with_reasoning_effort(self) -> None:\n        \"\"\"Test calling with reasoning effort enabled.\"\"\"\n        with patch(\"openai.AsyncClient\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            model = OpenAIChatModel(\n                model_name=\"o3-mini\",\n                api_key=\"test_key\",\n                stream=False,\n                reasoning_effort=\"high\",\n            )\n            model.client = mock_client\n\n            messages = [\n                {\"role\": \"user\", \"content\": \"Think about this problem\"},\n            ]\n            mock_response = self._create_mock_response_with_reasoning(\n                \"Here's my analysis\",\n                \"Let me analyze this step by step...\",\n            )\n            mock_client.chat.completions.create = AsyncMock(\n                return_value=mock_response,\n            )\n            result = await model(messages)\n\n            call_args = mock_client.chat.completions.create.call_args[1]\n            self.assertEqual(call_args[\"reasoning_effort\"], \"high\")\n            expected_content = [\n                ThinkingBlock(\n                    type=\"thinking\",\n                    thinking=\"Let me analyze this step by step...\",\n                ),\n                TextBlock(type=\"text\", text=\"Here's my analysis\"),\n            ]\n            self.assertEqual(result.content, expected_content)\n\n    async def test_call_with_structured_model_integration(self) -> None:\n        \"\"\"Test full integration of a structured model.\"\"\"\n        with patch(\"openai.AsyncClient\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            model = OpenAIChatModel(\n                model_name=\"gpt-4\",\n                api_key=\"test_key\",\n                stream=False,\n            )\n            model.client = mock_client\n\n            messages = [{\"role\": \"user\", \"content\": \"Generate a person\"}]\n            mock_response = self._create_mock_response_with_structured_data(\n                {\"name\": \"John\", \"age\": 30},\n            )\n            mock_client.chat.completions.parse = AsyncMock(\n                return_value=mock_response,\n            )\n\n            result = await model(messages, structured_model=SampleModel)\n            call_args = mock_client.chat.completions.parse.call_args[1]\n            self.assertEqual(call_args[\"response_format\"], SampleModel)\n            self.assertNotIn(\"tools\", call_args)\n            self.assertNotIn(\"tool_choice\", call_args)\n            self.assertNotIn(\"stream\", call_args)\n            self.assertIsInstance(result, ChatResponse)\n            self.assertEqual(result.metadata, {\"name\": \"John\", \"age\": 30})\n\n    async def test_streaming_response_processing(self) -> None:\n        \"\"\"Test processing of streaming response.\"\"\"\n        with patch(\"openai.AsyncClient\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            model = OpenAIChatModel(\n                model_name=\"gpt-4\",\n                api_key=\"test_key\",\n                stream=True,\n            )\n            model.client = mock_client\n\n            messages = [{\"role\": \"user\", \"content\": \"Hello\"}]\n            stream_mock = self._create_stream_mock(\n                [\n                    {\"content\": \"Hello\"},\n                    {\"content\": \" there!\"},\n                ],\n            )\n\n            mock_client.chat.completions.create = AsyncMock(\n                return_value=stream_mock,\n            )\n            result = await model(messages)\n\n            call_args = mock_client.chat.completions.create.call_args[1]\n            self.assertEqual(\n                call_args[\"stream_options\"],\n                {\"include_usage\": True},\n            )\n            responses = []\n            async for response in result:\n                responses.append(response)\n\n            self.assertEqual(len(responses), 2)\n            final_response = responses[-1]\n            expected_content = [TextBlock(type=\"text\", text=\"Hello there!\")]\n            self.assertEqual(final_response.content, expected_content)\n\n    # Auxiliary methods - ensure all Mock objects have complete attributes\n    def _create_mock_response(\n        self,\n        content: str = \"\",\n        prompt_tokens: int = 10,\n        completion_tokens: int = 20,\n    ) -> Mock:\n        \"\"\"Create a standard mock response.\"\"\"\n        message = Mock()\n        message.content = content\n        message.reasoning_content = None\n        message.tool_calls = []\n        message.audio = None\n        message.parsed = None\n\n        choice = Mock()\n        choice.message = message\n\n        response = Mock()\n        response.choices = [choice]\n\n        usage = Mock()\n        usage.prompt_tokens = prompt_tokens\n        usage.completion_tokens = completion_tokens\n        response.usage = usage\n        return response\n\n    def _create_mock_response_with_tools(\n        self,\n        content: str,\n        tool_calls: list,\n    ) -> Mock:\n        \"\"\"Create a mock response with tool calls.\"\"\"\n        response = self._create_mock_response(content)\n        tool_call_mocks = []\n        for tool_call in tool_calls:\n            tc_mock = Mock()\n            tc_mock.id = tool_call[\"id\"]\n            tc_mock.function = Mock()\n            tc_mock.function.name = tool_call[\"name\"]\n            tc_mock.function.arguments = tool_call[\"arguments\"]\n            tool_call_mocks.append(tc_mock)\n        response.choices[0].message.tool_calls = tool_call_mocks\n        return response\n\n    def _create_mock_response_with_reasoning(\n        self,\n        content: str,\n        reasoning_content: str,\n    ) -> Mock:\n        \"\"\"Create a mock response with reasoning content.\"\"\"\n        response = self._create_mock_response(content)\n        response.choices[0].message.reasoning_content = reasoning_content\n        return response\n\n    def _create_mock_response_with_structured_data(self, data: dict) -> Mock:\n        \"\"\"Create a mock response with structured data.\"\"\"\n        message = Mock()\n        message.parsed = Mock()\n        message.parsed.model_dump.return_value = data\n        message.content = None\n        message.reasoning_content = None\n        message.tool_calls = []\n\n        choice = Mock()\n        choice.message = message\n\n        response = Mock()\n        response.choices = [choice]\n        response.usage = None\n\n        return response\n\n    async def test_streaming_response_with_none_delta(self) -> None:\n        \"\"\"Test streaming response when a chunk has delta = None.\"\"\"\n        with patch(\"openai.AsyncClient\") as mock_client_class:\n            mock_client = AsyncMock()\n            mock_client_class.return_value = mock_client\n\n            model = OpenAIChatModel(\n                model_name=\"gpt-4\",\n                api_key=\"test_key\",\n                stream=True,\n            )\n            model.client = mock_client\n\n            messages = [{\"role\": \"user\", \"content\": \"Hello\"}]\n            stream_mock = self._create_stream_mock(\n                [\n                    {\"content\": \"Hello\"},\n                    None,  # simulate missing delta\n                    {\"content\": \" there!\"},\n                ],\n            )\n\n            mock_client.chat.completions.create = AsyncMock(\n                return_value=stream_mock,\n            )\n\n            result = await model(messages)\n\n            responses = []\n            async for response in result:\n                responses.append(response)\n\n            # The None-delta chunk should not break streaming parsing.\n            # We still expect the final aggregated text.\n            final_response = responses[-1]\n            expected_content = [TextBlock(type=\"text\", text=\"Hello there!\")]\n            self.assertEqual(final_response.content, expected_content)\n\n    def _create_stream_mock(self, chunks_data: list) -> Any:\n        \"\"\"Create a mock stream with proper async context management.\"\"\"\n\n        class MockStream:\n            \"\"\"Mock stream class.\"\"\"\n\n            def __init__(self, chunks_data: list) -> None:\n                self.chunks_data = chunks_data\n                self.index = 0\n\n            async def __aenter__(self) -> \"MockStream\":\n                return self\n\n            async def __aexit__(\n                self,\n                exc_type: Any,\n                exc_val: Any,\n                exc_tb: Any,\n            ) -> None:\n                pass\n\n            def __aiter__(self) -> \"MockStream\":\n                return self\n\n            async def __anext__(self) -> AsyncGenerator:\n                if self.index >= len(self.chunks_data):\n                    raise StopAsyncIteration\n                chunk_data = self.chunks_data[self.index]\n                self.index += 1\n\n                choice = Mock()\n\n                if chunk_data is None:\n                    choice.delta = None\n                else:\n                    delta = Mock()\n                    delta.content = chunk_data.get(\"content\")\n                    delta.reasoning_content = chunk_data.get(\n                        \"reasoning_content\",\n                    )\n\n                    audio_mock = Mock()\n                    audio_mock.__contains__ = lambda self, key: False\n                    delta.audio = audio_mock\n                    if \"audio\" in chunk_data:\n                        delta.audio = chunk_data[\"audio\"]\n                    if \"tool_calls\" in chunk_data:\n                        tool_call_mocks = []\n                        for tc_data in chunk_data[\"tool_calls\"]:\n                            tc_mock = Mock()\n                            tc_mock.id = tc_data[\"id\"]\n                            tc_mock.index = 0\n                            tc_mock.function = Mock()\n                            tc_mock.function.name = tc_data[\"name\"]\n                            tc_mock.function.arguments = tc_data[\"arguments\"]\n                            tool_call_mocks.append(tc_mock)\n                        delta.tool_calls = tool_call_mocks\n                    else:\n                        delta.tool_calls = []\n\n                    choice.delta = delta\n\n                chunk = Mock()\n                chunk.choices = [choice]\n                chunk.usage = Mock()\n                chunk.usage.prompt_tokens = 5\n                chunk.usage.completion_tokens = 10\n                return chunk\n\n        return MockStream(chunks_data)\n"
  },
  {
    "path": "tests/pipeline_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unit tests for pipeline classes and functions\"\"\"\nfrom typing import Any\nfrom unittest.async_case import IsolatedAsyncioTestCase\n\nfrom agentscope.message import Msg\nfrom agentscope.pipeline import (\n    SequentialPipeline,\n    FanoutPipeline,\n    sequential_pipeline,\n    fanout_pipeline,\n    stream_printing_messages,\n)\n\nfrom agentscope.agent import AgentBase\n\n\nclass AddAgent(AgentBase):\n    \"\"\"Add agent class.\"\"\"\n\n    def __init__(self, value: int) -> None:\n        \"\"\"Initialize the agent\"\"\"\n        super().__init__()\n        self.name = \"Add\"\n        self.value = value\n\n    async def reply(self, x: Msg | None) -> Msg | None:\n        \"\"\"Reply function\"\"\"\n        if x is None:\n            return None\n        x.metadata[\"result\"] += self.value\n        return x\n\n    async def observe(self, msg: Msg | list[Msg] | None) -> None:\n        \"\"\"Observe function\"\"\"\n\n    async def handle_interrupt(\n        self,\n        *args: Any,\n        **kwargs: Any,\n    ) -> Msg:\n        \"\"\"Handle interrupt\"\"\"\n\n\nclass StreamAgent(AgentBase):\n    \"\"\"Add agent class.\"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"Initialize the agent\"\"\"\n        super().__init__()\n        self.name = \"Stream\"\n\n    async def reply(self) -> Msg | None:\n        \"\"\"Reply function\"\"\"\n        await self.print(\n            Msg(\n                self.name,\n                \"123\",\n                \"user\",\n            ),\n        )\n        await self.print(\n            Msg(\n                \"user\",\n                \"456\",\n                \"user\",\n            ),\n        )\n        await self.print(\n            Msg(\n                self.name,\n                \"789\",\n                \"user\",\n            ),\n        )\n        return None\n\n    async def observe(self, msg: Msg | list[Msg] | None) -> None:\n        \"\"\"Observe function\"\"\"\n\n    async def handle_interrupt(\n        self,\n        *args: Any,\n        **kwargs: Any,\n    ) -> Msg:\n        \"\"\"Handle interrupt\"\"\"\n\n\nclass ErrorAgent(AgentBase):\n    \"\"\"Agent that raises an error during execution.\"\"\"\n\n    def __init__(self, error_msg: str = \"Test error\") -> None:\n        \"\"\"Initialize the agent\"\"\"\n        super().__init__()\n        self.name = \"ErrorAgent\"\n        self.error_msg = error_msg\n\n    async def reply(self) -> Msg | None:\n        \"\"\"Reply function that raises an error\"\"\"\n        msg = Msg(\n            self.name,\n            \"Message before error\",\n            \"user\",\n        )\n        await self.print(msg)\n        # Raise error after printing\n        raise ValueError(self.error_msg)\n\n    async def observe(self, msg: Msg | list[Msg] | None) -> None:\n        \"\"\"Observe function\"\"\"\n\n    async def handle_interrupt(\n        self,\n        *args: Any,\n        **kwargs: Any,\n    ) -> Msg:\n        \"\"\"Handle interrupt\"\"\"\n\n\nclass MultAgent(AgentBase):\n    \"\"\"Mult agent class.\"\"\"\n\n    def __init__(self, value: int) -> None:\n        \"\"\"Initialize the agent\"\"\"\n        super().__init__()\n        self.name = \"Mult\"\n        self.value = value\n\n    async def reply(self, x: Msg | None) -> Msg | None:\n        \"\"\"Reply function\"\"\"\n        if x is None:\n            return None\n        x.metadata[\"result\"] *= self.value\n        return x\n\n    async def observe(self, msg: Msg | list[Msg] | None) -> None:\n        \"\"\"Observe function\"\"\"\n\n    async def handle_interrupt(\n        self,\n        *args: Any,\n        **kwargs: Any,\n    ) -> Msg:\n        \"\"\"Handle interrupt\"\"\"\n\n\nclass PipelineTest(IsolatedAsyncioTestCase):\n    \"\"\"Test cases for Pipelines\"\"\"\n\n    async def test_functional_sequential_pipeline(self) -> None:\n        \"\"\"Test SequentialPipeline executes agents sequentially\"\"\"\n\n        add1 = AddAgent(1)\n        add2 = AddAgent(2)\n        mult3 = MultAgent(3)\n\n        x = Msg(\"user\", \"\", \"user\", metadata={\"result\": 0})\n        res = await sequential_pipeline([add1, add2, mult3], x)\n        self.assertEqual(9, res.metadata[\"result\"])\n\n        x = Msg(\"user\", \"\", \"user\", metadata={\"result\": 0})\n        res = await sequential_pipeline([add1, mult3, add2], x)\n        self.assertEqual(5, res.metadata[\"result\"])\n\n        x = Msg(\"user\", \"\", \"user\", metadata={\"result\": 0})\n        res = await sequential_pipeline([mult3, add1, add2], x)\n        self.assertEqual(3, res.metadata[\"result\"])\n\n    async def test_class_sequential_pipeline(self) -> None:\n        \"\"\"Test SequentialPipeline executes agents sequentially\"\"\"\n\n        add1 = AddAgent(1)\n        add2 = AddAgent(2)\n        mult3 = MultAgent(3)\n\n        x = Msg(\"user\", \"\", \"user\", metadata={\"result\": 0})\n        pipeline = SequentialPipeline([add1, add2, mult3])\n        res = await pipeline(x)\n        self.assertEqual(res.metadata[\"result\"], 9)\n\n        x = Msg(\"user\", \"\", \"user\", metadata={\"result\": 0})\n        pipeline = SequentialPipeline([add1, mult3, add2])\n        res = await pipeline(x)\n        self.assertEqual(res.metadata[\"result\"], 5)\n\n        x = Msg(\"user\", \"\", \"user\", metadata={\"result\": 0})\n        pipeline = SequentialPipeline([mult3, add1, add2])\n        res = await pipeline(x)\n        self.assertEqual(res.metadata[\"result\"], 3)\n\n    async def test_functional_sequential_pipeline_with_none_message(\n        self,\n    ) -> None:\n        \"\"\"Test functional sequential pipeline with None message input\"\"\"\n\n        add1 = AddAgent(1)\n        add2 = AddAgent(2)\n        mult3 = MultAgent(3)\n\n        # Test with None input\n        res = await sequential_pipeline([add1, add2, mult3], None)\n        self.assertIsNone(res)\n        # Test with empty agent list and None input\n        res = await sequential_pipeline([], None)\n        self.assertIsNone(res)\n\n    async def test_class_sequential_pipeline_with_none_message(self) -> None:\n        \"\"\"Test class-based sequential pipeline with None message input\"\"\"\n\n        add1 = AddAgent(1)\n        add2 = AddAgent(2)\n        mult3 = MultAgent(3)\n\n        # Test with None input\n        pipeline = SequentialPipeline([add1, add2, mult3])\n        res = await pipeline(None)\n        self.assertIsNone(res)\n\n        # Test with empty agent list and None input\n        empty_pipeline = SequentialPipeline([])\n        res = await empty_pipeline(None)\n        self.assertIsNone(res)\n\n    async def test_empty_agent_list(self) -> None:\n        \"\"\"Test pipeline with empty agent list\"\"\"\n\n        x = Msg(\"user\", \"\", \"user\", metadata={\"result\": 42})\n\n        # Functional pipeline\n        res = await sequential_pipeline([], x)\n        self.assertEqual(res.metadata[\"result\"], 42)\n        self.assertEqual(res, x)  # Should return the same message object\n\n        # Class-based pipeline\n        pipeline = SequentialPipeline([])\n        res = await pipeline(x)\n        self.assertEqual(res.metadata[\"result\"], 42)\n        self.assertEqual(res, x)  # Should return the same message object\n\n    async def test_single_agent_pipeline(\n        self,\n    ) -> None:\n        \"\"\"Test pipeline with single agent\"\"\"\n\n        add1 = AddAgent(5)\n\n        x = Msg(\"user\", \"\", \"user\", metadata={\"result\": 10})\n\n        # Functional pipeline\n        res = await sequential_pipeline([add1], x)\n        self.assertEqual(res.metadata[\"result\"], 15)\n\n        # Class-based pipeline\n        pipeline = SequentialPipeline([add1])\n        x = Msg(\"user\", \"\", \"user\", metadata={\"result\": 10})\n        res = await pipeline(x)\n        self.assertEqual(res.metadata[\"result\"], 15)\n\n        # Test single agent with None input\n        res = await sequential_pipeline([add1], None)\n        self.assertIsNone(res)\n        res = await pipeline(None)\n        self.assertIsNone(res)\n\n    # ==================== Fanout Pipeline Tests ====================\n\n    async def test_functional_fanout_pipeline_concurrent(self) -> None:\n        \"\"\"Test fanout_pipeline executes agents concurrently with\n        independent inputs\"\"\"\n\n        add1 = AddAgent(1)\n        add2 = AddAgent(2)\n        mult3 = MultAgent(3)\n\n        x = Msg(\"user\", \"\", \"user\", metadata={\"result\": 0})\n        res = await fanout_pipeline([add1, add2, mult3], x, enable_gather=True)\n        # Each agent should process the original input independently\n        self.assertEqual(len(res), 3)\n        self.assertEqual(res[0].metadata[\"result\"], 1)  # 0 + 1\n        self.assertEqual(res[1].metadata[\"result\"], 2)  # 0 + 2\n        self.assertEqual(res[2].metadata[\"result\"], 0)  # 0 * 3\n\n        # Test different order\n        x = Msg(\"user\", \"\", \"user\", metadata={\"result\": 0})\n        res = await fanout_pipeline([mult3, add1, add2], x, enable_gather=True)\n        self.assertEqual(len(res), 3)\n        self.assertEqual(res[0].metadata[\"result\"], 0)  # 0 * 3\n        self.assertEqual(res[1].metadata[\"result\"], 1)  # 0 + 1\n        self.assertEqual(res[2].metadata[\"result\"], 2)  # 0 + 2\n\n    async def test_functional_fanout_pipeline_sequential(self) -> None:\n        \"\"\"Test fanout_pipeline executes agents sequentially with\n        independent inputs\"\"\"\n\n        add1 = AddAgent(1)\n        add2 = AddAgent(2)\n        mult3 = MultAgent(3)\n\n        x = Msg(\"user\", \"\", \"user\", metadata={\"result\": 0})\n        res = await fanout_pipeline(\n            [add1, add2, mult3],\n            x,\n            enable_gather=False,\n        )\n\n        # Each agent should still process the original input independently\n        self.assertEqual(len(res), 3)\n        self.assertEqual(res[0].metadata[\"result\"], 1)  # 0 + 1\n        self.assertEqual(res[1].metadata[\"result\"], 2)  # 0 + 2\n        self.assertEqual(res[2].metadata[\"result\"], 0)  # 0 * 3\n\n    async def test_class_fanout_pipeline_concurrent(self) -> None:\n        \"\"\"Test FanoutPipeline class with concurrent execution\"\"\"\n\n        add1 = AddAgent(1)\n        add2 = AddAgent(2)\n        mult3 = MultAgent(3)\n\n        x = Msg(\"user\", \"\", \"user\", metadata={\"result\": 0})\n        pipeline = FanoutPipeline([add1, add2, mult3], enable_gather=True)\n        res = await pipeline(x)\n        self.assertEqual(len(res), 3)\n        self.assertEqual(res[0].metadata[\"result\"], 1)  # 0 + 1\n        self.assertEqual(res[1].metadata[\"result\"], 2)  # 0 + 2\n        self.assertEqual(res[2].metadata[\"result\"], 0)  # 0 * 3\n\n    async def test_class_fanout_pipeline_sequential(self) -> None:\n        \"\"\"Test FanoutPipeline class with sequential execution\"\"\"\n\n        add1 = AddAgent(1)\n        add2 = AddAgent(2)\n        mult3 = MultAgent(3)\n\n        x = Msg(\"user\", \"\", \"user\", metadata={\"result\": 0})\n        pipeline = FanoutPipeline([add1, add2, mult3], enable_gather=False)\n        res = await pipeline(x)\n\n        self.assertEqual(len(res), 3)\n        self.assertEqual(res[0].metadata[\"result\"], 1)  # 0 + 1\n        self.assertEqual(res[1].metadata[\"result\"], 2)  # 0 + 2\n        self.assertEqual(res[2].metadata[\"result\"], 0)  # 0 * 3\n\n    async def test_fanout_pipeline_empty_agents(self) -> None:\n        \"\"\"Test fanout pipeline with empty agent list\"\"\"\n\n        x = Msg(\"user\", \"\", \"user\", metadata={\"result\": 42})\n\n        # Functional pipeline\n        res = await fanout_pipeline([], x)\n        self.assertEqual(res, [])\n\n        res = await fanout_pipeline([], x, enable_gather=False)\n        self.assertEqual(res, [])\n\n        # Class-based pipeline\n        pipeline = FanoutPipeline([])\n        res = await pipeline(x)\n        self.assertEqual(res, [])\n\n    async def test_fanout_pipeline_with_none_message(self) -> None:\n        \"\"\"Test fanout pipeline with None message input\"\"\"\n\n        add1 = AddAgent(1)\n        add2 = AddAgent(2)\n\n        # Functional pipeline\n        res = await fanout_pipeline([add1, add2], None)\n        self.assertEqual(len(res), 2)\n        self.assertIsNone(res[0])\n        self.assertIsNone(res[1])\n\n        # Class-based pipeline\n        pipeline = FanoutPipeline([add1, add2])\n        res = await pipeline(None)\n        self.assertEqual(len(res), 2)\n        self.assertIsNone(res[0])\n        self.assertIsNone(res[1])\n\n    async def test_stream_printing_messages(self) -> None:\n        \"\"\"Test stream_printing_messages function\"\"\"\n\n        agent = StreamAgent()\n\n        i = 0\n        async for msg, last in stream_printing_messages(\n            [agent],\n            agent(),\n        ):\n            self.assertTrue(last)\n\n            if i == 0:\n                self.assertEqual(\n                    msg.content,\n                    \"123\",\n                )\n\n            elif i == 1:\n                self.assertEqual(\n                    msg.content,\n                    \"456\",\n                )\n\n            elif i == 2:\n                self.assertEqual(\n                    msg.content,\n                    \"789\",\n                )\n\n            i += 1\n\n    async def test_stream_printing_messages_with_error_after_print(\n        self,\n    ) -> None:\n        \"\"\"Test stream_printing_messages function raises exception even\n        after printing some messages\"\"\"\n        error_agent = ErrorAgent(\"Error after printing\")\n\n        messages_received = []\n        exception_raised = False\n\n        try:\n            async for msg, _ in stream_printing_messages(\n                [error_agent],\n                error_agent(),\n            ):\n                messages_received.append(msg)\n        except ValueError as e:\n            exception_raised = True\n            self.assertEqual(str(e), \"Error after printing\")\n\n        # Verify that we received the message before error\n        self.assertEqual(len(messages_received), 1)\n        self.assertEqual(\n            messages_received[0].content,\n            \"Message before error\",\n        )\n        # Verify that exception was raised\n        self.assertTrue(exception_raised)\n"
  },
  {
    "path": "tests/plan_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The plan module related tests.\"\"\"\nimport os\nfrom unittest import IsolatedAsyncioTestCase\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.plan import SubTask, Plan, PlanNotebook\n\n\nclass PlanTest(IsolatedAsyncioTestCase):\n    \"\"\"Test the plan module.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the test case.\"\"\"\n        self.subtask1 = SubTask(\n            name=\"Task 1\",\n            description=\"Description 1\",\n            expected_outcome=\"Expected outcome 1\",\n            state=\"done\",\n        )\n\n        self.subtask2 = SubTask(\n            name=\"Task 2\",\n            description=\"Description 2\",\n            expected_outcome=\"Expected outcome 2\",\n            state=\"in_progress\",\n        )\n\n        self.subtask3 = SubTask(\n            name=\"Task 3\",\n            description=\"Description 3\",\n            expected_outcome=\"Expected outcome 3\",\n            state=\"todo\",\n        )\n\n        self.plan = Plan(\n            name=\"Create website\",\n            description=\"Create a personal portfolio website.\",\n            expected_outcome=\"A new personal portfolio website is created on \"\n            \"GitHub.\",\n            subtasks=[self.subtask1, self.subtask2, self.subtask3],\n        )\n\n    async def test_plan_model(self) -> None:\n        \"\"\"Test the models used in plan module.\"\"\"\n\n        self.assertEqual(\n            self.subtask1.to_markdown(detailed=True),\n            f\"\"\"- [x] Task 1\n\\t- Created At: {self.subtask1.created_at}\n\\t- Description: Description 1\n\\t- Expected Outcome: Expected outcome 1\n\\t- State: done\n\\t- Finished At: None\n\\t- Actual Outcome: None\"\"\",\n        )\n\n        self.assertEqual(\n            self.subtask1.to_markdown(detailed=False),\n            \"\"\"- [x] Task 1\"\"\",\n        )\n\n        self.assertEqual(\n            self.plan.to_markdown(detailed=True),\n            f\"\"\"# Create website\n**Description**: Create a personal portfolio website.\n**Expected Outcome**: A new personal portfolio website is created on GitHub.\n**State**: todo\n**Created At**: {self.plan.created_at}\n## Subtasks\n- [x] Task 1\n\\t- Created At: {self.subtask1.created_at}\n\\t- Description: Description 1\n\\t- Expected Outcome: Expected outcome 1\n\\t- State: done\n\\t- Finished At: None\n\\t- Actual Outcome: None\n- [ ] [WIP]Task 2\n\\t- Created At: {self.subtask2.created_at}\n\\t- Description: Description 2\n\\t- Expected Outcome: Expected outcome 2\n\\t- State: in_progress\n- [ ] Task 3\n\\t- Created At: {self.subtask3.created_at}\n\\t- Description: Description 3\n\\t- Expected Outcome: Expected outcome 3\n\\t- State: todo\"\"\",\n        )\n\n        self.assertEqual(\n            self.plan.to_markdown(detailed=True),\n            f\"\"\"# Create website\n**Description**: Create a personal portfolio website.\n**Expected Outcome**: A new personal portfolio website is created on GitHub.\n**State**: todo\n**Created At**: {self.plan.created_at}\n## Subtasks\n- [x] Task 1\n\\t- Created At: {self.subtask1.created_at}\n\\t- Description: Description 1\n\\t- Expected Outcome: Expected outcome 1\n\\t- State: done\n\\t- Finished At: None\n\\t- Actual Outcome: None\n- [ ] [WIP]Task 2\n\\t- Created At: {self.subtask2.created_at}\n\\t- Description: Description 2\n\\t- Expected Outcome: Expected outcome 2\n\\t- State: in_progress\n- [ ] Task 3\n\\t- Created At: {self.subtask3.created_at}\n\\t- Description: Description 3\n\\t- Expected Outcome: Expected outcome 3\n\\t- State: todo\"\"\",\n        )\n\n    async def test_plan_subtasks(self) -> None:\n        \"\"\"Test the plan and subtask models.\"\"\"\n        plan_notebook = PlanNotebook()\n\n        self.assertListEqual(\n            [_.__name__ for _ in plan_notebook.list_tools()],\n            [\n                \"view_subtasks\",\n                \"update_subtask_state\",\n                \"finish_subtask\",\n                \"create_plan\",\n                \"revise_current_plan\",\n                \"finish_plan\",\n                \"view_historical_plans\",\n                \"recover_historical_plan\",\n            ],\n        )\n\n        plan_hint = await plan_notebook.get_current_hint()\n        self.assertEqual(\n            plan_hint.get_text_content(),\n            \"<system-hint>If the user's query is complex (e.g. \"\n            \"programming a website, game or app), or requires a long chain of \"\n            \"steps to complete (e.g. conduct research on a certain topic from \"\n            \"different sources), you NEED to create a plan first by calling \"\n            \"'create_plan'. Otherwise, you can directly execute the user's \"\n            \"query without planning.</system-hint>\",\n        )\n\n        res = await plan_notebook.create_plan(\n            name=\"Example Plan\",\n            description=\"Example Description\",\n            expected_outcome=\"Example Expected Outcome\",\n            subtasks=[self.subtask1, self.subtask2, self.subtask3],\n        )\n        self.assertEqual(\n            res.content[0][\"text\"],\n            \"Plan 'Example Plan' created successfully.\",\n        )\n\n        res = await plan_notebook.view_subtasks([3])\n        self.assertEqual(\n            res.content[0][\"text\"],\n            \"Invalid subtask_idx '[3]'. Must be between 0 and 2.\",\n        )\n        res = await plan_notebook.view_subtasks([0, 2])\n        self.assertEqual(\n            res.content[0][\"text\"],\n            f\"\"\"Subtask at index 0:\n```\n- [x] Task 1\n\\t- Created At: {self.subtask1.created_at}\n\\t- Description: Description 1\n\\t- Expected Outcome: Expected outcome 1\n\\t- State: done\n\\t- Finished At: None\n\\t- Actual Outcome: None\n```\n\nSubtask at index 2:\n```\n- [ ] Task 3\n\\t- Created At: {self.subtask3.created_at}\n\\t- Description: Description 3\n\\t- Expected Outcome: Expected outcome 3\n\\t- State: todo\n```\n\"\"\",\n        )\n\n        await plan_notebook.revise_current_plan(\n            1,\n            action=\"add\",\n            subtask=SubTask(\n                name=\"Task 11\",\n                description=\"Description 11\",\n                expected_outcome=\"Expected outcome 11\",\n            ),\n        )\n        self.assertEqual(\n            plan_notebook.current_plan.subtasks[1].name,\n            \"Task 11\",\n        )\n        self.assertEqual(\n            len(plan_notebook.current_plan.subtasks),\n            4,\n        )\n\n        res = await plan_notebook.revise_current_plan(\n            1,\n            \"delete\",\n        )\n        self.assertEqual(\n            res.content[0][\"text\"],\n            \"Subtask (named 'Task 11') at index 1 is deleted successfully.\",\n        )\n        self.assertEqual(\n            len(plan_notebook.current_plan.subtasks),\n            3,\n        )\n\n        res = await plan_notebook.revise_current_plan(\n            1,\n            \"revise\",\n            subtask=SubTask(\n                name=\"Task 22\",\n                description=\"Description 22\",\n                expected_outcome=\"Expected outcome 22\",\n            ),\n        )\n        self.assertEqual(\n            res.content[0][\"text\"],\n            \"Subtask at index 1 is revised successfully.\",\n        )\n        self.assertEqual(\n            plan_notebook.current_plan.subtasks[1].name,\n            \"Task 22\",\n        )\n        self.assertEqual(\n            len(plan_notebook.current_plan.subtasks),\n            3,\n        )\n\n        res = await plan_notebook.update_subtask_state(\n            2,\n            \"in_progress\",\n        )\n        self.assertEqual(\n            res.content[0][\"text\"],\n            \"Subtask (at index 1) named 'Task 22' is not done yet. \"\n            \"You should finish the previous subtasks first.\",\n        )\n\n        await plan_notebook.update_subtask_state(0, \"in_progress\")\n        res = await plan_notebook.update_subtask_state(\n            1,\n            \"in_progress\",\n        )\n        self.assertEqual(\n            res.content[0][\"text\"],\n            \"Subtask (at index 0) named 'Task 1' is not done yet. You \"\n            \"should finish the previous subtasks first.\",\n        )\n\n        res = await plan_notebook.finish_subtask(\n            0,\n            \"Fake outcome for task 1\",\n        )\n        self.assertEqual(\n            res.content[0][\"text\"],\n            \"Subtask (at index 0) named 'Task 1' is marked as done \"\n            \"successfully. The next subtask named 'Task 22' is activated.\",\n        )\n        self.assertEqual(\n            plan_notebook.current_plan.subtasks[1].state,\n            \"in_progress\",\n        )\n\n    async def test_serialization(self) -> None:\n        \"\"\"Test the serialization and deserialization of plan and subtask.\"\"\"\n        plan_notebook = PlanNotebook()\n        agent = ReActAgent(\n            name=\"Friday\",\n            sys_prompt=\"You are a helpful assistant named Friday. \",\n            model=DashScopeChatModel(\n                model_name=\"qwen-max\",\n                api_key=os.environ.get(\"DASH_API_KEY\"),\n            ),\n            formatter=DashScopeChatFormatter(),\n            plan_notebook=plan_notebook,\n        )\n\n        await plan_notebook.create_plan(\n            name=\"text\",\n            description=\"abc\",\n            expected_outcome=\"edf\",\n            subtasks=[\n                SubTask(\n                    name=\"1\",\n                    description=\"1\",\n                    expected_outcome=\"1\",\n                ),\n                SubTask(\n                    name=\"2\",\n                    description=\"2\",\n                    expected_outcome=\"2\",\n                ),\n            ],\n        )\n\n        self.assertIsNotNone(plan_notebook.current_plan)\n\n        # Check the exported state\n        state = agent.state_dict()\n        subtasks = plan_notebook.current_plan.subtasks\n\n        self.assertDictEqual(\n            state,\n            {\n                \"memory\": {\"_compressed_summary\": \"\", \"content\": []},\n                \"toolkit\": {\"active_groups\": []},\n                \"plan_notebook\": {\n                    \"storage\": {\n                        \"plans\": {},\n                    },\n                    \"current_plan\": {\n                        \"id\": plan_notebook.current_plan.id,\n                        \"name\": \"text\",\n                        \"description\": \"abc\",\n                        \"expected_outcome\": \"edf\",\n                        \"subtasks\": [\n                            {\n                                \"name\": \"1\",\n                                \"description\": \"1\",\n                                \"expected_outcome\": \"1\",\n                                \"outcome\": None,\n                                \"state\": \"todo\",\n                                \"created_at\": subtasks[0].created_at,\n                                \"finished_at\": None,\n                            },\n                            {\n                                \"name\": \"2\",\n                                \"description\": \"2\",\n                                \"expected_outcome\": \"2\",\n                                \"outcome\": None,\n                                \"state\": \"todo\",\n                                \"created_at\": subtasks[1].created_at,\n                                \"finished_at\": None,\n                            },\n                        ],\n                        \"created_at\": plan_notebook.current_plan.created_at,\n                        \"state\": \"todo\",\n                        \"finished_at\": None,\n                        \"outcome\": None,\n                    },\n                },\n                \"name\": \"Friday\",\n                \"_sys_prompt\": \"You are a helpful assistant named Friday. \",\n            },\n        )\n\n        # Test finish the plan serialization\n        res = await plan_notebook.update_subtask_state(\n            0,\n            \"in_progress\",\n        )\n        self.assertEqual(\n            res.content[0][\"text\"],\n            \"Subtask at index 0, named '1' is marked as 'in_progress' \"\n            \"successfully. The plan state has been updated to 'in_progress'.\",\n        )\n\n        self.assertEqual(\n            plan_notebook.state_dict(),\n            {\n                \"storage\": {\n                    \"plans\": {},\n                },\n                \"current_plan\": {\n                    \"id\": plan_notebook.current_plan.id,\n                    \"name\": \"text\",\n                    \"description\": \"abc\",\n                    \"expected_outcome\": \"edf\",\n                    \"subtasks\": [\n                        {\n                            \"name\": \"1\",\n                            \"description\": \"1\",\n                            \"expected_outcome\": \"1\",\n                            \"state\": \"in_progress\",\n                            \"created_at\": subtasks[0].created_at,\n                            \"outcome\": None,\n                            \"finished_at\": None,\n                        },\n                        {\n                            \"name\": \"2\",\n                            \"description\": \"2\",\n                            \"expected_outcome\": \"2\",\n                            \"state\": \"todo\",\n                            \"created_at\": subtasks[1].created_at,\n                            \"outcome\": None,\n                            \"finished_at\": None,\n                        },\n                    ],\n                    \"state\": \"in_progress\",\n                    \"created_at\": plan_notebook.current_plan.created_at,\n                    \"finished_at\": None,\n                    \"outcome\": None,\n                },\n            },\n        )\n\n        # When finish a subtask\n        await plan_notebook.finish_subtask(\n            0,\n            subtask_outcome=\"abc\",\n        )\n        self.assertDictEqual(\n            plan_notebook.state_dict(),\n            {\n                \"storage\": {\n                    \"plans\": {},\n                },\n                \"current_plan\": {\n                    \"id\": plan_notebook.current_plan.id,\n                    \"name\": \"text\",\n                    \"description\": \"abc\",\n                    \"expected_outcome\": \"edf\",\n                    \"created_at\": plan_notebook.current_plan.created_at,\n                    \"subtasks\": [\n                        {\n                            \"name\": \"1\",\n                            \"description\": \"1\",\n                            \"expected_outcome\": \"1\",\n                            \"state\": \"done\",\n                            \"created_at\": subtasks[0].created_at,\n                            \"finished_at\": plan_notebook.current_plan.subtasks[\n                                0\n                            ].finished_at,\n                            \"outcome\": \"abc\",\n                        },\n                        {\n                            \"name\": \"2\",\n                            \"description\": \"2\",\n                            \"expected_outcome\": \"2\",\n                            \"state\": \"in_progress\",\n                            \"created_at\": subtasks[1].created_at,\n                            \"finished_at\": None,\n                            \"outcome\": None,\n                        },\n                    ],\n                    \"finished_at\": None,\n                    \"outcome\": None,\n                    \"state\": \"in_progress\",\n                },\n            },\n        )\n\n        # Test deserialization\n        await plan_notebook.finish_subtask(1, \"def\")\n        self.assertDictEqual(\n            plan_notebook.state_dict(),\n            {\n                \"storage\": {\n                    \"plans\": {},\n                },\n                \"current_plan\": {\n                    \"id\": plan_notebook.current_plan.id,\n                    \"name\": \"text\",\n                    \"description\": \"abc\",\n                    \"expected_outcome\": \"edf\",\n                    \"created_at\": plan_notebook.current_plan.created_at,\n                    \"subtasks\": [\n                        {\n                            \"name\": \"1\",\n                            \"description\": \"1\",\n                            \"expected_outcome\": \"1\",\n                            \"state\": \"done\",\n                            \"created_at\": subtasks[0].created_at,\n                            \"finished_at\": plan_notebook.current_plan.subtasks[\n                                0\n                            ].finished_at,\n                            \"outcome\": \"abc\",\n                        },\n                        {\n                            \"name\": \"2\",\n                            \"description\": \"2\",\n                            \"expected_outcome\": \"2\",\n                            \"state\": \"done\",\n                            \"created_at\": subtasks[1].created_at,\n                            \"finished_at\": plan_notebook.current_plan.subtasks[\n                                1\n                            ].finished_at,\n                            \"outcome\": \"def\",\n                        },\n                    ],\n                    \"finished_at\": None,\n                    \"outcome\": None,\n                    \"state\": \"in_progress\",\n                },\n            },\n        )\n\n        # Finish the plan\n        await plan_notebook.finish_plan(\"done\", \"Overall outcome\")\n        self.assertIsNone(\n            plan_notebook.current_plan,\n        )\n\n        # Check the finished plan\n        plan = (await plan_notebook.storage.get_plans())[0]\n\n        self.assertDictEqual(\n            plan_notebook.state_dict(),\n            {\n                \"storage\": {\n                    \"plans\": {\n                        plan.id: {\n                            \"id\": plan.id,\n                            \"name\": \"text\",\n                            \"description\": \"abc\",\n                            \"expected_outcome\": \"edf\",\n                            \"subtasks\": [\n                                {\n                                    \"name\": \"1\",\n                                    \"description\": \"1\",\n                                    \"expected_outcome\": \"1\",\n                                    \"outcome\": \"abc\",\n                                    \"state\": \"done\",\n                                    \"created_at\": plan.subtasks[0].created_at,\n                                    \"finished_at\": plan.subtasks[\n                                        0\n                                    ].finished_at,\n                                },\n                                {\n                                    \"name\": \"2\",\n                                    \"description\": \"2\",\n                                    \"expected_outcome\": \"2\",\n                                    \"outcome\": \"def\",\n                                    \"state\": \"done\",\n                                    \"created_at\": plan.subtasks[1].created_at,\n                                    \"finished_at\": plan.subtasks[\n                                        1\n                                    ].finished_at,\n                                },\n                            ],\n                            \"created_at\": plan.created_at,\n                            \"state\": \"done\",\n                            \"finished_at\": plan.finished_at,\n                            \"outcome\": \"Overall outcome\",\n                        },\n                    },\n                },\n                \"current_plan\": None,\n            },\n        )\n\n        # Load the state\n        new_plan_notebook = PlanNotebook()\n        new_plan_notebook.load_state_dict(\n            plan_notebook.state_dict(),\n        )\n        self.assertDictEqual(\n            new_plan_notebook.state_dict(),\n            {\n                \"storage\": {\n                    \"plans\": {\n                        plan.id: {\n                            \"id\": plan.id,\n                            \"name\": \"text\",\n                            \"description\": \"abc\",\n                            \"expected_outcome\": \"edf\",\n                            \"subtasks\": [\n                                {\n                                    \"name\": \"1\",\n                                    \"description\": \"1\",\n                                    \"expected_outcome\": \"1\",\n                                    \"outcome\": \"abc\",\n                                    \"state\": \"done\",\n                                    \"created_at\": plan.subtasks[0].created_at,\n                                    \"finished_at\": plan.subtasks[\n                                        0\n                                    ].finished_at,\n                                },\n                                {\n                                    \"name\": \"2\",\n                                    \"description\": \"2\",\n                                    \"expected_outcome\": \"2\",\n                                    \"outcome\": \"def\",\n                                    \"state\": \"done\",\n                                    \"created_at\": plan.subtasks[1].created_at,\n                                    \"finished_at\": plan.subtasks[\n                                        1\n                                    ].finished_at,\n                                },\n                            ],\n                            \"created_at\": plan.created_at,\n                            \"state\": \"done\",\n                            \"finished_at\": plan.finished_at,\n                            \"outcome\": \"Overall outcome\",\n                        },\n                    },\n                },\n                \"current_plan\": None,\n            },\n        )\n\n        plan_notebook.current_plan = None\n        self.assertIsNone(\n            agent.plan_notebook.current_plan,\n        )\n        agent.load_state_dict(state)\n        self.assertIsNotNone(\n            agent.plan_notebook.current_plan,\n        )\n\n    async def test_hint_generator_all_states(self) -> None:\n        \"\"\"Test DefaultPlanToHint covers all plan states.\"\"\"\n        from agentscope.plan._plan_notebook import DefaultPlanToHint\n\n        hint_gen = DefaultPlanToHint()\n\n        # State 1: No plan\n        hint = hint_gen(None)\n        assert hint is not None\n        self.assertIn(\"create_plan\", hint)\n\n        # State 2: All todo\n        plan = Plan(\n            name=\"Test\",\n            description=\"desc\",\n            expected_outcome=\"outcome\",\n            subtasks=[\n                SubTask(name=\"t1\", description=\"d\", expected_outcome=\"e\"),\n            ],\n        )\n        hint = hint_gen(plan)\n        assert hint is not None\n        self.assertIn(\"subtask_idx=0\", hint)\n\n        # State 3: In progress\n        plan.subtasks[0].state = \"in_progress\"\n        hint = hint_gen(plan)\n        assert hint is not None\n        self.assertIn(\"finish_subtask\", hint)\n\n        # State 4: Some done, none in progress\n        plan.subtasks[0].state = \"done\"\n        plan.subtasks.append(\n            SubTask(name=\"t2\", description=\"d\", expected_outcome=\"e\"),\n        )\n        hint = hint_gen(plan)\n        assert hint is not None\n        self.assertIn(\"first 1\", hint.lower())\n\n        # State 5: All done\n        plan.subtasks[1].state = \"done\"\n        hint = hint_gen(plan)\n        assert hint is not None\n        self.assertIn(\"finish_plan\", hint)\n\n        # State 6: Mix done and abandoned\n        plan.subtasks[1].state = \"abandoned\"\n        hint = hint_gen(plan)\n        assert hint is not None\n        self.assertIn(\"finish_plan\", hint)\n\n    async def test_error_paths(self) -> None:\n        \"\"\"Test error handling paths to improve coverage.\"\"\"\n        notebook = PlanNotebook()\n\n        # Test operations without plan\n        with self.assertRaises(ValueError):\n            await notebook.revise_current_plan(0, \"delete\")\n\n        # Create plan for subsequent tests\n        await notebook.create_plan(\n            name=\"Test\",\n            description=\"d\",\n            expected_outcome=\"e\",\n            subtasks=[\n                SubTask(name=\"t1\", description=\"d\", expected_outcome=\"e\"),\n                SubTask(name=\"t2\", description=\"d\", expected_outcome=\"e\"),\n            ],\n        )\n\n        # Invalid types and values\n        # type: ignore\n        res = await notebook.revise_current_plan(\"invalid\", \"delete\")\n        self.assertIn(\"Invalid type\", res.content[0].get(\"text\", \"\"))\n\n        # type: ignore\n        res = await notebook.revise_current_plan(0, \"bad_action\")\n        self.assertIn(\"Invalid action\", res.content[0].get(\"text\", \"\"))\n\n        res = await notebook.revise_current_plan(0, \"add\", None)\n        self.assertIn(\"must be provided\", res.content[0].get(\"text\", \"\"))\n\n        res = await notebook.revise_current_plan(999, \"delete\")\n        self.assertIn(\"Invalid subtask_idx\", res.content[0].get(\"text\", \"\"))\n\n        res = await notebook.update_subtask_state(999, \"in_progress\")\n        self.assertIn(\"Invalid subtask_idx\", res.content[0].get(\"text\", \"\"))\n\n        # type: ignore\n        res = await notebook.update_subtask_state(0, \"bad_state\")\n        self.assertIn(\"Invalid state\", res.content[0].get(\"text\", \"\"))\n\n        # State constraints\n        res = await notebook.update_subtask_state(1, \"in_progress\")\n        self.assertIn(\"not done yet\", res.content[0].get(\"text\", \"\"))\n\n        await notebook.update_subtask_state(0, \"in_progress\")\n        res = await notebook.update_subtask_state(1, \"in_progress\")\n        self.assertIn(\"not done yet\", res.content[0].get(\"text\", \"\"))\n\n        res = await notebook.finish_subtask(1, \"outcome\")\n        self.assertIn(\"not done yet\", res.content[0].get(\"text\", \"\"))\n\n    async def test_edge_cases(self) -> None:\n        \"\"\"Test edge cases and special scenarios.\"\"\"\n        notebook = PlanNotebook()\n\n        # Finish plan when no plan exists\n        res = await notebook.finish_plan(\"done\", \"outcome\")\n        self.assertIn(\"no plan\", res.content[0].get(\"text\", \"\").lower())\n\n        # Create and replace plan\n        await notebook.create_plan(\n            \"P1\",\n            \"d\",\n            \"e\",\n            [SubTask(name=\"t\", description=\"d\", expected_outcome=\"e\")],\n        )\n        res = await notebook.create_plan(\n            \"P2\",\n            \"d\",\n            \"e\",\n            [SubTask(name=\"t\", description=\"d\", expected_outcome=\"e\")],\n        )\n        self.assertIn(\"replaced\", res.content[0].get(\"text\", \"\").lower())\n\n        # Auto-activate next subtask\n        assert notebook.current_plan is not None\n        notebook.current_plan.subtasks.append(\n            SubTask(name=\"t2\", description=\"d\", expected_outcome=\"e\"),\n        )\n        await notebook.update_subtask_state(0, \"in_progress\")\n        res = await notebook.finish_subtask(0, \"done\")\n        self.assertIn(\"next subtask\", res.content[0].get(\"text\", \"\").lower())\n        assert notebook.current_plan is not None\n        self.assertEqual(\n            notebook.current_plan.subtasks[1].state,\n            \"in_progress\",\n        )\n\n        # Historical plans\n        await notebook.finish_subtask(1, \"done\")\n        await notebook.finish_plan(\"done\", \"final\")\n        res = await notebook.view_historical_plans()\n        self.assertIn(\"P2\", res.content[0].get(\"text\", \"\"))\n\n        plans = await notebook.storage.get_plans()\n        res = await notebook.recover_historical_plan(plans[0].id)\n        self.assertIn(\"recovered\", res.content[0].get(\"text\", \"\").lower())\n\n        res = await notebook.recover_historical_plan(\"bad_id\")\n        self.assertIn(\"cannot find\", res.content[0].get(\"text\", \"\").lower())\n\n        # Hooks\n        called = []\n\n        async def hook(_nb: PlanNotebook, _p: Plan | None) -> None:\n            called.append(True)\n\n        notebook.register_plan_change_hook(\"test\", hook)\n        await notebook.create_plan(\n            \"P3\",\n            \"d\",\n            \"e\",\n            [SubTask(name=\"t\", description=\"d\", expected_outcome=\"e\")],\n        )\n        self.assertTrue(called)\n\n        notebook.remove_plan_change_hook(\"test\")\n        with self.assertRaises(ValueError):\n            notebook.remove_plan_change_hook(\"bad_hook\")\n\n    async def test_recover_historical_plan_triggers_hook(self) -> None:\n        \"\"\"Test recovering a historical plan triggers plan change hooks.\"\"\"\n        notebook = PlanNotebook()\n        hook_calls: list[str | None] = []\n\n        def hook(_nb: PlanNotebook, plan: Plan | None) -> None:\n            hook_calls.append(plan.name if plan else None)\n\n        notebook.register_plan_change_hook(\"recover_hook\", hook)\n\n        await notebook.create_plan(\n            \"P1\",\n            \"desc\",\n            \"outcome\",\n            [SubTask(name=\"t1\", description=\"d\", expected_outcome=\"e\")],\n        )\n        await notebook.finish_plan(\"done\", \"final\")\n\n        self.assertEqual(\n            len(hook_calls),\n            2,\n        )\n        self.assertEqual(\n            hook_calls,\n            [\"P1\", None],\n        )\n\n        historical_plan = (await notebook.storage.get_plans())[0]\n        await notebook.recover_historical_plan(historical_plan.id)\n\n        self.assertEqual(\n            len(hook_calls),\n            3,\n        )\n        self.assertEqual(\n            hook_calls[-1],\n            \"P1\",\n        )\n"
  },
  {
    "path": "tests/rag_knowledge_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Test the RAG knowledge implementations.\"\"\"\nfrom typing import Any\nfrom unittest.async_case import IsolatedAsyncioTestCase\n\nfrom agentscope.embedding import (\n    EmbeddingModelBase,\n    EmbeddingResponse,\n)\nfrom agentscope.message import TextBlock\nfrom agentscope.rag import (\n    SimpleKnowledge,\n    QdrantStore,\n    Document,\n    DocMetadata,\n)\n\n\nclass TestTextEmbedding(EmbeddingModelBase):\n    \"\"\"A mock text embedding model for testing.\"\"\"\n\n    supported_modalities: list[str] = [\"text\"]\n    \"\"\"This class only supports text input.\"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"The constructor for the mock text embedding model.\"\"\"\n        super().__init__(model_name=\"mock-model\", dimensions=3)\n\n    async def __call__(\n        self,\n        text: list[TextBlock | str],\n        **kwargs: Any,\n    ) -> EmbeddingResponse:\n        \"\"\"Return a fixed embedding for testing.\"\"\"\n        embeddings = []\n        for t in text:\n            if isinstance(t, dict):\n                t = t.get(\"text\")\n            if t == \"This is an apple\":\n                embeddings.append([0.1, 0.2, 0.3])\n            elif t == \"This is a banana\":\n                embeddings.append([0.9, 0.1, 0.4])\n            elif t == \"apple\":\n                embeddings.append([0.15, 0.25, 0.35])\n\n        return EmbeddingResponse(\n            embeddings=embeddings,\n        )\n\n\nclass RAGKnowledgeTest(IsolatedAsyncioTestCase):\n    \"\"\"Test cases for RAG knowledge implementations.\"\"\"\n\n    async def test_simple_knowledge(self) -> None:\n        \"\"\"Test the SimpleKnowledge implementation.\"\"\"\n\n        knowledge = SimpleKnowledge(\n            embedding_model=TestTextEmbedding(),\n            embedding_store=QdrantStore(\n                location=\":memory:\",\n                collection_name=\"test\",\n                dimensions=3,\n            ),\n        )\n\n        await knowledge.add_documents(\n            [\n                Document(\n                    embedding=[0.1, 0.2, 0.3],\n                    metadata=DocMetadata(\n                        content=TextBlock(\n                            type=\"text\",\n                            text=\"This is an apple.\",\n                        ),\n                        doc_id=\"doc1\",\n                        chunk_id=1,\n                        total_chunks=2,\n                    ),\n                ),\n                Document(\n                    embedding=[0.9, 0.1, 0.4],\n                    metadata=DocMetadata(\n                        content=TextBlock(\n                            type=\"text\",\n                            text=\"This is a banana.\",\n                        ),\n                        doc_id=\"doc1\",\n                        chunk_id=2,\n                        total_chunks=2,\n                    ),\n                ),\n            ],\n        )\n\n        res = await knowledge.retrieve(\n            query=\"apple\",\n            limit=3,\n            score_threshold=0.7,\n        )\n\n        self.assertEqual(len(res), 1)\n        self.assertEqual(\n            res[0].metadata.content[\"text\"],\n            \"This is an apple.\",\n        )\n        self.assertEqual(\n            res[0].score // 0.0001,\n            9974,\n        )\n"
  },
  {
    "path": "tests/rag_reader_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Test the RAG reader implementations.\"\"\"\nimport os\nimport json\nfrom unittest.async_case import IsolatedAsyncioTestCase\n\nfrom agentscope.rag import (\n    TextReader,\n    PDFReader,\n    WordReader,\n    ExcelReader,\n    PowerPointReader,\n)\n\n\nclass RAGReaderText(IsolatedAsyncioTestCase):\n    \"\"\"Test cases for RAG reader implementations.\"\"\"\n\n    async def test_text_reader(self) -> None:\n        \"\"\"Test the TextReader implementation.\"\"\"\n        # Split by char\n        reader = TextReader(\n            chunk_size=10,\n            split_by=\"char\",\n        )\n        docs = await reader(\n            text=\"\".join(str(i) for i in range(22)),\n        )\n        self.assertEqual(len(docs), 4)\n        self.assertEqual(\n            docs[0].metadata.content[\"text\"],\n            \"0123456789\",\n        )\n        self.assertEqual(\n            docs[1].metadata.content[\"text\"],\n            \"1011121314\",\n        )\n        self.assertEqual(\n            docs[2].metadata.content[\"text\"],\n            \"1516171819\",\n        )\n        self.assertEqual(\n            docs[3].metadata.content[\"text\"],\n            \"2021\",\n        )\n\n        # Split by sentence\n        reader = TextReader(\n            chunk_size=10,\n            split_by=\"sentence\",\n        )\n        docs = await reader(\n            text=\"012345678910111213. 141516171819! 2021? 22\",\n        )\n        self.assertEqual(\n            [_.metadata.content[\"text\"] for _ in docs],\n            [\"0123456789\", \"10111213.\", \"1415161718\", \"19!\", \"2021?\", \"22\"],\n        )\n\n        docs = await reader(\n            text=\"01234. 56789! 10111213? 14151617..\",\n        )\n        self.assertEqual(\n            [_.metadata.content[\"text\"] for _ in docs],\n            [\"01234.\", \"56789!\", \"10111213?\", \"14151617..\"],\n        )\n\n        # Split by paragraph\n        reader = TextReader(\n            chunk_size=5,\n            split_by=\"paragraph\",\n        )\n        docs = await reader(\n            text=\"01234\\n\\n5678910111213.\\n\\n\\n1415\",\n        )\n        self.assertEqual(\n            [_.metadata.content[\"text\"] for _ in docs],\n            [\"01234\", \"56789\", \"10111\", \"213.\", \"1415\"],\n        )\n\n    async def test_pdf_reader(self) -> None:\n        \"\"\"Test the PDFReader implementation.\"\"\"\n        reader = PDFReader(\n            chunk_size=200,\n            split_by=\"sentence\",\n        )\n        pdf_path = os.path.join(\n            os.path.abspath(os.path.dirname(__file__)),\n            \"../examples/functionality/rag/example.pdf\",\n        )\n        docs = await reader(pdf_path=pdf_path)\n        self.assertEqual(len(docs), 17)\n        self.assertEqual(\n            [_.metadata.content[\"text\"] for _ in docs][:2],\n            [\n                \"1\\nThe Great Transformations: From Print to Space\\n\"\n                \"The invention of the printing press in the 15th century \"\n                \"marked a revolutionary change in \\nhuman history.\",\n                \"Johannes Gutenberg's innovation democratized knowledge and \"\n                \"made books \\naccessible to the common people.\",\n            ],\n        )\n\n    async def test_word_reader_with_images_and_tables(self) -> None:\n        \"\"\"Test the WordReader implementation with images and table\n        separation.\"\"\"\n        # Test with images and table separation enabled\n        reader = WordReader(\n            chunk_size=200,\n            split_by=\"sentence\",\n            include_image=True,\n            separate_table=True,\n        )\n        word_path = os.path.join(\n            os.path.abspath(os.path.dirname(__file__)),\n            \"../tests/test.docx\",\n        )\n        docs = await reader(word_path=word_path)\n\n        self.assertListEqual(\n            [_.metadata.content[\"type\"] for _ in docs],\n            [\"text\"] * 4 + [\"image\"] * 2 + [\"text\", \"image\", \"text\", \"text\"],\n        )\n\n        self.assertEqual(\n            [_.metadata.content.get(\"text\") for _ in docs],\n            [\n                \"AgentScope\\n\"\n                \"标题2\\n\"\n                \"This is a test file for AgentScope word reader.\",\n                \"标题3\\nTest table:\",\n                \"| Header1 | Header2 | Header3 | Header4 |\\n\"\n                \"| --- | --- | --- | --- |\\n\"\n                \"| 1 | 2 | 3 | 4 |\\n\"\n                \"| 5 | 6 | 7 | 8 |\",\n                \"\\nTest list:\\nAlice\\nBob\\nCharlie\\nDavid\\nTest image:\",\n                None,  # image\n                None,  # image\n                \"\\nText between images\",\n                None,  # image\n                \"\\nText between image and table\",\n                \"| a | b | c |\\n| --- | --- | --- |\\n| d\\ne | f | g |\",\n            ],\n        )\n\n        self.assertEqual(\n            [\n                _.metadata.content[\"source\"][\"media_type\"]\n                for _ in docs\n                if _.metadata.content[\"type\"] == \"image\"\n            ],\n            [\"image/png\", \"image/png\", \"image/png\"],\n        )\n\n    async def test_excel_reader_with_images_and_tables(self) -> None:\n        \"\"\"Test the ExcelReader implementation with images and table\n        separation.\"\"\"\n        # Test with images and table separation enabled\n        reader = ExcelReader(\n            chunk_size=200,\n            split_by=\"sentence\",\n            include_image=True,\n            separate_table=True,\n            include_cell_coordinates=True,\n            table_format=\"markdown\",\n        )\n        excel_path = os.path.join(\n            os.path.abspath(os.path.dirname(__file__)),\n            \"test.xlsx\",\n        )\n        docs = await reader(excel_path=excel_path)\n\n        # Verify document types match expected sequence\n        # Expected: table blocks from first sheet, then image (row 9),\n        # then table blocks from second sheet\n        # Order is based on row positions: table (row 0-5) → image (row 9)\n        # → table (row 0-4)\n        # Note: with include_cell_coordinates=True, cell coordinates are added\n        # to each cell (e.g., [A1], [B1], etc.), which increases text length\n        # and results in more chunks\n        self.assertListEqual(\n            [_.metadata.content[\"type\"] for _ in docs],\n            [\"text\"] * 3 + [\"image\"] * 1 + [\"text\"] * 5,\n        )\n\n        # Verify exact document content\n        doc_texts = [_.metadata.content.get(\"text\") for _ in docs]\n\n        # Verify sheet headers and table content with cell coordinates\n        # First text block should contain Employee Info sheet header and table\n        # Note: Due to chunk_size=200, the rows are truncated\n        # Order: table (row 0-5) → image (row 9) → table (row 0-4)\n        self.assertEqual(\n            doc_texts[0],\n            \"Sheet: Employee Info\\n\"\n            \"| [A1] John Smith | [B1] 25 | [C1] Engineering | \"\n            \"[D1] 8000 | [E1] 2020-01-15 |\\n\"\n            \"| --- | --- | --- | --- | --- |\\n\"\n            \"| [A2] Jane Doe | [B2] 30 | [C2] Sales | \"\n            \"[D2] 12000 | [E2] 2019-03-2\",\n        )\n        # Second text block continues the employee table\n        self.assertEqual(\n            doc_texts[1],\n            \"0 |\\n\"\n            \"| [A3] Mike \\\\| Johnson | [B3] 35 | [C3] HR | \"\n            \"[D3] 9000 | [E3] 2021-06-10 |\\n\"\n            \"| [A4] Sarah Wilson | [B4] 28 | [C4] Finance | \"\n            \"[D4] 10000 | [E4] 2020-09-05 |\\n\"\n            \"| [A5] David Brown | [B5] 32 | [C5] Marketi\",\n        )\n        # Third text block continues the employee table\n        self.assertEqual(\n            doc_texts[2],\n            \"ng | [D5] 11000 | [E5] 2018-12-01 |\",\n        )\n        # Image block (text is None)\n        self.assertIsNone(doc_texts[3])\n        # Fourth text block should contain Product Info sheet header and\n        # start of table\n        self.assertEqual(\n            doc_texts[4],\n            \"Sheet: Product Info\\n\"\n            \"| [A1] Product A | [B1] 100 | [C1] 50 | \"\n            \"[D1] High-quality Product A, suitable for various scenarios.\",\n        )\n        # Remaining blocks continue the product table\n        self.assertEqual(\n            doc_texts[5],\n            \"|\\n\"\n            \"| --- | --- | --- | --- |\\n\"\n            \"| [A2] Product B | [B2] 200 | [C2] 30 | \"\n            \"[D2] Product B offers excellent performance.\",\n        )\n        self.assertEqual(\n            doc_texts[6],\n            \"|\\n\"\n            \"| [A3] Product C | [B3] 300 | [C3] 20 | \"\n            \"[D3] Product C is a market-leading solution.\",\n        )\n        self.assertEqual(\n            doc_texts[7],\n            \"|\\n\"\n            \"| [A4] Product D | [B4] 400 | [C4] 40 | \"\n            \"[D4] Product D provides comprehensive functionality.\",\n        )\n\n        # Verify image media types\n        self.assertEqual(\n            [\n                _.metadata.content[\"source\"][\"media_type\"]\n                for _ in docs\n                if _.metadata.content[\"type\"] == \"image\"\n            ],\n            [\"image/png\"],\n        )\n\n    async def test_ppt_reader_with_images_and_tables(self) -> None:\n        \"\"\"Test the PowerPointReader implementation with images and table\n        separation.\"\"\"\n        # Test with images and table separation enabled (using defaults)\n        reader = PowerPointReader(\n            chunk_size=200,\n            split_by=\"sentence\",\n            separate_table=True,\n        )\n        ppt_path = os.path.join(\n            os.path.abspath(os.path.dirname(__file__)),\n            \"../tests/test.pptx\",\n        )\n        docs = await reader(ppt_path=ppt_path)\n\n        # Verify document types match expected sequence\n        # Expected: text blocks from slides, then table, then image\n        self.assertListEqual(\n            [_.metadata.content[\"type\"] for _ in docs],\n            [\"text\"] * 5 + [\"image\"] * 1 + [\"text\"],\n        )\n\n        # Verify exact document content\n        doc_texts = [_.metadata.content.get(\"text\") for _ in docs]\n\n        # Verify slide content (with slide tags by default)\n        self.assertEqual(\n            doc_texts[0],\n            \"<slide index=1>\\nAgentScope\\nText content in slide 1\\n</slide>\",\n        )\n        self.assertEqual(\n            doc_texts[1],\n            \"<slide index=2>\\nTitle 2\\nText content above table\",\n        )\n        # Table should be extracted as a separate block with Markdown format\n        self.assertEqual(\n            doc_texts[2],\n            \"| Name | Age | Career |\\n\"\n            \"| --- | --- | --- |\\n\"\n            \"| Alice | 25 | Teacher |\\n\"\n            \"| Bob | 26 | Doctor |\",\n        )\n        self.assertEqual(\n            doc_texts[3],\n            \"Text content below table\\n</slide>\",\n        )\n        self.assertEqual(\n            doc_texts[4],\n            \"<slide index=3>\\nTitle 3\\ntext content above image\",\n        )\n        # Image block\n        self.assertIsNone(doc_texts[5])\n        self.assertEqual(\n            doc_texts[6],\n            \"text content below image\\n</slide>\",\n        )\n\n        # Verify image media types\n        self.assertEqual(\n            [\n                _.metadata.content[\"source\"][\"media_type\"]\n                for _ in docs\n                if _.metadata.content[\"type\"] == \"image\"\n            ],\n            [\"image/png\"],\n        )\n\n    async def test_ppt_reader_with_json_table_format(self) -> None:\n        \"\"\"Test the PowerPointReader with JSON table format.\"\"\"\n\n        reader = PowerPointReader(\n            chunk_size=500,\n            split_by=\"sentence\",\n            include_image=False,\n            separate_table=True,\n            table_format=\"json\",\n        )\n        ppt_path = os.path.join(\n            os.path.abspath(os.path.dirname(__file__)),\n            \"../tests/test.pptx\",\n        )\n        docs = await reader(ppt_path=ppt_path)\n\n        # Find the table block\n        table_texts = [\n            _.metadata.content.get(\"text\")\n            for _ in docs\n            if _.metadata.content.get(\"text\")\n            and \"JSON array\" in _.metadata.content.get(\"text\", \"\")\n        ]\n\n        # Verify we have a table in JSON format\n        self.assertEqual(len(table_texts), 1)\n\n        # Extract JSON part (after the system-info tag)\n        table_text = table_texts[0]\n        json_part = table_text.split(\"\\n\", 1)[1]\n\n        # Verify it's valid JSON and matches expected table content\n        parsed = json.loads(json_part)\n        self.assertEqual(\n            parsed,\n            [\n                [\"Name\", \"Age\", \"Career\"],\n                [\"Alice\", \"25\", \"Teacher\"],\n                [\"Bob\", \"26\", \"Doctor\"],\n            ],\n        )\n\n    async def test_ppt_reader_without_image(self) -> None:\n        \"\"\"Test the PowerPointReader without image extraction.\"\"\"\n        reader = PowerPointReader(\n            chunk_size=200,\n            split_by=\"sentence\",\n            include_image=False,\n            separate_table=True,\n        )\n        ppt_path = os.path.join(\n            os.path.abspath(os.path.dirname(__file__)),\n            \"../tests/test.pptx\",\n        )\n        docs = await reader(ppt_path=ppt_path)\n\n        # Verify no image blocks are present\n        image_blocks = [\n            _ for _ in docs if _.metadata.content[\"type\"] == \"image\"\n        ]\n        self.assertEqual(len(image_blocks), 0)\n\n        # All blocks should be text type\n        self.assertTrue(\n            all(_.metadata.content[\"type\"] == \"text\" for _ in docs),\n        )\n\n    async def test_ppt_reader_merged_table(self) -> None:\n        \"\"\"Test the PowerPointReader with merged table\n        (separate_table=False).\"\"\"\n        reader = PowerPointReader(\n            chunk_size=500,\n            split_by=\"sentence\",\n            include_image=False,\n            separate_table=False,\n        )\n        ppt_path = os.path.join(\n            os.path.abspath(os.path.dirname(__file__)),\n            \"../tests/test.pptx\",\n        )\n        docs = await reader(ppt_path=ppt_path)\n\n        # When separate_table=False, table should be merged with adjacent text\n        # Find the document that contains the table\n        table_doc = None\n        for doc in docs:\n            text = doc.metadata.content.get(\"text\", \"\")\n            if \"Name\" in text and \"Age\" in text and \"Career\" in text:\n                table_doc = doc\n                break\n\n        self.assertIsNotNone(table_doc)\n        # The table should be merged with surrounding text (with slide tags)\n        table_text = table_doc.metadata.content.get(\"text\", \"\")\n        self.assertEqual(\n            table_text,\n            \"<slide index=2>\\n\"\n            \"Title 2\\n\"\n            \"Text content above table\\n\"\n            \"| Name | Age | Career |\\n\"\n            \"| --- | --- | --- |\\n\"\n            \"| Alice | 25 | Teacher |\\n\"\n            \"| Bob | 26 | Doctor |\\n\"\n            \"\\n\"\n            \"Text content below table\\n\"\n            \"</slide>\",\n        )\n\n    async def test_ppt_reader_without_slide_tags(self) -> None:\n        \"\"\"Test the PowerPointReader without slide prefix/suffix XML tags.\"\"\"\n        reader = PowerPointReader(\n            chunk_size=500,\n            split_by=\"sentence\",\n            include_image=False,\n            separate_table=False,\n            slide_prefix=None,\n            slide_suffix=None,\n        )\n        ppt_path = os.path.join(\n            os.path.abspath(os.path.dirname(__file__)),\n            \"../tests/test.pptx\",\n        )\n        docs = await reader(ppt_path=ppt_path)\n\n        # Without slide_prefix/suffix, content should not have XML tags\n        doc_texts = [_.metadata.content.get(\"text\") for _ in docs]\n\n        # Verify exact content without slide tags\n        self.assertEqual(\n            doc_texts[0],\n            \"AgentScope\\nText content in slide 1\",\n        )\n        self.assertEqual(\n            doc_texts[1],\n            \"Title 2\\n\"\n            \"Text content above table\\n\"\n            \"| Name | Age | Career |\\n\"\n            \"| --- | --- | --- |\\n\"\n            \"| Alice | 25 | Teacher |\\n\"\n            \"| Bob | 26 | Doctor |\\n\"\n            \"\\n\"\n            \"Text content below table\",\n        )\n        self.assertEqual(\n            doc_texts[2],\n            \"Title 3\\ntext content above image\\ntext content below image\",\n        )\n"
  },
  {
    "path": "tests/rag_store_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Test the RAG store implementations.\"\"\"\nimport os\nimport types\nimport uuid\nfrom typing import AsyncGenerator\nfrom unittest import IsolatedAsyncioTestCase\nfrom unittest.mock import MagicMock, patch, AsyncMock\n\nfrom agentscope.message import TextBlock\nfrom agentscope.rag import (\n    QdrantStore,\n    Document,\n    DocMetadata,\n    MilvusLiteStore,\n    OceanBaseStore,\n    AlibabaCloudMySQLStore,\n    MongoDBStore,\n)\n\n\nclass RAGStoreTest(IsolatedAsyncioTestCase):\n    \"\"\"Test cases for RAG store implementations.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up before each test.\"\"\"\n        # Remove the test database file after the test\n        if os.path.exists(\"./milvus_demo.db\"):\n            os.remove(\"./milvus_demo.db\")\n\n    async def test_qdrant_store(self) -> None:\n        \"\"\"Test the QdrantStore implementation.\"\"\"\n        store = QdrantStore(\n            location=\":memory:\",\n            collection_name=\"test\",\n            dimensions=3,\n        )\n\n        await store.add(\n            [\n                Document(\n                    embedding=[0.1, 0.2, 0.3],\n                    metadata=DocMetadata(\n                        content=TextBlock(\n                            type=\"text\",\n                            text=\"This is a test document.\",\n                        ),\n                        doc_id=\"doc1\",\n                        chunk_id=0,\n                        total_chunks=2,\n                    ),\n                ),\n                Document(\n                    embedding=[0.9, 0.1, 0.4],\n                    metadata=DocMetadata(\n                        content=TextBlock(\n                            type=\"text\",\n                            text=\"This is another test document.\",\n                        ),\n                        doc_id=\"doc1\",\n                        chunk_id=1,\n                        total_chunks=2,\n                    ),\n                ),\n            ],\n        )\n\n        res = await store.search(\n            query_embedding=[0.15, 0.25, 0.35],\n            limit=3,\n            score_threshold=0.8,\n        )\n        self.assertEqual(len(res), 1)\n        self.assertEqual(\n            res[0].score // 0.0001,\n            9974,\n        )\n        self.assertEqual(\n            res[0].metadata.content[\"text\"],\n            \"This is a test document.\",\n        )\n\n    async def test_milvus_lite_store(self) -> None:\n        \"\"\"Test the MilvusLiteStore implementation.\"\"\"\n        if os.name == \"nt\":\n            self.skipTest(\"Milvus Lite is not supported on Windows.\")\n\n        store = MilvusLiteStore(\n            uri=\"./milvus_demo.db\",\n            collection_name=\"test_milvus\",\n            dimensions=3,\n        )\n\n        await store.add(\n            [\n                Document(\n                    embedding=[0.1, 0.2, 0.3],\n                    metadata=DocMetadata(\n                        content=TextBlock(\n                            type=\"text\",\n                            text=\"This is a test document.\",\n                        ),\n                        doc_id=\"doc1\",\n                        chunk_id=0,\n                        total_chunks=2,\n                    ),\n                ),\n                Document(\n                    embedding=[0.9, 0.1, 0.4],\n                    metadata=DocMetadata(\n                        content=TextBlock(\n                            type=\"text\",\n                            text=\"This is another test document.\",\n                        ),\n                        doc_id=\"doc1\",\n                        chunk_id=1,\n                        total_chunks=2,\n                    ),\n                ),\n            ],\n        )\n\n        res = await store.search(\n            query_embedding=[0.15, 0.25, 0.35],\n            limit=3,\n            score_threshold=0.8,\n        )\n        self.assertEqual(len(res), 1)\n        self.assertEqual(\n            round(res[0].score, 4),\n            0.9974,\n        )\n        self.assertEqual(\n            res[0].metadata.content[\"text\"],\n            \"This is a test document.\",\n        )\n\n    async def test_oceanbase_store(self) -> None:\n        \"\"\"Use real OceanBase when env is provided, otherwise use a mock.\"\"\"\n\n        def _make_mock_pyobvector(\n            search_rows: list[dict],\n        ) -> tuple[types.SimpleNamespace, MagicMock]:\n            \"\"\"Create a minimal pyobvector mock aligned with the\n            existing style.\"\"\"\n            mock_client = MagicMock()\n            mock_client.has_collection.return_value = False\n            mock_client.create_schema.return_value = MagicMock()\n            mock_client.prepare_index_params.return_value = MagicMock()\n            mock_client.search.return_value = search_rows\n\n            mock_pyobvector = types.SimpleNamespace(\n                MilvusLikeClient=MagicMock(return_value=mock_client),\n                DataType=types.SimpleNamespace(\n                    VARCHAR=\"VARCHAR\",\n                    FLOAT_VECTOR=\"FLOAT_VECTOR\",\n                    STRING=\"STRING\",\n                    INT64=\"INT64\",\n                    JSON=\"JSON\",\n                ),\n            )\n            return mock_pyobvector, mock_client\n\n        required_vars = [\n            \"OCEANBASE_URI\",\n            \"OCEANBASE_USER\",\n            \"OCEANBASE_DB\",\n        ]\n        missing_vars = [var for var in required_vars if not os.getenv(var)]\n        if missing_vars:\n            # COSINE distance = 1 - similarity\n            mock_rows = [\n                {\n                    \"doc_id\": \"doc1\",\n                    \"chunk_id\": 0,\n                    \"total_chunks\": 2,\n                    \"content\": {\n                        \"type\": \"text\",\n                        \"text\": \"This is a test document.\",\n                    },\n                    \"distance\": 1.0 - 0.9974,\n                },\n            ]\n            mock_pyobvector, mock_client = _make_mock_pyobvector(mock_rows)\n\n            with patch.dict(\"sys.modules\", {\"pyobvector\": mock_pyobvector}):\n                store = OceanBaseStore(\n                    collection_name=f\"test_ob_{uuid.uuid4().hex[:8]}\",\n                    dimensions=3,\n                    uri=\"127.0.0.1:2881\",\n                    user=\"root@test\",\n                    password=\"\",\n                    db_name=\"test\",\n                )\n\n                await store.add(\n                    [\n                        Document(\n                            embedding=[0.1, 0.2, 0.3],\n                            metadata=DocMetadata(\n                                content=TextBlock(\n                                    type=\"text\",\n                                    text=\"This is a test document.\",\n                                ),\n                                doc_id=\"doc1\",\n                                chunk_id=0,\n                                total_chunks=2,\n                            ),\n                        ),\n                    ],\n                )\n\n                self.assertTrue(mock_client.insert.called)\n                self.assertTrue(mock_client.create_collection.called)\n\n                res = await store.search(\n                    query_embedding=[0.15, 0.25, 0.35],\n                    limit=3,\n                    score_threshold=0.8,\n                )\n                self.assertEqual(len(res), 1)\n                self.assertEqual(round(res[0].score, 4), 0.9974)\n                self.assertEqual(\n                    res[0].metadata.content[\"text\"],\n                    \"This is a test document.\",\n                )\n\n                await store.delete(ids=[\"dummy-id\"])\n                self.assertTrue(mock_client.delete.called)\n            return\n\n        collection_name = f\"test_ob_{uuid.uuid4().hex[:8]}\"\n        store = OceanBaseStore(\n            collection_name=collection_name,\n            dimensions=3,\n            uri=os.getenv(\"OCEANBASE_URI\", \"\"),\n            user=os.getenv(\"OCEANBASE_USER\", \"\"),\n            password=os.getenv(\"OCEANBASE_PASSWORD\", \"\"),\n            db_name=os.getenv(\"OCEANBASE_DB\", \"\"),\n        )\n\n        client = store.get_client()\n        client.drop_collection(collection_name)\n\n        try:\n            await store.add(\n                [\n                    Document(\n                        embedding=[0.1, 0.2, 0.3],\n                        metadata=DocMetadata(\n                            content=TextBlock(\n                                type=\"text\",\n                                text=\"This is a test document.\",\n                            ),\n                            doc_id=\"doc1\",\n                            chunk_id=0,\n                            total_chunks=2,\n                        ),\n                    ),\n                    Document(\n                        embedding=[0.9, 0.1, 0.4],\n                        metadata=DocMetadata(\n                            content=TextBlock(\n                                type=\"text\",\n                                text=\"This is another test document.\",\n                            ),\n                            doc_id=\"doc1\",\n                            chunk_id=1,\n                            total_chunks=2,\n                        ),\n                    ),\n                ],\n            )\n\n            res = await store.search(\n                query_embedding=[0.15, 0.25, 0.35],\n                limit=3,\n                score_threshold=0.8,\n            )\n            self.assertEqual(len(res), 1)\n            self.assertEqual(\n                round(res[0].score, 4),\n                0.9974,\n            )\n            self.assertEqual(\n                res[0].metadata.content[\"text\"],\n                \"This is a test document.\",\n            )\n        finally:\n            client.drop_collection(collection_name)\n\n    async def test_alibabacloud_mysql_store(self) -> None:\n        \"\"\"Test the AlibabaCloudMySQLStore implementation using mocks.\"\"\"\n        # Create mock MySQL module and connector\n        mock_mysql_connector = MagicMock()\n        mock_mysql = MagicMock()\n        mock_mysql.connector = mock_mysql_connector\n\n        # Create mock cursor and connection\n        mock_cursor = MagicMock()\n        mock_conn = MagicMock()\n\n        # Configure mock connection to return mock cursor\n        mock_conn.cursor.return_value = mock_cursor\n        mock_mysql_connector.connect.return_value = mock_conn\n\n        # Mock the search query result\n        # Simulate a database row returned by fetchall\n        mock_search_result = [\n            {\n                \"id\": \"test-uuid-1\",\n                \"doc_id\": \"doc1\",\n                \"chunk_id\": 0,\n                \"content\": (\n                    '{\"type\": \"text\", \"text\": \"This is a test document.\"}'\n                ),\n                \"total_chunks\": 2,\n                \"distance\": 0.03,  # Low distance = high similarity\n            },\n        ]\n\n        # Use patch.dict to mock sys.modules\n        with patch.dict(\n            \"sys.modules\",\n            {\n                \"mysql\": mock_mysql,\n                \"mysql.connector\": mock_mysql_connector,\n            },\n        ):\n            # Create store instance\n            store = AlibabaCloudMySQLStore(\n                host=\"test-host\",\n                port=3306,\n                user=\"test-user\",\n                password=\"test-password\",\n                database=\"test-database\",\n                table_name=\"test_vectors\",\n                dimensions=3,\n            )\n\n            # Verify connection was established\n            mock_mysql_connector.connect.assert_called_once()\n\n            # Test add operation\n            await store.add(\n                [\n                    Document(\n                        embedding=[0.1, 0.2, 0.3],\n                        metadata=DocMetadata(\n                            content=TextBlock(\n                                type=\"text\",\n                                text=\"This is a test document.\",\n                            ),\n                            doc_id=\"doc1\",\n                            chunk_id=0,\n                            total_chunks=2,\n                        ),\n                    ),\n                    Document(\n                        embedding=[2.0, 3.8, 2.7],\n                        metadata=DocMetadata(\n                            content=TextBlock(\n                                type=\"text\",\n                                text=\"This is another test document.\",\n                            ),\n                            doc_id=\"doc1\",\n                            chunk_id=1,\n                            total_chunks=2,\n                        ),\n                    ),\n                ],\n            )\n\n            # Verify add operations executed SQL\n            self.assertTrue(mock_cursor.execute.called)\n            self.assertTrue(mock_conn.commit.called)\n\n            # Reset mock for search operation\n            mock_cursor.reset_mock()\n            mock_conn.reset_mock()\n\n            # Configure mock to return search results\n            mock_cursor.fetchall.return_value = mock_search_result\n\n            # Test search operation\n            res = await store.search(\n                query_embedding=[0.15, 0.25, 0.35],\n                limit=3,\n                score_threshold=0.95,\n            )\n\n            # Verify search results\n            self.assertEqual(len(res), 1)\n            # Score = 1 - distance = 1 - 0.03 = 0.97\n            self.assertAlmostEqual(res[0].score, 0.97, places=2)\n            self.assertEqual(\n                res[0].metadata.content[\"text\"],\n                \"This is a test document.\",\n            )\n            self.assertEqual(res[0].metadata.doc_id, \"doc1\")\n            self.assertEqual(res[0].metadata.chunk_id, 0)\n            self.assertEqual(res[0].metadata.total_chunks, 2)\n\n            # Verify search executed SQL query\n            self.assertTrue(mock_cursor.execute.called)\n            self.assertTrue(mock_cursor.fetchall.called)\n\n            # Test delete operation\n            await store.delete(filter='doc_id = \"doc1\"')\n\n            # Verify delete executed SQL\n            self.assertTrue(mock_conn.commit.called)\n\n            # Test close\n            store.close()\n\n            # Verify connections were closed\n            mock_cursor.close.assert_called()\n            mock_conn.close.assert_called()\n\n    async def test_mongodb_store(self) -> None:\n        \"\"\"Test the MongoDBStore implementation using mocks.\"\"\"\n        # Create mock pymongo module\n        mock_pymongo = MagicMock()\n        mock_operations = MagicMock()\n        mock_pymongo.operations = mock_operations\n\n        # Create mock AsyncMongoClient\n        mock_client = MagicMock()\n        mock_db = MagicMock()\n        mock_collection = MagicMock()\n\n        # Configure mock client\n        mock_pymongo.AsyncMongoClient.return_value = mock_client\n        mock_client.get_database.return_value = mock_db\n\n        # Configure mock database\n        mock_db.list_collection_names = AsyncMock(return_value=[])\n        mock_db.create_collection = AsyncMock(return_value=mock_collection)\n        mock_db.get_collection.return_value = mock_collection\n\n        # Configure mock collection\n        mock_collection.create_search_index = AsyncMock()\n        mock_collection.bulk_write = AsyncMock()\n        mock_collection.delete_many = AsyncMock()\n        mock_collection.drop = AsyncMock()\n\n        # Mock list_search_indexes to return queryable index\n        async def mock_index_iter() -> AsyncGenerator:\n            yield {\"queryable\": True}\n\n        mock_collection.list_search_indexes = AsyncMock(\n            return_value=mock_index_iter(),\n        )\n\n        # Mock aggregate to return search results\n        mock_search_results = [\n            {\n                \"vector\": [0.1, 0.2, 0.3],\n                \"payload\": {\n                    \"doc_id\": \"doc1\",\n                    \"chunk_id\": 0,\n                    \"total_chunks\": 2,\n                    \"content\": {\n                        \"type\": \"text\",\n                        \"text\": \"This is a test document.\",\n                    },\n                },\n                \"score\": 0.97,\n            },\n        ]\n\n        async def mock_aggregate_iter() -> AsyncGenerator:\n            for item in mock_search_results:\n                yield item\n\n        mock_collection.aggregate = AsyncMock(\n            return_value=mock_aggregate_iter(),\n        )\n\n        # Mock client close\n        mock_client.close = AsyncMock()\n        mock_client.drop_database = AsyncMock()\n\n        # Mock ReplaceOne\n        mock_replace_one = MagicMock()\n        mock_pymongo.ReplaceOne = mock_replace_one\n\n        with patch.dict(\n            \"sys.modules\",\n            {\n                \"pymongo\": mock_pymongo,\n                \"pymongo.operations\": mock_operations,\n            },\n        ):\n            # Create store instance\n            store = MongoDBStore(\n                host=\"mongodb://localhost:27017\",\n                db_name=\"test_db\",\n                collection_name=\"test_collection\",\n                dimensions=3,\n                distance=\"cosine\",\n            )\n\n            # Verify client was created\n            mock_pymongo.AsyncMongoClient.assert_called_once()\n\n            # Test add operation\n            await store.add(\n                [\n                    Document(\n                        embedding=[0.1, 0.2, 0.3],\n                        metadata=DocMetadata(\n                            content=TextBlock(\n                                type=\"text\",\n                                text=\"This is a test document.\",\n                            ),\n                            doc_id=\"doc1\",\n                            chunk_id=0,\n                            total_chunks=2,\n                        ),\n                    ),\n                    Document(\n                        embedding=[0.9, 0.1, 0.4],\n                        metadata=DocMetadata(\n                            content=TextBlock(\n                                type=\"text\",\n                                text=\"This is another test document.\",\n                            ),\n                            doc_id=\"doc1\",\n                            chunk_id=1,\n                            total_chunks=2,\n                        ),\n                    ),\n                ],\n            )\n\n            # Verify add operations\n            self.assertTrue(mock_collection.bulk_write.called)\n\n            # Reset mocks for search operation\n            mock_collection.reset_mock()\n\n            # Reconfigure list_search_indexes for search\n            mock_collection.list_search_indexes = AsyncMock(\n                return_value=mock_index_iter(),\n            )\n            mock_collection.aggregate = AsyncMock(\n                return_value=mock_aggregate_iter(),\n            )\n\n            # Test search operation\n            res = await store.search(\n                query_embedding=[0.15, 0.25, 0.35],\n                limit=3,\n                score_threshold=0.8,\n            )\n\n            # Verify search results\n            self.assertEqual(len(res), 1)\n            self.assertAlmostEqual(res[0].score, 0.97, places=2)\n            self.assertEqual(\n                res[0].metadata.content[\"text\"],\n                \"This is a test document.\",\n            )\n            self.assertEqual(res[0].metadata.doc_id, \"doc1\")\n            self.assertEqual(res[0].metadata.chunk_id, 0)\n            self.assertEqual(res[0].metadata.total_chunks, 2)\n\n            # Verify search executed aggregate\n            self.assertTrue(mock_collection.aggregate.called)\n\n            # Test delete operation\n            await store.delete(ids=\"doc1\")\n\n            # Verify delete executed\n            mock_collection.delete_many.assert_called_with(\n                {\"payload.doc_id\": {\"$in\": [\"doc1\"]}},\n            )\n\n            # Test delete_collection\n            await store.delete_collection()\n            mock_collection.drop.assert_called()\n\n            # Test delete_database\n            await store.delete_database()\n            mock_client.drop_database.assert_called_with(\"test_db\")\n\n            # Test close\n            await store.close()\n            mock_client.close.assert_called()\n\n    async def asyncTearDown(self) -> None:\n        \"\"\"Clean up after tests.\"\"\"\n        # Remove Milvus Lite database file\n        if os.path.exists(\"./milvus_demo.db\"):\n            os.remove(\"./milvus_demo.db\")\n"
  },
  {
    "path": "tests/react_agent_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The ReAct agent unittests.\"\"\"\nfrom typing import Any\nfrom unittest import IsolatedAsyncioTestCase\n\nfrom pydantic import BaseModel, Field\n\nfrom agentscope.agent import ReActAgent\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.message import TextBlock, ToolUseBlock, Msg\nfrom agentscope.model import ChatModelBase, ChatResponse\nfrom agentscope.tool import Toolkit\n\n\nclass MyModel(ChatModelBase):\n    \"\"\"Test model class.\"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"Initialize the test model.\"\"\"\n        super().__init__(\"test_model\", stream=False)\n        self.cnt = 1\n        self.fake_content_1 = [\n            TextBlock(\n                type=\"text\",\n                text=\"123\",\n            ),\n        ]\n        self.fake_content_2 = [\n            TextBlock(type=\"text\", text=\"456\"),\n            ToolUseBlock(\n                type=\"tool_use\",\n                name=\"generate_response\",\n                id=\"xx\",\n                input={\"result\": \"789\"},\n            ),\n        ]\n\n    async def __call__(\n        self,\n        _messages: list[dict],\n        **kwargs: Any,\n    ) -> ChatResponse:\n        \"\"\"Mock model call.\"\"\"\n        self.cnt += 1\n        if self.cnt == 2:\n            return ChatResponse(\n                content=self.fake_content_1,\n            )\n        else:\n            return ChatResponse(\n                content=self.fake_content_2,\n            )\n\n\nasync def pre_reasoning_hook(self: ReActAgent, _kwargs: Any) -> None:\n    \"\"\"Mock pre-reasoning hook.\"\"\"\n    if hasattr(self, \"cnt_pre_reasoning\"):\n        self.cnt_pre_reasoning += 1\n    else:\n        self.cnt_pre_reasoning = 1\n\n\nasync def post_reasoning_hook(\n    self: ReActAgent,\n    _kwargs: Any,\n    _output: Msg | None,\n) -> None:\n    \"\"\"Mock post-reasoning hook.\"\"\"\n    if hasattr(self, \"cnt_post_reasoning\"):\n        self.cnt_post_reasoning += 1\n    else:\n        self.cnt_post_reasoning = 1\n\n\nasync def pre_acting_hook(self: ReActAgent, _kwargs: Any) -> None:\n    \"\"\"Mock pre-acting hook.\"\"\"\n    if hasattr(self, \"cnt_pre_acting\"):\n        self.cnt_pre_acting += 1\n    else:\n        self.cnt_pre_acting = 1\n\n\nasync def post_acting_hook(\n    self: ReActAgent,\n    _kwargs: Any,\n    _output: Msg | None,\n) -> None:\n    \"\"\"Mock post-acting hook.\"\"\"\n    if hasattr(self, \"cnt_post_acting\"):\n        self.cnt_post_acting += 1\n    else:\n        self.cnt_post_acting = 1\n\n\nclass ReActAgentTest(IsolatedAsyncioTestCase):\n    \"\"\"Test class for ReActAgent.\"\"\"\n\n    async def test_react_agent(self) -> None:\n        \"\"\"Test the ReActAgent class\"\"\"\n        model = MyModel()\n        agent = ReActAgent(\n            name=\"Friday\",\n            sys_prompt=\"You are a helpful assistant named Friday.\",\n            model=model,\n            formatter=DashScopeChatFormatter(),\n            memory=InMemoryMemory(),\n            toolkit=Toolkit(),\n        )\n\n        agent.register_instance_hook(\n            \"pre_reasoning\",\n            \"test_hook\",\n            pre_reasoning_hook,\n        )\n\n        agent.register_instance_hook(\n            \"post_reasoning\",\n            \"test_hook\",\n            post_reasoning_hook,\n        )\n\n        agent.register_instance_hook(\n            \"pre_acting\",\n            \"test_hook\",\n            pre_acting_hook,\n        )\n\n        agent.register_instance_hook(\n            \"post_acting\",\n            \"test_hook\",\n            post_acting_hook,\n        )\n\n        await agent()\n        self.assertEqual(\n            getattr(agent, \"cnt_pre_reasoning\"),\n            1,\n        )\n        self.assertEqual(\n            getattr(agent, \"cnt_post_reasoning\"),\n            1,\n        )\n        # Note: pre_acting and post_acting hooks are not called when model\n        # returns plain text without structured output, as plain text is not\n        # converted to tool call in this case\n        self.assertFalse(\n            hasattr(agent, \"cnt_pre_acting\"),\n            \"pre_acting hook should not be called for plain text response\",\n        )\n        self.assertFalse(\n            hasattr(agent, \"cnt_post_acting\"),\n            \"post_acting hook should not be called for plain text response\",\n        )\n\n        # Test with structured output: generate_response should be registered\n        # and visible in tool list\n        class TestStructuredModel(BaseModel):\n            \"\"\"Test structured model.\"\"\"\n\n            result: str = Field(description=\"Test result field.\")\n\n        await agent(structured_model=TestStructuredModel)\n        self.assertEqual(\n            getattr(agent, \"cnt_pre_reasoning\"),\n            2,\n        )\n        self.assertEqual(\n            getattr(agent, \"cnt_post_reasoning\"),\n            2,\n        )\n        # pre_acting and post_acting hooks are called only when model returns\n        # tool calls (not plain text). With structured_model, generate_response\n        # is registered and model can call it.\n        self.assertEqual(\n            getattr(agent, \"cnt_pre_acting\"),\n            1,  # Only called once (second call with tool use)\n        )\n        self.assertEqual(\n            getattr(agent, \"cnt_post_acting\"),\n            1,  # Only called once (second call with tool use)\n        )\n\n        # Verify that generate_response is removed when no structured_model\n        # Reset model to return plain text\n        model.fake_content_2 = [TextBlock(type=\"text\", text=\"456\")]\n        await agent()\n        self.assertFalse(\n            agent.finish_function_name in agent.toolkit.tools,\n            \"generate_response should be removed when no structured_model\",\n        )\n"
  },
  {
    "path": "tests/realtime_dashscope_test.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=protected-access\n\"\"\"Unit tests for DashScope Realtime Model class.\"\"\"\nimport json\nfrom unittest.async_case import IsolatedAsyncioTestCase\nfrom unittest.mock import AsyncMock, patch\n\nfrom agentscope.realtime import DashScopeRealtimeModel, ModelEvents\nfrom agentscope.message import (\n    AudioBlock,\n    ImageBlock,\n    Base64Source,\n    URLSource,\n)\n\n\nclass TestDashScopeRealtimeModelParseAPIMessage(IsolatedAsyncioTestCase):\n    \"\"\"Test parsing API messages from DashScope realtime model.\"\"\"\n\n    def setUp(self) -> None:\n        \"\"\"Set up test fixtures.\"\"\"\n        self.model = DashScopeRealtimeModel(\n            model_name=\"qwen3-omni-flash-realtime\",\n            api_key=\"test_api_key\",\n            voice=\"Cherry\",\n        )\n\n    async def test_parse_session_created_event(self) -> None:\n        \"\"\"Test parsing session.created event.\"\"\"\n        message = json.dumps(\n            {\n                \"type\": \"session.created\",\n                \"session\": {\n                    \"id\": \"session_123\",\n                },\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelSessionCreatedEvent)\n        self.assertEqual(event.session_id, \"session_123\")\n        self.assertEqual(event.type, \"model_session_created\")\n\n    async def test_parse_response_created_event(self) -> None:\n        \"\"\"Test parsing response.created event.\"\"\"\n        message = json.dumps(\n            {\n                \"type\": \"response.created\",\n                \"response\": {\n                    \"id\": \"resp_456\",\n                },\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelResponseCreatedEvent)\n        self.assertEqual(event.response_id, \"resp_456\")\n        self.assertEqual(event.type, \"model_response_created\")\n        # Check that response_id is stored internally\n        self.assertEqual(self.model._response_id, \"resp_456\")\n\n    async def test_parse_response_done_event(self) -> None:\n        \"\"\"Test parsing response.done event.\"\"\"\n        # Set up the internal response_id\n        self.model._response_id = \"resp_789\"\n\n        message = json.dumps(\n            {\n                \"type\": \"response.done\",\n                \"response\": {\n                    \"id\": \"resp_789\",\n                    \"usage\": {\n                        \"input_tokens\": 100,\n                        \"output_tokens\": 50,\n                    },\n                },\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelResponseDoneEvent)\n        self.assertEqual(event.response_id, \"resp_789\")\n        self.assertEqual(event.input_tokens, 100)\n        self.assertEqual(event.output_tokens, 50)\n        self.assertEqual(event.type, \"model_response_done\")\n        # Check that response_id is cleared\n        self.assertEqual(self.model._response_id, \"\")\n\n    async def test_parse_response_audio_delta_event(self) -> None:\n        \"\"\"Test parsing response.audio.delta event.\"\"\"\n        self.model._response_id = \"resp_audio_1\"\n\n        message = json.dumps(\n            {\n                \"type\": \"response.audio.delta\",\n                \"item_id\": \"item_audio_1\",\n                \"delta\": \"base64_audio_data_chunk\",\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelResponseAudioDeltaEvent)\n        self.assertEqual(event.response_id, \"resp_audio_1\")\n        self.assertEqual(event.item_id, \"item_audio_1\")\n        self.assertEqual(event.delta, \"base64_audio_data_chunk\")\n        self.assertEqual(event.format.type, \"audio/pcm\")\n        self.assertEqual(event.format.rate, 24000)\n        self.assertEqual(event.type, \"model_response_audio_delta\")\n\n    async def test_parse_response_audio_done_event(self) -> None:\n        \"\"\"Test parsing response.audio.done event.\"\"\"\n        self.model._response_id = \"resp_audio_2\"\n\n        message = json.dumps(\n            {\n                \"type\": \"response.audio.done\",\n                \"item_id\": \"item_audio_2\",\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelResponseAudioDoneEvent)\n        self.assertEqual(event.response_id, \"resp_audio_2\")\n        self.assertEqual(event.item_id, \"item_audio_2\")\n        self.assertEqual(event.type, \"model_response_audio_done\")\n\n    async def test_parse_response_audio_transcript_delta_event(self) -> None:\n        \"\"\"Test parsing response.audio_transcript.delta event.\"\"\"\n        self.model._response_id = \"resp_transcript_1\"\n\n        message = json.dumps(\n            {\n                \"type\": \"response.audio_transcript.delta\",\n                \"item_id\": \"item_transcript_1\",\n                \"delta\": \"Hello \",\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(\n            event,\n            ModelEvents.ModelResponseAudioTranscriptDeltaEvent,\n        )\n        self.assertEqual(event.response_id, \"resp_transcript_1\")\n        self.assertEqual(event.item_id, \"item_transcript_1\")\n        self.assertEqual(event.delta, \"Hello \")\n        self.assertEqual(\n            event.type,\n            \"model_response_audio_transcript_delta\",\n        )\n\n    async def test_parse_response_audio_transcript_done_event(self) -> None:\n        \"\"\"Test parsing response.audio_transcript.done event.\"\"\"\n        self.model._response_id = \"resp_transcript_2\"\n\n        message = json.dumps(\n            {\n                \"type\": \"response.audio_transcript.done\",\n                \"item_id\": \"item_transcript_2\",\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(\n            event,\n            ModelEvents.ModelResponseAudioTranscriptDoneEvent,\n        )\n        self.assertEqual(event.response_id, \"resp_transcript_2\")\n        self.assertEqual(event.item_id, \"item_transcript_2\")\n        self.assertEqual(event.type, \"model_response_audio_transcript_done\")\n\n    async def test_parse_input_audio_transcription_completed_event(\n        self,\n    ) -> None:\n        \"\"\"Test parsing conversation.item.input_audio_transcription.completed\n        event.\"\"\"\n        message = json.dumps(\n            {\n                \"type\": \"conversation.item.input_audio_transcription.\"\n                \"completed\",\n                \"item_id\": \"item_input_1\",\n                \"transcript\": \"Hello world\",\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(\n            event,\n            ModelEvents.ModelInputTranscriptionDoneEvent,\n        )\n        self.assertEqual(event.item_id, \"item_input_1\")\n        self.assertEqual(event.transcript, \"Hello world\")\n        self.assertEqual(event.type, \"model_input_transcription_done\")\n\n    async def test_parse_input_audio_buffer_speech_started_event(\n        self,\n    ) -> None:\n        \"\"\"Test parsing input_audio_buffer.speech_started event.\"\"\"\n        message = json.dumps(\n            {\n                \"type\": \"input_audio_buffer.speech_started\",\n                \"item_id\": \"item_vad_1\",\n                \"audio_start_ms\": 1000,\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelInputStartedEvent)\n        self.assertEqual(event.item_id, \"item_vad_1\")\n        self.assertEqual(event.audio_start_ms, 1000)\n        self.assertEqual(event.type, \"model_input_started\")\n\n    async def test_parse_input_audio_buffer_speech_stopped_event(\n        self,\n    ) -> None:\n        \"\"\"Test parsing input_audio_buffer.speech_stopped event.\"\"\"\n        message = json.dumps(\n            {\n                \"type\": \"input_audio_buffer.speech_stopped\",\n                \"item_id\": \"item_vad_2\",\n                \"audio_end_ms\": 5000,\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelInputDoneEvent)\n        self.assertEqual(event.item_id, \"item_vad_2\")\n        self.assertEqual(event.audio_end_ms, 5000)\n        self.assertEqual(event.type, \"model_input_done\")\n\n    async def test_parse_error_event(self) -> None:\n        \"\"\"Test parsing error event.\"\"\"\n        message = json.dumps(\n            {\n                \"type\": \"error\",\n                \"error\": {\n                    \"type\": \"invalid_request\",\n                    \"code\": \"400\",\n                    \"message\": \"Invalid request format\",\n                },\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelErrorEvent)\n        self.assertEqual(event.error_type, \"invalid_request\")\n        self.assertEqual(event.code, \"400\")\n        self.assertEqual(event.message, \"Invalid request format\")\n        self.assertEqual(event.type, \"model_error\")\n\n\nclass TestDashScopeRealtimeModelSend(IsolatedAsyncioTestCase):\n    \"\"\"Test sending data to DashScope realtime model.\"\"\"\n\n    def setUp(self) -> None:\n        \"\"\"Set up test fixtures.\"\"\"\n        from websockets import State\n\n        self.model = DashScopeRealtimeModel(\n            model_name=\"qwen3-omni-flash-realtime\",\n            api_key=\"test_api_key\",\n            voice=\"Cherry\",\n        )\n        # Mock the websocket\n        self.mock_websocket = AsyncMock()\n        self.mock_websocket.state = State.OPEN\n        self.model._websocket = self.mock_websocket\n\n    async def test_send_audio_base64(self) -> None:\n        \"\"\"Test sending audio data with base64 source.\"\"\"\n        audio_data = AudioBlock(\n            type=\"audio\",\n            source=Base64Source(\n                type=\"base64\",\n                media_type=\"audio/wav\",\n                data=\"base64_encoded_audio_data\",\n            ),\n        )\n\n        await self.model.send(audio_data)\n\n        # Verify websocket.send was called\n        self.mock_websocket.send.assert_called_once()\n\n        # Parse the send message\n        sent_message = self.mock_websocket.send.call_args[0][0]\n        sent_data = json.loads(sent_message)\n\n        self.assertEqual(sent_data[\"type\"], \"input_audio_buffer.append\")\n        self.assertEqual(sent_data[\"audio\"], \"base64_encoded_audio_data\")\n\n    async def test_send_image_base64(self) -> None:\n        \"\"\"Test sending image data with base64 source.\"\"\"\n        image_data = ImageBlock(\n            type=\"image\",\n            source=Base64Source(\n                type=\"base64\",\n                media_type=\"image/png\",\n                data=\"base64_encoded_image_data\",\n            ),\n        )\n\n        await self.model.send(image_data)\n\n        # Verify websocket.send was called\n        self.mock_websocket.send.assert_called_once()\n\n        # Parse the send message\n        sent_message = self.mock_websocket.send.call_args[0][0]\n        sent_data = json.loads(sent_message)\n\n        self.assertEqual(sent_data[\"type\"], \"input_image_buffer.append\")\n        self.assertEqual(sent_data[\"image\"], \"base64_encoded_image_data\")\n\n    async def test_send_image_url(self) -> None:\n        \"\"\"Test sending image data with URL source.\"\"\"\n        image_data = ImageBlock(\n            type=\"image\",\n            source=URLSource(\n                type=\"url\",\n                url=\"https://example.com/image.jpg\",\n            ),\n        )\n\n        with patch(\n            \"agentscope.realtime._dashscope_realtime_model.\"\n            \"_get_bytes_from_web_url\",\n        ) as mock_get_bytes:\n            mock_get_bytes.return_value = \"fetched_image_bytes\"\n\n            await self.model.send(image_data)\n\n            # Verify URL was fetched\n            mock_get_bytes.assert_called_once_with(\n                \"https://example.com/image.jpg\",\n            )\n\n            # Verify websocket.send was called\n            self.mock_websocket.send.assert_called_once()\n\n            # Parse the sent message\n            sent_message = self.mock_websocket.send.call_args[0][0]\n            sent_data = json.loads(sent_message)\n\n            self.assertEqual(sent_data[\"type\"], \"input_image_url.append\")\n            self.assertEqual(sent_data[\"image_url\"], \"fetched_image_bytes\")\n\n    # async def test_send_text(self) -> None:\n    #     \"\"\"Test sending text data.\"\"\"\n    #     text_data = TextBlock(\n    #         type=\"text\",\n    #         text=\"Hello, how are you?\",\n    #     )\n    #\n    #     with patch(\"shortuuid.uuid\") as mock_uuid:\n    #         mock_uuid.return_value = \"test_uuid_123\"\n    #\n    #         await self.model.send(text_data)\n    #\n    #         # Verify websocket.send was called\n    #         self.mock_websocket.send.assert_called_once()\n    #\n    #         # Parse the sent message\n    #         sent_message = self.mock_websocket.send.call_args[0][0]\n    #         sent_data = json.loads(sent_message)\n    #\n    #         self.assertEqual(sent_data[\"event_id\"], \"test_uuid_123\")\n    #         self.assertEqual(sent_data[\"type\"], \"response.create\")\n    #         self.assertEqual(\n    #             sent_data[\"response\"][\"instructions\"],\n    #             \"Hello, how are you?\",\n    #         )\n"
  },
  {
    "path": "tests/realtime_event_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The realtime event test unittests.\"\"\"\nfrom unittest import IsolatedAsyncioTestCase\n\nfrom agentscope.realtime import ModelEvents, ServerEvents, ClientEvents\nfrom agentscope.message import ToolUseBlock\n\n\nclass TestServerEventsFromModelEvent(IsolatedAsyncioTestCase):\n    \"\"\"Test ServerEvents.from_model_event method.\"\"\"\n\n    def setUp(self) -> None:\n        \"\"\"Set up test fixtures.\"\"\"\n        self.agent_id = \"test_agent_123\"\n        self.agent_name = \"TestAgent\"\n\n    async def test_model_response_created_event(self) -> None:\n        \"\"\"Test converting ModelResponseCreatedEvent to\n        AgentResponseCreatedEvent.\"\"\"\n        model_event = ModelEvents.ModelResponseCreatedEvent(\n            response_id=\"resp_001\",\n        )\n\n        server_event = ServerEvents.from_model_event(\n            model_event,\n            agent_id=self.agent_id,\n            agent_name=self.agent_name,\n        )\n\n        self.assertIsInstance(\n            server_event,\n            ServerEvents.AgentResponseCreatedEvent,\n        )\n        self.assertEqual(server_event.response_id, \"resp_001\")\n        self.assertEqual(server_event.agent_id, self.agent_id)\n        self.assertEqual(server_event.agent_name, self.agent_name)\n        self.assertEqual(server_event.type, \"agent_response_created\")\n\n    async def test_model_response_done_event(self) -> None:\n        \"\"\"Test converting ModelResponseDoneEvent to AgentResponseDoneEvent.\"\"\"\n        model_event = ModelEvents.ModelResponseDoneEvent(\n            response_id=\"resp_002\",\n            input_tokens=100,\n            output_tokens=50,\n            metadata={\"key\": \"value\"},\n        )\n\n        server_event = ServerEvents.from_model_event(\n            model_event,\n            agent_id=self.agent_id,\n            agent_name=self.agent_name,\n        )\n\n        self.assertIsInstance(\n            server_event,\n            ServerEvents.AgentResponseDoneEvent,\n        )\n        self.assertEqual(server_event.response_id, \"resp_002\")\n        self.assertEqual(server_event.input_tokens, 100)\n        self.assertEqual(server_event.output_tokens, 50)\n        self.assertEqual(server_event.metadata, {\"key\": \"value\"})\n        self.assertEqual(server_event.agent_id, self.agent_id)\n        self.assertEqual(server_event.agent_name, self.agent_name)\n        self.assertEqual(server_event.type, \"agent_response_done\")\n\n    async def test_model_response_audio_delta_event(self) -> None:\n        \"\"\"Test converting ModelResponseAudioDeltaEvent to\n        AgentResponseAudioDeltaEvent.\"\"\"\n        model_event = ModelEvents.ModelResponseAudioDeltaEvent(\n            response_id=\"resp_003\",\n            item_id=\"item_001\",\n            delta=\"base64_audio_data\",\n            format={\"type\": \"audio/pcm\", \"rate\": 16000},\n        )\n\n        server_event = ServerEvents.from_model_event(\n            model_event,\n            agent_id=self.agent_id,\n            agent_name=self.agent_name,\n        )\n\n        self.assertIsInstance(\n            server_event,\n            ServerEvents.AgentResponseAudioDeltaEvent,\n        )\n        self.assertEqual(server_event.response_id, \"resp_003\")\n        self.assertEqual(server_event.item_id, \"item_001\")\n        self.assertEqual(server_event.delta, \"base64_audio_data\")\n        self.assertEqual(server_event.format.type, \"audio/pcm\")\n        self.assertEqual(server_event.format.rate, 16000)\n        self.assertEqual(server_event.agent_id, self.agent_id)\n        self.assertEqual(server_event.agent_name, self.agent_name)\n\n    async def test_model_response_audio_done_event(self) -> None:\n        \"\"\"Test converting ModelResponseAudioDoneEvent to\n        AgentResponseAudioDoneEvent.\"\"\"\n        model_event = ModelEvents.ModelResponseAudioDoneEvent(\n            response_id=\"resp_004\",\n            item_id=\"item_002\",\n        )\n\n        server_event = ServerEvents.from_model_event(\n            model_event,\n            agent_id=self.agent_id,\n            agent_name=self.agent_name,\n        )\n\n        self.assertIsInstance(\n            server_event,\n            ServerEvents.AgentResponseAudioDoneEvent,\n        )\n        self.assertEqual(server_event.response_id, \"resp_004\")\n        self.assertEqual(server_event.item_id, \"item_002\")\n        self.assertEqual(server_event.agent_id, self.agent_id)\n        self.assertEqual(server_event.agent_name, self.agent_name)\n\n    async def test_model_response_audio_transcript_delta_event(self) -> None:\n        \"\"\"Test converting ModelResponseAudioTranscriptDeltaEvent.\"\"\"\n        model_event = ModelEvents.ModelResponseAudioTranscriptDeltaEvent(\n            response_id=\"resp_005\",\n            item_id=\"item_003\",\n            delta=\"Hello \",\n        )\n\n        server_event = ServerEvents.from_model_event(\n            model_event,\n            agent_id=self.agent_id,\n            agent_name=self.agent_name,\n        )\n\n        self.assertIsInstance(\n            server_event,\n            ServerEvents.AgentResponseAudioTranscriptDeltaEvent,\n        )\n        self.assertEqual(server_event.response_id, \"resp_005\")\n        self.assertEqual(server_event.item_id, \"item_003\")\n        self.assertEqual(server_event.delta, \"Hello \")\n        self.assertEqual(server_event.agent_id, self.agent_id)\n        self.assertEqual(server_event.agent_name, self.agent_name)\n\n    async def test_model_response_audio_transcript_done_event(self) -> None:\n        \"\"\"Test converting ModelResponseAudioTranscriptDoneEvent.\"\"\"\n        model_event = ModelEvents.ModelResponseAudioTranscriptDoneEvent(\n            response_id=\"resp_006\",\n            item_id=\"item_004\",\n        )\n\n        server_event = ServerEvents.from_model_event(\n            model_event,\n            agent_id=self.agent_id,\n            agent_name=self.agent_name,\n        )\n\n        self.assertIsInstance(\n            server_event,\n            ServerEvents.AgentResponseAudioTranscriptDoneEvent,\n        )\n        self.assertEqual(server_event.response_id, \"resp_006\")\n        self.assertEqual(server_event.item_id, \"item_004\")\n        self.assertEqual(server_event.agent_id, self.agent_id)\n        self.assertEqual(server_event.agent_name, self.agent_name)\n\n    async def test_model_response_tool_use_delta_event(self) -> None:\n        \"\"\"Test converting ModelResponseToolUseDeltaEvent.\"\"\"\n        tool_use = ToolUseBlock(\n            type=\"tool_use\",\n            id=\"tool_001\",\n            name=\"get_weather\",\n            input={\"location\": \"San\"},\n        )\n\n        model_event = ModelEvents.ModelResponseToolUseDeltaEvent(\n            response_id=\"resp_007\",\n            item_id=\"item_005\",\n            tool_use=tool_use,\n        )\n\n        server_event = ServerEvents.from_model_event(\n            model_event,\n            agent_id=self.agent_id,\n            agent_name=self.agent_name,\n        )\n\n        self.assertIsInstance(\n            server_event,\n            ServerEvents.AgentResponseToolUseDeltaEvent,\n        )\n        self.assertEqual(server_event.response_id, \"resp_007\")\n        self.assertEqual(server_event.item_id, \"item_005\")\n        self.assertEqual(server_event.tool_use[\"id\"], \"tool_001\")\n        self.assertEqual(server_event.tool_use[\"name\"], \"get_weather\")\n        self.assertEqual(server_event.agent_id, self.agent_id)\n        self.assertEqual(server_event.agent_name, self.agent_name)\n\n    async def test_model_response_tool_use_done_event(self) -> None:\n        \"\"\"Test converting ModelResponseToolUseDoneEvent.\"\"\"\n        tool_use = ToolUseBlock(\n            type=\"tool_use\",\n            id=\"tool_002\",\n            name=\"get_weather\",\n            input={\"location\": \"San Francisco\"},\n        )\n\n        model_event = ModelEvents.ModelResponseToolUseDoneEvent(\n            response_id=\"resp_008\",\n            item_id=\"item_006\",\n            tool_use=tool_use,\n        )\n\n        server_event = ServerEvents.from_model_event(\n            model_event,\n            agent_id=self.agent_id,\n            agent_name=self.agent_name,\n        )\n\n        self.assertIsInstance(\n            server_event,\n            ServerEvents.AgentResponseToolUseDoneEvent,\n        )\n        self.assertEqual(server_event.response_id, \"resp_008\")\n        self.assertEqual(server_event.item_id, \"item_006\")\n        self.assertEqual(server_event.tool_use[\"id\"], \"tool_002\")\n        self.assertEqual(server_event.tool_use[\"name\"], \"get_weather\")\n        self.assertEqual(server_event.agent_id, self.agent_id)\n        self.assertEqual(server_event.agent_name, self.agent_name)\n\n    async def test_model_input_transcription_delta_event(self) -> None:\n        \"\"\"Test converting ModelInputTranscriptionDeltaEvent.\"\"\"\n        model_event = ModelEvents.ModelInputTranscriptionDeltaEvent(\n            item_id=\"item_007\",\n            delta=\"How are \",\n        )\n\n        server_event = ServerEvents.from_model_event(\n            model_event,\n            agent_id=self.agent_id,\n            agent_name=self.agent_name,\n        )\n\n        self.assertIsInstance(\n            server_event,\n            ServerEvents.AgentInputTranscriptionDeltaEvent,\n        )\n        self.assertEqual(server_event.item_id, \"item_007\")\n        self.assertEqual(server_event.delta, \"How are \")\n        self.assertEqual(server_event.agent_id, self.agent_id)\n        self.assertEqual(server_event.agent_name, self.agent_name)\n\n    async def test_model_input_transcription_done_event(self) -> None:\n        \"\"\"Test converting ModelInputTranscriptionDoneEvent.\"\"\"\n        model_event = ModelEvents.ModelInputTranscriptionDoneEvent(\n            transcript=\"How are you?\",\n            item_id=\"item_008\",\n            input_tokens=10,\n            output_tokens=5,\n        )\n\n        server_event = ServerEvents.from_model_event(\n            model_event,\n            agent_id=self.agent_id,\n            agent_name=self.agent_name,\n        )\n\n        self.assertIsInstance(\n            server_event,\n            ServerEvents.AgentInputTranscriptionDoneEvent,\n        )\n        self.assertEqual(server_event.transcript, \"How are you?\")\n        self.assertEqual(server_event.item_id, \"item_008\")\n        self.assertEqual(server_event.input_tokens, 10)\n        self.assertEqual(server_event.output_tokens, 5)\n        self.assertEqual(server_event.agent_id, self.agent_id)\n        self.assertEqual(server_event.agent_name, self.agent_name)\n\n    async def test_model_input_started_event(self) -> None:\n        \"\"\"Test converting ModelInputStartedEvent.\"\"\"\n        model_event = ModelEvents.ModelInputStartedEvent(\n            item_id=\"item_009\",\n            audio_start_ms=1000,\n        )\n\n        server_event = ServerEvents.from_model_event(\n            model_event,\n            agent_id=self.agent_id,\n            agent_name=self.agent_name,\n        )\n\n        self.assertIsInstance(\n            server_event,\n            ServerEvents.AgentInputStartedEvent,\n        )\n        self.assertEqual(server_event.item_id, \"item_009\")\n        self.assertEqual(server_event.audio_start_ms, 1000)\n        self.assertEqual(server_event.agent_id, self.agent_id)\n        self.assertEqual(server_event.agent_name, self.agent_name)\n\n    async def test_model_input_done_event(self) -> None:\n        \"\"\"Test converting ModelInputDoneEvent.\"\"\"\n        model_event = ModelEvents.ModelInputDoneEvent(\n            item_id=\"item_010\",\n            audio_end_ms=5000,\n        )\n\n        server_event = ServerEvents.from_model_event(\n            model_event,\n            agent_id=self.agent_id,\n            agent_name=self.agent_name,\n        )\n\n        self.assertIsInstance(\n            server_event,\n            ServerEvents.AgentInputDoneEvent,\n        )\n        self.assertEqual(server_event.item_id, \"item_010\")\n        self.assertEqual(server_event.audio_end_ms, 5000)\n        self.assertEqual(server_event.agent_id, self.agent_id)\n        self.assertEqual(server_event.agent_name, self.agent_name)\n\n    async def test_model_error_event(self) -> None:\n        \"\"\"Test converting ModelErrorEvent.\"\"\"\n        model_event = ModelEvents.ModelErrorEvent(\n            error_type=\"rate_limit_error\",\n            code=\"429\",\n            message=\"Rate limit exceeded\",\n        )\n\n        server_event = ServerEvents.from_model_event(\n            model_event,\n            agent_id=self.agent_id,\n            agent_name=self.agent_name,\n        )\n\n        self.assertIsInstance(\n            server_event,\n            ServerEvents.AgentErrorEvent,\n        )\n        self.assertEqual(server_event.error_type, \"rate_limit_error\")\n        self.assertEqual(server_event.code, \"429\")\n        self.assertEqual(server_event.message, \"Rate limit exceeded\")\n        self.assertEqual(server_event.agent_id, self.agent_id)\n        self.assertEqual(server_event.agent_name, self.agent_name)\n        self.assertEqual(server_event.type, \"agent_error\")\n\n\nclass TestClientEventsFromJson(IsolatedAsyncioTestCase):\n    \"\"\"Test ClientEvents.from_json method.\"\"\"\n\n    async def test_client_session_create_event(self) -> None:\n        \"\"\"Test parsing ClientSessionCreateEvent from JSON.\"\"\"\n        json_data = {\n            \"type\": \"client_session_create\",\n            \"config\": {\n                \"instructions\": \"You are a helpful assistant.\",\n                \"user_name\": \"TestUser\",\n            },\n        }\n\n        event = ClientEvents.from_json(json_data)\n\n        self.assertIsInstance(event, ClientEvents.ClientSessionCreateEvent)\n        self.assertEqual(event.type, \"client_session_create\")\n        self.assertEqual(\n            event.config[\"instructions\"],\n            \"You are a helpful assistant.\",\n        )\n        self.assertEqual(event.config[\"user_name\"], \"TestUser\")\n\n    async def test_client_session_end_event(self) -> None:\n        \"\"\"Test parsing ClientSessionEndEvent from JSON.\"\"\"\n        json_data = {\n            \"type\": \"client_session_end\",\n            \"session_id\": \"session_123\",\n        }\n\n        event = ClientEvents.from_json(json_data)\n\n        self.assertIsInstance(event, ClientEvents.ClientSessionEndEvent)\n        self.assertEqual(event.type, \"client_session_end\")\n        self.assertEqual(event.session_id, \"session_123\")\n\n    async def test_client_response_create_event(self) -> None:\n        \"\"\"Test parsing ClientResponseCreateEvent from JSON.\"\"\"\n        json_data = {\n            \"type\": \"client_response_create\",\n            \"session_id\": \"session_456\",\n        }\n\n        event = ClientEvents.from_json(json_data)\n\n        self.assertIsInstance(event, ClientEvents.ClientResponseCreateEvent)\n        self.assertEqual(event.type, \"client_response_create\")\n        self.assertEqual(event.session_id, \"session_456\")\n\n    async def test_client_response_cancel_event(self) -> None:\n        \"\"\"Test parsing ClientResponseCancelEvent from JSON.\"\"\"\n        json_data = {\n            \"type\": \"client_response_cancel\",\n            \"session_id\": \"session_789\",\n        }\n\n        event = ClientEvents.from_json(json_data)\n\n        self.assertIsInstance(event, ClientEvents.ClientResponseCancelEvent)\n        self.assertEqual(event.type, \"client_response_cancel\")\n        self.assertEqual(event.session_id, \"session_789\")\n\n    async def test_client_image_append_event(self) -> None:\n        \"\"\"Test parsing ClientImageAppendEvent from JSON.\"\"\"\n        json_data = {\n            \"type\": \"client_image_append\",\n            \"session_id\": \"session_001\",\n            \"image\": \"base64_image_data\",\n            \"format\": {\"mime_type\": \"image/png\"},\n        }\n\n        event = ClientEvents.from_json(json_data)\n\n        self.assertIsInstance(event, ClientEvents.ClientImageAppendEvent)\n        self.assertEqual(event.type, \"client_image_append\")\n        self.assertEqual(event.session_id, \"session_001\")\n        self.assertEqual(event.image, \"base64_image_data\")\n        self.assertEqual(event.format, {\"mime_type\": \"image/png\"})\n\n    async def test_client_text_append_event(self) -> None:\n        \"\"\"Test parsing ClientTextAppendEvent from JSON.\"\"\"\n        json_data = {\n            \"type\": \"client_text_append\",\n            \"session_id\": \"session_002\",\n            \"text\": \"Hello, how are you?\",\n        }\n\n        event = ClientEvents.from_json(json_data)\n\n        self.assertIsInstance(event, ClientEvents.ClientTextAppendEvent)\n        self.assertEqual(event.type, \"client_text_append\")\n        self.assertEqual(event.session_id, \"session_002\")\n        self.assertEqual(event.text, \"Hello, how are you?\")\n\n    async def test_client_audio_append_event(self) -> None:\n        \"\"\"Test parsing ClientAudioAppendEvent from JSON.\"\"\"\n        json_data = {\n            \"type\": \"client_audio_append\",\n            \"session_id\": \"session_003\",\n            \"audio\": \"base64_audio_data\",\n            \"format\": {\"type\": \"audio/pcm\", \"rate\": 16000},\n        }\n\n        event = ClientEvents.from_json(json_data)\n\n        self.assertIsInstance(event, ClientEvents.ClientAudioAppendEvent)\n        self.assertEqual(event.type, \"client_audio_append\")\n        self.assertEqual(event.session_id, \"session_003\")\n        self.assertEqual(event.audio, \"base64_audio_data\")\n        self.assertEqual(event.format.type, \"audio/pcm\")\n        self.assertEqual(event.format.rate, 16000)\n\n    async def test_client_audio_commit_event(self) -> None:\n        \"\"\"Test parsing ClientAudioCommitEvent from JSON.\"\"\"\n        json_data = {\n            \"type\": \"client_audio_commit\",\n            \"session_id\": \"session_004\",\n        }\n\n        event = ClientEvents.from_json(json_data)\n\n        self.assertIsInstance(event, ClientEvents.ClientAudioCommitEvent)\n        self.assertEqual(event.type, \"client_audio_commit\")\n        self.assertEqual(event.session_id, \"session_004\")\n\n    async def test_client_tool_result_event(self) -> None:\n        \"\"\"Test parsing ClientToolResultEvent from JSON.\"\"\"\n        json_data = {\n            \"type\": \"client_tool_result\",\n            \"session_id\": \"session_005\",\n            \"id\": \"tool_call_123\",\n            \"name\": \"get_weather\",\n            \"output\": \"The weather is sunny, 25°C\",\n        }\n\n        event = ClientEvents.from_json(json_data)\n\n        self.assertIsInstance(event, ClientEvents.ClientToolResultEvent)\n        self.assertEqual(event.type, \"client_tool_result\")\n        self.assertEqual(event.session_id, \"session_005\")\n        self.assertEqual(event.id, \"tool_call_123\")\n        self.assertEqual(event.name, \"get_weather\")\n        self.assertEqual(event.output, \"The weather is sunny, 25°C\")\n\n    async def test_invalid_json_data_no_type(self) -> None:\n        \"\"\"Test parsing invalid JSON data without type field.\"\"\"\n        json_data = {\n            \"session_id\": \"session_006\",\n        }\n\n        with self.assertRaises(ValueError) as context:\n            ClientEvents.from_json(json_data)\n\n        self.assertIn(\"Invalid JSON data\", str(context.exception))\n\n    async def test_invalid_json_data_not_dict(self) -> None:\n        \"\"\"Test parsing invalid JSON data that is not a dict.\"\"\"\n        json_data = \"not a dict\"\n\n        with self.assertRaises(ValueError) as context:\n            ClientEvents.from_json(json_data)\n\n        self.assertIn(\"Invalid JSON data\", str(context.exception))\n\n    async def test_unknown_event_type(self) -> None:\n        \"\"\"Test parsing JSON with unknown event type.\"\"\"\n        json_data = {\n            \"type\": \"unknown_event_type\",\n            \"session_id\": \"session_007\",\n        }\n\n        with self.assertRaises(ValueError) as context:\n            ClientEvents.from_json(json_data)\n\n        self.assertIn(\"Unknown ClientEvent type\", str(context.exception))\n"
  },
  {
    "path": "tests/realtime_gemini_test.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=protected-access\n\"\"\"Unit tests for Gemini Realtime Model class.\"\"\"\nimport json\nfrom unittest.async_case import IsolatedAsyncioTestCase\nfrom unittest.mock import AsyncMock\n\nfrom agentscope.realtime import GeminiRealtimeModel, ModelEvents\nfrom agentscope.message import (\n    AudioBlock,\n    TextBlock,\n    ImageBlock,\n    ToolResultBlock,\n    Base64Source,\n)\n\n\nclass TestGeminiRealtimeModelParseAPIMessage(IsolatedAsyncioTestCase):\n    \"\"\"Test parsing API messages from Gemini realtime model.\"\"\"\n\n    def setUp(self) -> None:\n        \"\"\"Set up test fixtures.\"\"\"\n        self.model = GeminiRealtimeModel(\n            model_name=\"gemini-2.5-flash-native-audio-preview\",\n            api_key=\"test_api_key\",\n            voice=\"Puck\",\n        )\n\n    async def test_parse_setup_complete_event(self) -> None:\n        \"\"\"Test parsing setupComplete event.\"\"\"\n        message = json.dumps({\"setupComplete\": {}})\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelSessionCreatedEvent)\n        self.assertEqual(event.session_id, \"gemini_session\")\n        self.assertEqual(event.type, \"model_session_created\")\n\n    async def test_parse_audio_delta_event(self) -> None:\n        \"\"\"Test parsing serverContent with audio data.\"\"\"\n        message = json.dumps(\n            {\n                \"serverContent\": {\n                    \"modelTurn\": {\n                        \"parts\": [\n                            {\n                                \"inlineData\": {\n                                    \"mimeType\": \"audio/pcm;rate=24000\",\n                                    \"data\": \"base64_audio_chunk\",\n                                },\n                            },\n                        ],\n                    },\n                },\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelResponseAudioDeltaEvent)\n        self.assertEqual(event.delta, \"base64_audio_chunk\")\n        self.assertEqual(event.format.type, \"audio/pcm\")\n        self.assertEqual(event.format.rate, 24000)\n        self.assertEqual(event.type, \"model_response_audio_delta\")\n\n    async def test_parse_output_transcription_event(self) -> None:\n        \"\"\"Test parsing serverContent with output transcription.\"\"\"\n        message = json.dumps(\n            {\n                \"serverContent\": {\n                    \"outputTranscription\": {\n                        \"text\": \"Hello there\",\n                    },\n                },\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(\n            event,\n            ModelEvents.ModelResponseAudioTranscriptDeltaEvent,\n        )\n        self.assertEqual(event.delta, \"Hello there\")\n\n    async def test_parse_input_transcription_event(self) -> None:\n        \"\"\"Test parsing serverContent with input transcription.\"\"\"\n        message = json.dumps(\n            {\n                \"serverContent\": {\n                    \"inputTranscription\": {\n                        \"text\": \"How are you?\",\n                    },\n                },\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(\n            event,\n            ModelEvents.ModelInputTranscriptionDoneEvent,\n        )\n        self.assertEqual(event.transcript, \"How are you?\")\n\n    async def test_parse_generation_complete_event(self) -> None:\n        \"\"\"Test parsing serverContent with generationComplete.\"\"\"\n        self.model._response_id = \"resp_123\"\n\n        message = json.dumps(\n            {\n                \"serverContent\": {\n                    \"generationComplete\": True,\n                },\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelResponseDoneEvent)\n        self.assertEqual(event.response_id, \"resp_123\")\n        self.assertIsNone(self.model._response_id)\n\n    async def test_parse_turn_complete_event(self) -> None:\n        \"\"\"Test parsing serverContent with turnComplete.\"\"\"\n        self.model._response_id = \"resp_456\"\n\n        message = json.dumps(\n            {\n                \"serverContent\": {\n                    \"turnComplete\": True,\n                },\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelResponseDoneEvent)\n        self.assertEqual(event.response_id, \"resp_456\")\n        self.assertIsNone(self.model._response_id)\n\n    async def test_parse_tool_call_event(self) -> None:\n        \"\"\"Test parsing toolCall event.\"\"\"\n        message = json.dumps(\n            {\n                \"toolCall\": {\n                    \"functionCalls\": [\n                        {\n                            \"name\": \"get_weather\",\n                            \"id\": \"call_789\",\n                            \"args\": {\"location\": \"San Francisco\"},\n                        },\n                    ],\n                },\n            },\n        )\n\n        events = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(events, list)\n        self.assertEqual(len(events), 1)\n        event = events[0]\n        self.assertIsInstance(\n            event,\n            ModelEvents.ModelResponseToolUseDoneEvent,\n        )\n        self.assertEqual(event.tool_use[\"name\"], \"get_weather\")\n        self.assertEqual(event.tool_use[\"id\"], \"call_789\")\n        self.assertEqual(\n            event.tool_use[\"input\"],\n            {\"location\": \"San Francisco\"},\n        )\n\n    async def test_parse_error_event(self) -> None:\n        \"\"\"Test parsing error event.\"\"\"\n        message = json.dumps(\n            {\n                \"error\": {\n                    \"status\": \"INVALID_ARGUMENT\",\n                    \"code\": 400,\n                    \"message\": \"Invalid request\",\n                },\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelErrorEvent)\n        self.assertEqual(event.error_type, \"INVALID_ARGUMENT\")\n        self.assertEqual(event.code, \"400\")\n        self.assertEqual(event.message, \"Invalid request\")\n        self.assertEqual(event.type, \"model_error\")\n\n\nclass TestGeminiRealtimeModelSend(IsolatedAsyncioTestCase):\n    \"\"\"Test sending data to Gemini realtime model.\"\"\"\n\n    def setUp(self) -> None:\n        \"\"\"Set up test fixtures.\"\"\"\n        from websockets import State\n\n        self.model = GeminiRealtimeModel(\n            model_name=\"gemini-2.5-flash-native-audio-preview\",\n            api_key=\"test_api_key\",\n            voice=\"Puck\",\n        )\n        self.mock_websocket = AsyncMock()\n        self.mock_websocket.state = State.OPEN\n        self.model._websocket = self.mock_websocket\n\n    async def test_send_audio_base64(self) -> None:\n        \"\"\"Test sending audio data with base64 source.\"\"\"\n        audio_data = AudioBlock(\n            type=\"audio\",\n            source=Base64Source(\n                type=\"base64\",\n                media_type=\"audio/wav\",\n                data=\"base64_encoded_audio_data\",\n            ),\n        )\n\n        await self.model.send(audio_data)\n\n        self.mock_websocket.send.assert_called_once()\n\n        sent_message = self.mock_websocket.send.call_args[0][0]\n        sent_data = json.loads(sent_message)\n\n        self.assertIn(\"realtimeInput\", sent_data)\n        self.assertEqual(\n            sent_data[\"realtimeInput\"][\"audio\"][\"data\"],\n            \"base64_encoded_audio_data\",\n        )\n\n    async def test_send_image_base64(self) -> None:\n        \"\"\"Test sending image data with base64 source.\"\"\"\n        image_data = ImageBlock(\n            type=\"image\",\n            source=Base64Source(\n                type=\"base64\",\n                media_type=\"image/jpeg\",\n                data=\"base64_encoded_image_data\",\n            ),\n        )\n\n        await self.model.send(image_data)\n\n        self.mock_websocket.send.assert_called_once()\n\n        sent_message = self.mock_websocket.send.call_args[0][0]\n        sent_data = json.loads(sent_message)\n\n        self.assertIn(\"realtimeInput\", sent_data)\n        self.assertEqual(\n            sent_data[\"realtimeInput\"][\"video\"][\"data\"],\n            \"base64_encoded_image_data\",\n        )\n\n    async def test_send_text(self) -> None:\n        \"\"\"Test sending text data.\"\"\"\n        text_data = TextBlock(\n            type=\"text\",\n            text=\"Hello, how are you?\",\n        )\n\n        await self.model.send(text_data)\n\n        self.mock_websocket.send.assert_called_once()\n\n        sent_message = self.mock_websocket.send.call_args[0][0]\n        sent_data = json.loads(sent_message)\n\n        self.assertIn(\"clientContent\", sent_data)\n        turns = sent_data[\"clientContent\"][\"turns\"]\n        self.assertEqual(turns[0][\"parts\"][0][\"text\"], \"Hello, how are you?\")\n\n    async def test_send_tool_result(self) -> None:\n        \"\"\"Test sending tool result data.\"\"\"\n        tool_result = ToolResultBlock(\n            type=\"tool_result\",\n            id=\"call_123\",\n            output=\"The weather is sunny\",\n            name=\"get_weather\",\n        )\n\n        await self.model.send(tool_result)\n\n        self.mock_websocket.send.assert_called_once()\n\n        sent_message = self.mock_websocket.send.call_args[0][0]\n        sent_data = json.loads(sent_message)\n\n        self.assertIn(\"toolResponse\", sent_data)\n        func_responses = sent_data[\"toolResponse\"][\"functionResponses\"]\n        self.assertEqual(len(func_responses), 1)\n        self.assertEqual(func_responses[0][\"id\"], \"call_123\")\n        self.assertEqual(func_responses[0][\"name\"], \"get_weather\")\n"
  },
  {
    "path": "tests/realtime_openai_test.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=protected-access\n\"\"\"Unit tests for OpenAI Realtime Model class.\"\"\"\nimport json\nfrom unittest.async_case import IsolatedAsyncioTestCase\nfrom unittest.mock import AsyncMock, patch\n\nfrom agentscope.realtime import OpenAIRealtimeModel, ModelEvents\nfrom agentscope.message import (\n    AudioBlock,\n    TextBlock,\n    ToolResultBlock,\n    Base64Source,\n    URLSource,\n)\n\n\nclass TestOpenAIRealtimeModelParseAPIMessage(IsolatedAsyncioTestCase):\n    \"\"\"Test parsing API messages from OpenAI realtime model.\"\"\"\n\n    def setUp(self) -> None:\n        \"\"\"Set up test fixtures.\"\"\"\n        self.model = OpenAIRealtimeModel(\n            model_name=\"gpt-4o-realtime-preview\",\n            api_key=\"test_api_key\",\n            voice=\"alloy\",\n        )\n\n    async def test_parse_session_created_event(self) -> None:\n        \"\"\"Test parsing session.created event.\"\"\"\n        message = json.dumps(\n            {\n                \"type\": \"session.created\",\n                \"session\": {\n                    \"id\": \"session_123\",\n                },\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelSessionCreatedEvent)\n        self.assertEqual(event.session_id, \"session_123\")\n        self.assertEqual(event.type, \"model_session_created\")\n\n    async def test_parse_response_created_event(self) -> None:\n        \"\"\"Test parsing response.created event.\"\"\"\n        message = json.dumps(\n            {\n                \"type\": \"response.created\",\n                \"response\": {\n                    \"id\": \"resp_456\",\n                },\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelResponseCreatedEvent)\n        self.assertEqual(event.response_id, \"resp_456\")\n        self.assertEqual(event.type, \"model_response_created\")\n        self.assertEqual(self.model._response_id, \"resp_456\")\n\n    async def test_parse_response_done_event(self) -> None:\n        \"\"\"Test parsing response.done event.\"\"\"\n        self.model._response_id = \"resp_789\"\n\n        message = json.dumps(\n            {\n                \"type\": \"response.done\",\n                \"response\": {\n                    \"id\": \"resp_789\",\n                    \"usage\": {\n                        \"input_tokens\": 100,\n                        \"output_tokens\": 50,\n                    },\n                },\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelResponseDoneEvent)\n        self.assertEqual(event.response_id, \"resp_789\")\n        self.assertEqual(event.input_tokens, 100)\n        self.assertEqual(event.output_tokens, 50)\n        self.assertEqual(event.type, \"model_response_done\")\n        self.assertEqual(self.model._response_id, \"\")\n\n    async def test_parse_response_audio_delta_event(self) -> None:\n        \"\"\"Test parsing response.output_audio.delta event.\"\"\"\n        self.model._response_id = \"resp_audio_1\"\n\n        message = json.dumps(\n            {\n                \"type\": \"response.output_audio.delta\",\n                \"item_id\": \"item_audio_1\",\n                \"delta\": \"base64_audio_data_chunk\",\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelResponseAudioDeltaEvent)\n        self.assertEqual(event.response_id, \"resp_audio_1\")\n        self.assertEqual(event.item_id, \"item_audio_1\")\n        self.assertEqual(event.delta, \"base64_audio_data_chunk\")\n        self.assertEqual(event.format.type, \"audio/pcm\")\n        self.assertEqual(event.format.rate, 24000)\n        self.assertEqual(event.type, \"model_response_audio_delta\")\n\n    async def test_parse_response_audio_done_event(self) -> None:\n        \"\"\"Test parsing response.output_audio.done event.\"\"\"\n        self.model._response_id = \"resp_audio_2\"\n\n        message = json.dumps(\n            {\n                \"type\": \"response.output_audio.done\",\n                \"item_id\": \"item_audio_2\",\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelResponseAudioDoneEvent)\n        self.assertEqual(event.response_id, \"resp_audio_2\")\n        self.assertEqual(event.item_id, \"item_audio_2\")\n        self.assertEqual(event.type, \"model_response_audio_done\")\n\n    async def test_parse_response_audio_transcript_delta_event(self) -> None:\n        \"\"\"Test parsing response.output_audio_transcript.delta event.\"\"\"\n        self.model._response_id = \"resp_transcript_1\"\n\n        message = json.dumps(\n            {\n                \"type\": \"response.output_audio_transcript.delta\",\n                \"item_id\": \"item_transcript_1\",\n                \"delta\": \"Hello \",\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(\n            event,\n            ModelEvents.ModelResponseAudioTranscriptDeltaEvent,\n        )\n        self.assertEqual(event.response_id, \"resp_transcript_1\")\n        self.assertEqual(event.item_id, \"item_transcript_1\")\n        self.assertEqual(event.delta, \"Hello \")\n        self.assertEqual(\n            event.type,\n            \"model_response_audio_transcript_delta\",\n        )\n\n    async def test_parse_response_audio_transcript_done_event(self) -> None:\n        \"\"\"Test parsing response.output_audio_transcript.done event.\"\"\"\n        self.model._response_id = \"resp_transcript_2\"\n\n        message = json.dumps(\n            {\n                \"type\": \"response.output_audio_transcript.done\",\n                \"item_id\": \"item_transcript_2\",\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(\n            event,\n            ModelEvents.ModelResponseAudioTranscriptDoneEvent,\n        )\n        self.assertEqual(event.response_id, \"resp_transcript_2\")\n        self.assertEqual(event.item_id, \"item_transcript_2\")\n        self.assertEqual(event.type, \"model_response_audio_transcript_done\")\n\n    async def test_parse_function_call_arguments_delta_event(self) -> None:\n        \"\"\"Test parsing response.function_call_arguments.delta event.\"\"\"\n        self.model._response_id = \"resp_tool_1\"\n\n        message = json.dumps(\n            {\n                \"type\": \"response.function_call_arguments.delta\",\n                \"call_id\": \"call_123\",\n                \"item_id\": \"item_tool_1\",\n                \"name\": \"get_weather\",\n                \"delta\": '{\"location\": \"San',\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(\n            event,\n            ModelEvents.ModelResponseToolUseDeltaEvent,\n        )\n        self.assertEqual(event.response_id, \"resp_tool_1\")\n        self.assertEqual(event.item_id, \"item_tool_1\")\n        self.assertEqual(event.tool_use[\"id\"], \"call_123\")\n        self.assertEqual(event.tool_use[\"name\"], \"get_weather\")\n\n    async def test_parse_function_call_arguments_done_event(self) -> None:\n        \"\"\"Test parsing response.function_call_arguments.done event.\"\"\"\n        self.model._response_id = \"resp_tool_2\"\n        self.model._tool_args_accumulator[\n            \"call_456\"\n        ] = '{\"location\": \"San Francisco\"}'\n\n        message = json.dumps(\n            {\n                \"type\": \"response.function_call_arguments.done\",\n                \"call_id\": \"call_456\",\n                \"item_id\": \"item_tool_2\",\n                \"name\": \"get_weather\",\n                \"arguments\": '{\"location\": \"San Francisco\"}',\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(\n            event,\n            ModelEvents.ModelResponseToolUseDoneEvent,\n        )\n        self.assertEqual(event.response_id, \"resp_tool_2\")\n        self.assertEqual(event.item_id, \"item_tool_2\")\n        self.assertEqual(event.tool_use[\"id\"], \"call_456\")\n        self.assertEqual(event.tool_use[\"name\"], \"get_weather\")\n        self.assertEqual(\n            event.tool_use[\"input\"],\n            {\"location\": \"San Francisco\"},\n        )\n        self.assertNotIn(\"call_456\", self.model._tool_args_accumulator)\n\n    async def test_parse_input_audio_transcription_completed_event(\n        self,\n    ) -> None:\n        \"\"\"Test parsing\n        conversation.item.input_audio_transcription.completed event.\"\"\"\n        message = json.dumps(\n            {\n                \"type\": \"conversation.item.input_audio_transcription.\"\n                \"completed\",\n                \"item_id\": \"item_input_1\",\n                \"transcript\": \"Hello world\",\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(\n            event,\n            ModelEvents.ModelInputTranscriptionDoneEvent,\n        )\n        self.assertEqual(event.item_id, \"item_input_1\")\n        self.assertEqual(event.transcript, \"Hello world\")\n        self.assertEqual(event.type, \"model_input_transcription_done\")\n\n    async def test_parse_input_audio_buffer_speech_started_event(\n        self,\n    ) -> None:\n        \"\"\"Test parsing input_audio_buffer.speech_started event.\"\"\"\n        message = json.dumps(\n            {\n                \"type\": \"input_audio_buffer.speech_started\",\n                \"item_id\": \"item_vad_1\",\n                \"audio_start_ms\": 1000,\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelInputStartedEvent)\n        self.assertEqual(event.item_id, \"item_vad_1\")\n        self.assertEqual(event.audio_start_ms, 1000)\n        self.assertEqual(event.type, \"model_input_started\")\n\n    async def test_parse_input_audio_buffer_speech_stopped_event(\n        self,\n    ) -> None:\n        \"\"\"Test parsing input_audio_buffer.speech_stopped event.\"\"\"\n        message = json.dumps(\n            {\n                \"type\": \"input_audio_buffer.speech_stopped\",\n                \"item_id\": \"item_vad_2\",\n                \"audio_end_ms\": 5000,\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelInputDoneEvent)\n        self.assertEqual(event.item_id, \"item_vad_2\")\n        self.assertEqual(event.audio_end_ms, 5000)\n        self.assertEqual(event.type, \"model_input_done\")\n\n    async def test_parse_error_event(self) -> None:\n        \"\"\"Test parsing error event.\"\"\"\n        message = json.dumps(\n            {\n                \"type\": \"error\",\n                \"error\": {\n                    \"type\": \"invalid_request\",\n                    \"code\": \"400\",\n                    \"message\": \"Invalid request format\",\n                },\n            },\n        )\n\n        event = await self.model.parse_api_message(message)\n\n        self.assertIsInstance(event, ModelEvents.ModelErrorEvent)\n        self.assertEqual(event.error_type, \"invalid_request\")\n        self.assertEqual(event.code, \"400\")\n        self.assertEqual(event.message, \"Invalid request format\")\n        self.assertEqual(event.type, \"model_error\")\n\n\nclass TestOpenAIRealtimeModelSend(IsolatedAsyncioTestCase):\n    \"\"\"Test sending data to OpenAI realtime model.\"\"\"\n\n    def setUp(self) -> None:\n        \"\"\"Set up test fixtures.\"\"\"\n        from websockets import State\n\n        self.model = OpenAIRealtimeModel(\n            model_name=\"gpt-4o-realtime-preview\",\n            api_key=\"test_api_key\",\n            voice=\"alloy\",\n        )\n        self.mock_websocket = AsyncMock()\n        self.mock_websocket.state = State.OPEN\n        self.model._websocket = self.mock_websocket\n\n    async def test_send_audio_base64(self) -> None:\n        \"\"\"Test sending audio data with base64 source.\"\"\"\n        audio_data = AudioBlock(\n            type=\"audio\",\n            source=Base64Source(\n                type=\"base64\",\n                media_type=\"audio/wav\",\n                data=\"base64_encoded_audio_data\",\n            ),\n        )\n\n        await self.model.send(audio_data)\n\n        self.mock_websocket.send.assert_called_once()\n\n        sent_message = self.mock_websocket.send.call_args[0][0]\n        sent_data = json.loads(sent_message)\n\n        self.assertEqual(sent_data[\"type\"], \"input_audio_buffer.append\")\n        self.assertEqual(sent_data[\"audio\"], \"base64_encoded_audio_data\")\n\n    async def test_send_text(self) -> None:\n        \"\"\"Test sending text data.\"\"\"\n        text_data = TextBlock(\n            type=\"text\",\n            text=\"Hello, how are you?\",\n        )\n\n        await self.model.send(text_data)\n\n        self.mock_websocket.send.assert_called_once()\n\n        sent_message = self.mock_websocket.send.call_args[0][0]\n        sent_data = json.loads(sent_message)\n\n        self.assertEqual(sent_data[\"type\"], \"conversation.item.create\")\n        self.assertEqual(sent_data[\"item\"][\"type\"], \"message\")\n        self.assertEqual(sent_data[\"item\"][\"role\"], \"user\")\n        self.assertEqual(\n            sent_data[\"item\"][\"content\"][0][\"text\"],\n            \"Hello, how are you?\",\n        )\n\n    async def test_send_tool_result(self) -> None:\n        \"\"\"Test sending tool result data.\"\"\"\n        tool_result = ToolResultBlock(\n            type=\"tool_result\",\n            id=\"call_123\",\n            output=\"The weather is sunny, 25°C\",\n            name=\"get_weather\",\n        )\n\n        await self.model.send(tool_result)\n\n        self.mock_websocket.send.assert_called_once()\n\n        sent_message = self.mock_websocket.send.call_args[0][0]\n        sent_data = json.loads(sent_message)\n\n        self.assertEqual(sent_data[\"type\"], \"conversation.item.create\")\n        self.assertEqual(sent_data[\"item\"][\"type\"], \"function_call_output\")\n        self.assertEqual(sent_data[\"item\"][\"call_id\"], \"call_123\")\n        self.assertEqual(\n            sent_data[\"item\"][\"output\"],\n            \"The weather is sunny, 25°C\",\n        )\n\n    async def test_send_audio_url(self) -> None:\n        \"\"\"Test sending audio data with URL source.\"\"\"\n        audio_data = AudioBlock(\n            type=\"audio\",\n            source=URLSource(\n                type=\"url\",\n                url=\"https://example.com/audio.wav\",\n            ),\n        )\n\n        with patch(\n            \"agentscope.realtime._openai_realtime_model.\"\n            \"_get_bytes_from_web_url\",\n        ) as mock_get_bytes:\n            mock_get_bytes.return_value = \"fetched_audio_bytes\"\n\n            await self.model.send(audio_data)\n\n            mock_get_bytes.assert_called_once_with(\n                \"https://example.com/audio.wav\",\n            )\n\n            self.mock_websocket.send.assert_called_once()\n\n            sent_message = self.mock_websocket.send.call_args[0][0]\n            sent_data = json.loads(sent_message)\n\n            self.assertEqual(sent_data[\"type\"], \"input_audio_buffer.append\")\n            self.assertEqual(sent_data[\"audio\"], \"fetched_audio_bytes\")\n"
  },
  {
    "path": "tests/session_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Session module tests.\"\"\"\nimport os\nfrom typing import Union\nfrom unittest import IsolatedAsyncioTestCase\n\nfrom agentscope.agent import ReActAgent, AgentBase\nfrom agentscope.formatter import DashScopeChatFormatter\nfrom agentscope.memory import InMemoryMemory\nfrom agentscope.message import Msg\nfrom agentscope.model import DashScopeChatModel\nfrom agentscope.session import JSONSession, RedisSession\nfrom agentscope.tool import Toolkit\n\n\nclass MyAgent(AgentBase):\n    \"\"\"Test agent class.\"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"Initialize the test agent.\"\"\"\n        super().__init__()\n        self.name = \"Friday\"\n        self.sys_prompt = \"A helpful assistant.\"\n        self.memory = InMemoryMemory()\n\n        self.register_state(\"name\")\n        self.register_state(\"sys_prompt\")\n\n    async def reply(self, msg: Msg) -> None:\n        \"\"\"Reply to the message.\"\"\"\n\n    async def observe(self, msg: Msg) -> None:\n        \"\"\"Observe the message.\"\"\"\n        await self.memory.add(msg)\n\n    async def handle_interrupt(\n        self,\n        msg: Union[Msg, list[Msg], None] = None,\n    ) -> Msg:\n        \"\"\"Handle interrupt.\"\"\"\n\n\nclass SessionTest(IsolatedAsyncioTestCase):\n    \"\"\"Test cases for the session module.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the test case.\"\"\"\n        session_file = \"./user_1.json\"\n        if os.path.exists(session_file):\n            os.remove(session_file)\n\n    async def test_session_base(self) -> None:\n        \"\"\"Test the SessionBase class.\"\"\"\n        session = JSONSession(\n            save_dir=\"./\",\n        )\n\n        agent1 = ReActAgent(\n            name=\"Friday\",\n            sys_prompt=\"A helpful assistant.\",\n            model=DashScopeChatModel(api_key=\"xxx\", model_name=\"qwen_max\"),\n            formatter=DashScopeChatFormatter(),\n            toolkit=Toolkit(),\n            memory=InMemoryMemory(),\n        )\n        agent2 = MyAgent()\n\n        await agent2.memory.add(\n            Msg(\n                \"Alice\",\n                \"Hi!\",\n                \"user\",\n            ),\n        )\n\n        await session.save_session_state(\n            session_id=\"user_1\",\n            agent1=agent1,\n            agent2=agent2,\n        )\n\n        # Mutate local state to verify load really works\n        agent1.name = \"Changed\"\n        agent2.sys_prompt = \"Changed prompt\"\n\n        # Load back\n        await session.load_session_state(\n            session_id=\"user_1\",\n            agent1=agent1,\n            agent2=agent2,\n        )\n\n        self.assertEqual(agent1.name, \"Friday\")\n        self.assertEqual(agent2.sys_prompt, \"A helpful assistant.\")\n\n    async def asyncTearDown(self) -> None:\n        \"\"\"Clean up after the test.\"\"\"\n        # Remove the session file if it exists\n        session_file = \"./user_1.json\"\n        if os.path.exists(session_file):\n            os.remove(session_file)\n\n\nclass RedisSessionTest(IsolatedAsyncioTestCase):\n    \"\"\"Test cases for the redis session module (with fake redis).\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        # Use fakeredis (async)\n        try:\n            import fakeredis.aioredis  # type: ignore\n        except ImportError as e:\n            raise ImportError(\n                \"fakeredis is required for this test. \"\n                \"Please install it via `pip install fakeredis`.\",\n            ) from e\n\n        self._redis = fakeredis.aioredis.FakeRedis()\n        self.session = RedisSession(\n            connection_pool=self._redis.connection_pool,\n        )\n\n    async def test_redis_session_save_and_load(self) -> None:\n        \"\"\"Test the RedisSession class.\"\"\"\n        agent1 = ReActAgent(\n            name=\"Friday\",\n            sys_prompt=\"A helpful assistant.\",\n            model=DashScopeChatModel(api_key=\"xxx\", model_name=\"qwen_max\"),\n            formatter=DashScopeChatFormatter(),\n            toolkit=Toolkit(),\n            memory=InMemoryMemory(),\n        )\n        agent2 = MyAgent()\n\n        await agent2.memory.add(Msg(\"Alice\", \"Hi!\", \"user\"))\n\n        # Save\n        await self.session.save_session_state(\n            session_id=\"user_1\",\n            agent1=agent1,\n            agent2=agent2,\n        )\n\n        # Mutate local state to verify load really works\n        agent1.name = \"Changed\"\n        agent2.sys_prompt = \"Changed prompt\"\n\n        # Load back\n        await self.session.load_session_state(\n            session_id=\"user_1\",\n            agent1=agent1,\n            agent2=agent2,\n        )\n\n        self.assertEqual(agent1.name, \"Friday\")\n        self.assertEqual(agent2.sys_prompt, \"A helpful assistant.\")\n\n    async def asyncTearDown(self) -> None:\n        # close clients\n        await self.session.close()\n        await self._redis.close()\n"
  },
  {
    "path": "tests/token_anthropic_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The unittests for huggingface token counter.\"\"\"\nimport os\nfrom unittest.async_case import IsolatedAsyncioTestCase\n\nfrom agentscope.token import AnthropicTokenCounter\n\n\nclass AnthropicTokenCounterTest(IsolatedAsyncioTestCase):\n    \"\"\"The unittests for the Anthropic token counter.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the test case.\"\"\"\n        self.messages = [\n            {\n                \"role\": \"system\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"You're a helpful assistant.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"What is the capital of France?\",\n                    },\n                    {\n                        \"type\": \"image\",\n                        \"source\": {\n                            \"type\": \"base64\",\n                            \"media_type\": \"image/png\",\n                            \"data\": \"BhdWRpbyBjb250ZW50\",\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"The capital of France is Paris.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"What is the capital of Japan?\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"tool_use\",\n                        \"name\": \"get_capital\",\n                        \"input\": {\n                            \"country\": \"Japan\",\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"tool_result\",\n                        \"tool_use_id\": \"1\",\n                        \"content\": [\n                            {\n                                \"type\": \"text\",\n                                \"text\": \"The capital of Japan is Tokyo.\",\n                            },\n                        ],\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"The capital of Japan is Tokyo.\",\n                    },\n                ],\n            },\n        ]\n\n    async def test_anthropic_token_counter(self) -> None:\n        \"\"\"Test the HuggingFace token counter.\"\"\"\n\n        if os.environ.get(\"ANTHROPIC_API_KEY\"):\n            counter = AnthropicTokenCounter(\n                api_key=os.environ.get(\"ANTHROPIC_API_KEY\"),\n                model_name=\"claude-sonnet-4-20250514\",\n            )\n\n            res = await counter.count(self.messages)\n            self.assertEqual(res, 49)\n"
  },
  {
    "path": "tests/token_char_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The unittests for char-based token counter.\"\"\"\nfrom unittest.async_case import IsolatedAsyncioTestCase\n\n\nclass CharTokenCounterTest(IsolatedAsyncioTestCase):\n    \"\"\"The unittests for char-based token counter.\"\"\"\n\n    async def test_count_tokens(self) -> None:\n        \"\"\"Test the count_tokens method.\"\"\"\n        from agentscope.token import CharTokenCounter\n\n        counter = CharTokenCounter()\n\n        messages = [\n            {\n                \"role\": \"user\",\n                \"content\": \"This is a test string.\",\n            },\n            {\n                \"id\": \"1\",\n                \"name\": \"test_tool\",\n                \"type\": \"tool_use\",\n                \"input\": {\n                    \"param1\": \"value1\",\n                    \"param2\": \"value2\",\n                },\n            },\n        ]\n        num_tokens = await counter.count(messages)\n        self.assertEqual(num_tokens, 157)\n\n        messages = []\n        num_tokens = await counter.count(messages)\n        self.assertEqual(num_tokens, 0)\n"
  },
  {
    "path": "tests/token_openai_test.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=line-too-long\n# flake8: noqa: E501\n\"\"\"The unittests for OpenAI token counter.\"\"\"\nimport os\nfrom unittest.async_case import IsolatedAsyncioTestCase\n\nfrom agentscope.token import OpenAITokenCounter\n\n\nclass OpenAITokenCounterTest(IsolatedAsyncioTestCase):\n    \"\"\"The unittests for the OpenAI token counter.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the test case.\"\"\"\n        self.image_path = os.path.abspath(\"./image.png\")\n        self.messages = [\n            {\n                \"role\": \"system\",\n                \"name\": \"system\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"You're a helpful assistant.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"name\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"What is the capital of France?\",\n                    },\n                    {\n                        \"type\": \"image_url\",\n                        \"image_url\": {\n                            \"url\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAiYAAAImCAYAAABnzkFGAAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAACAASURBVHic7b1tsyTJdd93smcGAGkGlpfmo4ILygTC4sNGGKTliAUwwFQDixUAkRQoCgzTIVqSCX+Oe++X8BsvLJJ2mAyJFB2mYIjcgW6Nd4mZFw7ILwhYdoAOCkQIoEDjkgAI7GL2dvpFd9XNOnWyKqs6q7u6+/eLmLmVeatP/yv74Z7816ksEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgLng9i0AAABG8HtPi2pzsZJCZCEiK6l+rlZSth7zsXvtPoCZQWICsCMWj/2Fd/KgansvlyIi8i5X7ksTHAC/97RYyKIQEfHOPRDxhfjIvrq/d7/Ne8+Hv7pZvy9JYmBPkJgATMwmITmP/d55uVy9y13sUBLMld97WiwWi0JExIuci/fS+Jqu25uful1lGF6MtkFfIuPWrotb+Ue1A/MrJCwwLSQmABPSl5RUkJycKL//tFisFkV+J2Tsfj2JT9V2vhRZiLu5ebQSEhbIC4kJwESkJiUVJCcnQp2MGO+NvsSilWh0JBAullh0xUt9XrO/dN6vExWSFNgCEhOAiXBP9F+Bfvzzjs/kEbL4/ZsLkc3pmeRTMQF7c1JSTh3pfhHxUq67/SVJCgyFL0GACRjqllTgmhwRv78uWl0nIworUXBW/+jEwCbFkTF1jN2vOvUjpReSFEjj7r4FABwjY5ISOA4Wn7y58N49ECeFj51KqaaE1R9yJ5H+TYd3zbZE+uu2ilPRil/pqn5X15BII+Fxlv6q3zgOrV+kcOIK+Z/fuE1SRKhLARMcE4DMjHVLKjidc4D8/vpqGu/lPMlJiPUPftwQJyVSY5LVIenTq9rOl/5GLuVXSVDgFr4AATIzprak8XhO5xwMi09WtSMukpDM9FRMcrwhCY/xxKl6RUp/40lQQERITADy8tgXzsnVtmFwTebN4pM3a1ds8hoN/cwpicGAq3B27pj06i29x0E5dfjyA8iIe+KvRKTYOg6uySxZfOrmwovrSEiGOAsTXIWzdaIxH4fHeX+5+tV7F5HIcMSQmADkIpNbsqH0z7tlpliwJbcJycgajVj/1o+7PReybg5JhDIlFmMeF93P0O9c6T2neU4JEhOATORySyq8lyX30dkvi0/dXHhnOCQVyTUa4Q6pNRoHeSrGaHfFS31eEREhQTkRSEwAcpDXLRERTufsk8WnwgXRAiZ3DPr22zIx2Lle3R54qstIbJwIp3iOHBITgAzkdksqKILdPbVLEpL1VErmGo0xj4vuZ55KSXR4dqq39IJ7cqzwpQewLRO4JRW4Jrtj8Qc3F973LBk/5NTEFOt+tPYb6EB0FdvuRG9Mf9BOSYS8rPcTX/pfvUct1pFBYgKwJVO5JRW4JtOy+INNYWtItj/IQxKGxJqSVB1Za0qswH2JW/Yak+h+buUvV/+I0zvHAl94ANswoVtSgWsyHbdJyaHVaFTN4TUau9Xbt19Gh8dJ6d+QS/knnN45dBb7FgBwyDjuiXOQLP7g5sL94crfOiWbP/bObTbDttu03W071l/FqvcR1W+1g39y+yu7reaSpl5Df1Rv33FF9Df6E/RH94vob/RH9Op+7wp3x10tfuPphfHMcEDgmACMZQduSQWnc/Kx+IOguHXUDD5W82DF62nn3i+pRsOp/fap13pAn94ER8vJ+h48uCcHCY4JwEh26ZYsHvuLXT3X0fKyL9wfrq4aV9x0zeC1OxB1JiIzeLOtYop6LrMd6tN6g3b4mBQnpe4f4qQk6LUcoahe9S9Jr97P1I97csAwCwMYww7dkgpck/HcuiQpM/COGXlFTscg1WlxkqhXOzz71Kv7e2pkuhypkXqdEwpjDwy+6ABGMPWVOOZzUgQ7Cvfy6va14lRKu91IICp9uj3wVMrO9Pbtt9lwnlM7BwSncgCG8tgXsuOkBEbwsi/cyysvIkX8D6Fv/lGr21WnN/p9u13/k+CP9WYf3W7t13x4J3q/1uMsvR3HVR1LfVzSbvce1zZ6dX+KXr2ffg2C16h6zErWDuc/fVr0i4Z9Q2ICMJB9XYnjuQIomcXLNxeNU226rqHud3Y7paakaqfUaLQeI901G43+UF+knVKjET53X42GvkKnr8Yk1t+rVx9Xil5Df2KtjHNytfinTy8EZg2JCcAQ9uyWUATbz+Jl407A4cS7yyGp2qZD4u12rN/ar4ppOSlhu9Ef6o3o73RI9HMn6PXqd/V2h/7ocRn6+xwSPVYxh6RXb1u/d3JOcjJvqDEBGMA+aks0FMHGWbys7nPjIzvq/rH7eempedjskFSjYdRs7Fyv7rf0drTrBGLHeqP7deot/T++u4wogz3CFxxAKnu4EseCIlgb9zAocq1I+YPW6kj9A2wkElsnAkP16v5KlyWgLzHa5nkzP67uH5IIdSRGHc/rRZYUxc4LTuUAJMIqrzPlZV+4T6+8OFesOyas0UhdR6O3RkO1G3pz1GgE7Yw1Gi0Bpl5D/+DXoepPeR1cpG3p1ccl4pyj7mRmuP5dAGAubkmF97KUd7ly3zr2zsu+cAt/NXwGrttdM/OueAPb2fdLcXgm0muEH+/wpDhSuj1Qb89+TuRy9U/uXUT2hB2CYwKQwNzckgWXK4tcbZISke4ZuTkDV+2kq3DSZuDDV3pthzWPw9yvywkxHpBzpdeWo6PbKccV0a8dHasdc6cax6X0d7wOXhxFsTPB9e8CcOLMzC2pOOki2JeDpERkxzP42Ix+oI4seiP6U2tlhtSYTKJX9/c4WmYRcV69Dudk7+CYAPQwV3fiZC8dvvKFu+Ov4jN4PSMOHptlBq/baqbe6Uyk6DX0R/Ua+ruOw1pLJbXGpKFPt3XMsD9Bf93v1H6RsepztHpfB6U/iO3FnbMQ236x3hoAEOCepCxxuRdK/7xb7lvETrnyhfNDakr69kt0FoZchZOqw0X6B8XL4PCk6p1kvxEOT+q4bTm+3nG1zr7AMQHoYOauRDFzfXmpkpLWTF3UzFdUv9EOHQYJHpvkmPQ4C4NrNPR+XXr1caXojejPXKPR0J/6OiTpVe0kJ6T6Kao/ot94PzlxLGG/J0hMADpgGfiZUCclIrJZfMKc/nrVrdux/th+dWdVy6CeO3avFmulVB2r1qvb2+jVjzP0tjR5tV94TCl69X7b6NWPMzT0HVfjdRBpvD6mXt0vddt57q+zD1z/LgCnyeKxvziExOQUimDdVbWeeOXRV6ScAlC797V3dKqgU7956kW3U+Ln1qvbXeMeae90fPv2S9HrxIvntM4OwTEBiHAISYnI7E83bY27Wl21HYhglt93Dxf9T8JtYwYexmg4Iqodu0+LdLRb/RH94c9ar57RK/36OGLHtZXeiH4feS7rdQmThCS9er9t9Op/KXq9uBXOyS4hMQEwOKQ/9oeSQI1hcXVzIeKKztqG2kDpqNGIbnfVMlT9Tu1n1Dak1miEsftqTGLbMf2x42i1E2s0kvXq5+rSq0jSq/t79A+pKXFi7Gfr57TO7iAxATDwTh7sW8MQDimRSmVxtblLcFdtQNgMHRJRu1rbYdvcSbpn9GGg6Ezd0huZmbeeO0WvIb/LkTCPw9AfBupzpFr7baNXPy4y7lp/eHyDa0p0O47zxzsJmBMkJgCax76Qma5dcjJc+cK7xXl8Bj9kRizmDLh7Bq/buWbwkRl9lxPRqbfnuEy9HfqHXnVkOg6G/kZ/gv6639Kr2rkdLensLxafeHohMCnWWx3gpHFP/JUcYGJyNEWwV75wEqzqKhKffet+q+269tvssClyTCqGHKMjm17dXz1AtyP6Gy6Tbu9Cb1/b0tv3ugyIn2k/J3K5+rV7F5FHwZbgmACEHLBbciync5yT861rNBqzbrXdaKuYk9VoVP0d+lsOTyBY6w8Ga7zDo9vBdkuvauuYXcfV0KuOo9W29Pa0hzg8pj6j3ehv6/Ui5zgn02G91QFOlkN1SyoO3TVZXN1ceOfOp5rpDt7vQGbw8cel6NX9+9Sr+1P0duifWK//tXsH/XmbKzgmABUH7JZUHLRrEtaVTFKjsYMZ/NAaDVPv8Bl8q133p+jV/V169XGl6E3QHz2uFL2G/iGOViig73Vo6HXi/oenzVOOkAUSE4AN7ogvuz0EnPNXt1dTBFdXDF1Ho7W+idhtCfbtu/pjynU0Wno72v5WWrreDv2D9erHdeiNHoehv+5PeR286h+iVx+n3La7xl3rvx1jimEnwJhCAJwgj33hnFztW0YODvF0zuKRv/Ai7VM4x3wqJVpsu0+9ur/SpdpJp1KC12Nneg2Zjf4e/bH3U8/zOophs4JjAiDH5ZYc3OmcK19478+j62iEP/tmtBJsNmbmkee29vN6B4nP4E2t4czc0BvGMI9jG726P6I/9tNypKxj7DquUMRgvRH9nXpVO3RFwu2o3r73V79eL3Iu/z2Lr+WCxATgCGpLQg5tJVh3J7wKJ7U2oNqW5s91RNWW2zqEaC2DasdqHMy2U/1deiM1Dpb+KWo0wtjR4+rRHtM/uEbD0mvoT30d6ufqed+E77EkvZH3U6hXZP0+hiyQmMDJc0xuScWhuCaLRzcX4n2RXlOi9qtmuJ1Oiqj9RPWHbRVzkGOSordjBj+qRkP9S9Hb6u/Rm1Sj0aXX0B99HVL0RvTHtmPtZL2x95M0od4kG65/F4Aj5ohqSxSlf94t9y2ik1d84VZq7PWXvXgRq8YhqUZDtzueJ7Jb736DH2fp7WjXf4D3oNdZ/Sl6df8+9cb0qwdmrFly1JtsDY4JnDTH6JZsKDanqGbL+r4jwey0/hnObEXNdIN9e2s0jL8a0Zl6bL/ITD32MzYzbzxRZGZuxRysN6K/8YuO44rVl2iHJPbTOq5Bensw9QbPZR5X+DqETxbTq49rmF4vR/udsjNITOB0ObLaEs1izsf2SjX2wfl8ke7agKOu0RhS8yCqv0d/o79Dfx3Tqf0M/dHajYj+zuNS+s3jMPSHMfpqgJJrT1L06ueWFpzS2Q5jSAFOg0Nf5TWFuV467P731ZX4YOyTrfhIwNH7VU8ctKOniLRAJ61TRZPrNdoNWZZe13NcO9Tbu1/XuCfoz6039X1p4DilMxocEzhNjtwtqZhlEewrvhBxhX2VRGwGPmRGK8120gxcP6ZnBm45JJ0zcNehNzYDd9LWa/xrHFeH/uhxGfpTrsJxKnb0uAKN+jhabbVz5+sgCXoN/UOuwmmNb6RtwCmd8ZCYwElyxLUlDeZ46XC8tkSkXaMhzXa1b2dtgNz2h09Rbet2bDusWejU26G/1uc79OqYovaTNsk1Dyl69X6+fRxdNSYpNRq6GdWr5Xv1OEuv7tda1euR8jro91OqXgWndMZBYgKnx4m4JRWzck10bUnuGXyXExHO4KOOg27PbAbf0hdp1/0penV/h94pHK2GXn1czm4n1ZSIobfvuCy9RrvR3z6MCs9diEdBYgInx6m4JXPEeb8Z+5hDEmmnrKPRu55J4EAYpka/k2D0D9Zr6M+xjobWX/f3OCRD9fY5Wlpv9VymfsOCaB1XRL/lkGzzOvQ5Wrrd6G8fhjqk8+49QNOR6wEcIce7bkknsyiCfcUXzkfGXn+5x77sU4ohW0WeXfHG6hi7X6UraCcX2+5Dr7WDC/ojr0Ojf496I8PY0t/1fqoxjqdP3+0jKYQdAI4JnBSn6pbM4XSOE6O2pNOBqPoTZrQ6ZmM/Q0xrZh7Zr/W4yAy+87gM/eETZl5Ho6lXPy5Fr9YQ/Kv16baK2ejfRq9+XNf7xjiuauxjjomp19DfiNmlL3pY3EtnACQmcDqcWG1JyN6LYKvakkE1GlV/V22ARGoepNnuq3GI1QpEax6U/pSah6TaE0tvX7tLb0R/yutQ93fp1ceVolf3R/Q641/n+ybW36VXH5fY7c7jkiQWi9P87hkDiQmcDKfqllTs0zVx3p/Xs9CoQxLMaJNrA6R7Bh8+Z/1TpDUjrme+yjGYvEZDty29CTP6+lcpenX/Nnr1P0tfpF333x7GbYzgNYq+bwz9g/Xqx4l6nKVXH5ck4SmETYbEBE6JYt8CTpJXfCHOFXlntDJuRtuagat+0zHQeg39Q67C6dWrXYAuvTpkil7dn6C/5UjpdqDfcnSienW/s9upjpQ4SXsdXDt2lyMV1avaPXgKYZMgMYGTYA41Fvtmv6dzEh2Ivpmu6H3VT3NmHpHjVdvcT8UYVFMSPpFvP6bLSZCwf4heYz+vdxBxq9Wll9VSxJedx6X113qDdu9xbaNX93e9b4Lnbuyn3keNbUuv/jlCbwe4Jv0MyPUADhf3JFZOf1o4L5erd7mLnT7nq765/LxI9x+qvrYL+zcdrbaX+NUutTLVHqBjtN6+/Sy9He36D3CqXld6ubmUX7xX1n2/97Rw/s65iC+G69X9KXp1f5fesTrG7pei19AfixfBcZVOJzgmcPTgltyyc9fkFV+sk5LITHdobUBr1iuRdvBcrRoNsdtyG+a239Kr+4fo7dmvitnQF2nX/aHeiH7vxXm59L+4WDaSEhGRX7xX+r+/WPqb1VKcL4fp1dspevV+hv66v+t9o2LX/UP0duhPfR0aeiEHOCZw9OCWNNmpa/LKZt0Y/QrEXpG+/eqZarhDZEbrDEckl45kvX2Ps/Tmd3icl8vVL965iKhpsfjdmwsf1kNY8ZMdnkB/nUD06O963rGP24fD04H/tXv8/Y2AYwJHDW7JfnEi550ORMqMuDUlDWe6Ys90JexXD+8iNoNvdPTp1T8jM/ioXn1comIP0Svi/Wo5JCkREVn90p0L/0t3nFutLsX7sqG/sW3p1T9F7detN/W4zOeyXpeY9rE1Syl6E6DWJA4ZGxw1uCU2O1kJ9pVgld1tZsSzqtHIrVf3V7/Q7a7jsPU6kcvV3xuWkET5508Ld+fOuax80dSr9mvpSHkdBsRLHd8h8Tof1/W+GeBoGVBnEgfHBI6Xx+pLFGp24SQtRIreGo3ec/4d2+bvdOzNc8farZm5t9t9js7omhL1r9an213HFepd/3OrjEmJiMjHNnUofrUUv6lDSTnOpNdBosdhjner37fbsX+9enV/RH+jX9pt2AoSEzhaTn1BtS52UQTrnTzoXUejsaZEz/oTsXUzkld6Fbvd6HeRduQ4YnqjxyF2u/O4uvS2284NqycZxMfulf5jd5de/FLcorT1qnatz5l6G/90vzneut9F2sH4p66Lk/T+6tKbzmol5bBHnA4kJnCcnPDy86lM6ppUS9DrmbDpOBjtxs+EGax+jDmjjWg1Z+qx/VL06v0s/V7ienXsRL3Olf7v3XFZnZIYH7tX+n+wWCco4svo69DQazg8wa/SXgfVmfQ+ijgk1muW9P4aohfGQGICRwluST/eyYOpYi+qpLA1w4zMgJNmxB0z39R7tcRmwHLbtf0MPkF/ygw+2SGSdVLyC4ul7JrKQXEbB6XWVx1XRH+j/3b3aLvud2q/yPsoZWXbrvfTNo5WCk5K+W/VZdtQQ2ICxwduSSrFVHU4ddIzqEYjaKfUaJi1AZsYsZmv2R8K1/8iM/GYYzK6liGiv28G72W9Psk+kpKQj90r/ccCB2VUjYYYbWt8+9pj3jex/Sy9Cfp78Ddymb736UFiAkcHbkk6i6kTuEE1GkG7zyEZNaPtcEh020kzxr4cHrH6G3LzFrluS+Wg/PJd57y7jDs8kvY6JDlSup3yvgnec6MdnnGOiRO5xC3pZoj5BDB/HgeXqEISU1w67F5Vlyj4cMO123W/1bbiSHd/GKZvv9R45n4D9WfU4fzMkpIYv/W0cHfcuYS3Jcj1Oujx7nodXML7aev3Q6S/wknp/5t7y569Th4cEzgqcEuGk70I9hVf9M6AB82IuxwHPSOW23afEzFqBq/omsHXs+0hM3jdb+t37kCSEhGRX7lX+l++u/QLvxQnZf/rEtD1Omg3I/Y6VO26f8jrYOkz2o1+exicyCVJSRp39y0AIBvUlsyChUhxezo+ZUarfh0yaqbq2+1Gv26rXRs6hszAVbur7mDQcTX1Ou8PJykJ+di90ouU8ltPC+fcuUhV36RfFwleh473jTdel/D9FDLakRERF3vfqHa9XxMn63oSFlNLJ5LbARwe7om/EhKTUeQ8neM+469kJcX8TqVM8Idr6/26Eh6t14kTf7n6+QNMSix+62mRXOO0SozZt99K1ucJcu4X+9WKK2/GQmICxwG1JVvhvSzlXa7MEatOTBpPEG7E/gD3JRZWvEg7db8hTk1jBl/19+jPmAg5keNJSgA64FQOHAXUlswIv3FL1ttr6rZxzt/q1+3b2CqeblcJT9WvEoaw7YLEIRpPtWt9uq316v6O+GFiVOtrtp0/IqcEoAeKX+HwobZka7JdNvzKpm6grq+oHAPfbsf6R6034Zsx6/7guXS7fu5Qr26n6DX0J+vV/W39JCVwauCYwMGDWzJDYg5JsmOi4lRYDonlhFj9dVtunYqW3h79Uzg8oRCl96hqSgASITGBwwa3ZFa4O3JuXo2iE4G6f0iNRqxmwzd/aoekdfWEelgrXget47L09tXOdARt6JWSpAROERITOGhwS2ZKtEZD9w+t0TBqNho1Gjq+clKqBzSciepxRrFtb+1JTG9Hu1Pv7VP7v7tYCsAJQmIChwtuSTZWkvkW7D7ciDkhCVfhxIyGmNth7hepB4k7FW39SU5I4lU4vXpFvF+RlMDJQmICBwtuyQzxwV2FRTocg8QajYreGo2umhKjxkRi8XQ7Ve+Aq3Aax9XW50Qu/c+x/gWcLpFvAYCZw7olWcm1wJr7o43lkeJ0iCQ6KcbjovF62tn3G1JTEnGEwj1WrFUCgGMCBwluSVbK7BGjNSW6nVqjoR6nn6evxkQnDLEaE50IddSAdOtV7Vpv1d+OzxU4AGtITODwoLZk/rRqNKrmkBqNXI6JrinRtSZ6v/DxPfqHXoUT0+tcufoISQmACIkJHCC4JXnxfn2Tsa2pFlcTMWo0qv6hNRp9jkmVCOi2ckZ0cW3lkDTaAS29qj3kKpyGXh1/07xxeV4DgCOAxAQOC9yS3JS57pETpesqlEFXu8QciJjz0eOQ6EJYS1+vU5Oi10iMGg93pfz8xK8BwAFBYgIHBW5JXrK5JTGy1Wjo/tT4Vk1J0O5aGdaMp583Va+hv/4VbglACIkJHBrFvgUcETtwS4bUYnTUlEQMkzSHQzsmRrvRb+kP2r13Ra66e67C8SLOy6X/yKK0VAOcKiQmcDAsHvuLmLMOw5ncLRHpdkLqK2GCn33rgMSuuumsKYldhSPtRKQVXzsciQ6PMx7fjl+u/u7iQgCgAXcXhoPBcxonJ9ndksXCcLO8xGtMQtciuiqr+tm6S68EroRyPmJ3Fe6iV6/eL9AV1Wvr9+6GUzgABjgmcBDgluRlJ26JSNMhcFb/FjUaYZyKljNhOCYpdx/uOo4u/el3Hy7lw6zuCmBBYgIHAW5JZqauLalouQwpNRo6gYjEs9pmv+FaWP1mjUkgK+zo1NtzFY6I+I/cWUaUA5w8nMqB2bN47C/2reGYcBO5JauVsYKsk2Z9SOgs6JoNq4bDuc325l+sXfdvYoTPpdut/i69ejtFr96vqde5HblVAAcKjgnMHtySvKze5S529mThKRyzXkOf40lwUmoCK6PLIbHqPhr7dWi3trtIuOpoJVyFA9AFiQnMm8fBaqKwNc7L5U5rdaJX01RtVSSSXqOh+mPxq1MsVX/H1TrhqaPK9Rh9FY/hzqz7S/kwi6kBdEFiArOGBdXyslO3RCRwDOoOiTshjWIOkZZDIu3EwHqY3i+MYdaU6CtojCB9NSZJDo+IlxWncQB6oMYE5gvLz2dlqtqSmvcaTkBVc5FSo1HVgdQ1Gbpt1JQ41S+bmLrGJNYfqzGJ1p4Y+nuPq/7HlTgACeCYwGzBLcnLzt2SkJjTkeQ4aGciEq/VMH72XYXTq9fYMTwd1eGkOJFHOz2NBnCgkJjAPMEtycrOa0taAjY/dQ1Ick2J7o/Ea9WAVImBbm9qSszEqIpn1JhEa0pUu6XXycpTWwKQAokJzBLckrysxLiUd5e0ajRUO7FGox0v0m70R2pMoqvNxh4X6q2aqQ4PRa8AqVBjAvMDtyQ309+sr8JFEqBWjUb1b1CNxu0Do+1G8EhNiVFjEq0p0SGd6o/ob/Q78cIdhAFSwTGB2YFbkpedLT/fKULsGo2W8zHkahdnBFBBrZoSF3NIxui1djD0/u4bRjAAsMAxgXmBW5Kb3bklIuK8PLJ/EfzsdCYsx6GvneKkuOZjUpyUrqt2dLvlCAUxvioi4q7k429cycefFub4AEANjgnMCtySvMzCLREZWFOi2pYxMmo9E2k7KNXPrvVNq17UEgAAIABJREFUGvESakz0PXO+WscpRFwhH39DRKQU8ZfyEpcPA2hwTGA+4JbkZqduiUjkfjkiTUch1QmpXYzEGpPkmhLdH+oz2mZNiW6rmEG//6o5IgUuCoCNdRYVYC+4J/5KSEyy4b0sd52YiIi4P4pdSiMdNSVBO+Xuw7GrdgZdrZNhv5ZT09brH/nN6ZxeSlwUABwTmAu4JbnZuVtSE7syZ/27uAMRcRzMdqM/ocYk992HG3Ul4XasxiSJAhcFgMQEZgK1JXnZZ21JtABWJKjzqNpBPYeXWwekscaIb7cb/Qntut94Luu5G/2G/vqfpTdofzV8cDLFJkHxJClwipCYwP7BLcnN/tySPqKOSbxG49b5CAOk1p4EsaKOiajn7tIbtFMcnnS3JEaBiwKnBokJ7B3ckrzs+0qcaAGsiO1ANLa92s+4gib82eeYVPt2OSRajHklT+RYevbz4xwTiwIXBU4Fil9hvzz2hXNytW8ZR0Tpn3fLfYvoLIANSSkuHfO46H5jim3H6/XlKodrEqOkWBaOERwT2Cu4JXnZt1tSEyuArWovwhqN8Keu0YjWiqTWlOj9pN3urT3p+GfqDWJMl5SI4KLAkUJiAvuD2pLczKa2JL4CbGQdkL6VXYfeM2fIVTgNfSJJNSZdV+FU7d1SUIsCxwIrv8LewC3Jy2zcki7UmZRGf7hDdYrFXLdEt7vibRqd9Sqxx9m7RPdzjY2p3ZIYhaxXly1F/CMRKTnVA4cGNSawH6gtyc0saktCkupMJq8p0W2V2HQlPM5IfIbo+OqmxmT/lNSiwCHBqRzYC7gleZmjW+LE0DS0RsOsPRlSUxKpMal+mjUl6rkbetVxaP1a7zwogtM8F/sWA9AHiQnsi2LfAo6I2dSW9DK0RsOsPemqKdG1J5vH6hqTWH9rnRKtV/1O62+tUDsrChF3TrEszB0SE9g5i8f+Yt8ajok5uiUiIqv3uIvoLy0HwtzPckzCB4SOiG7Hakp6HBNLU7Le4Hf/YVauiaagWBbmyizTejhu3JPENS4ghdnVloQs/shfeOk4bTd5TUnf46yaklhxbWqx7ab9OS/+8wf1Vi+pRYE5gGMCOwW3JC9zdUsqoq5Jao1Go1YkaOu6kpQaE7NfjJoS7aRIpB3TG7QPiwIXBeYAjgnsFNySrMzaLalwn/FX4iM1RZbTYCy4On4/5YB0XnZsBNxCx8Srvu6KEhcFdg2OCewM3JK8zN0tqTAXW9M1G+EvGrUcVk1JuF/ECWk8QU9NSasWxdAX1Ttgv8OkwEWBXYNjAjsDtyQrB+GWVETXNEl1PmL9Wz8uxUmpCIQmPK8vVyL/IaLnsClxUWBKcExgJ+CW5OVQ3JKK1pomYY1Jl0Ni1n14tV+HY9Jbe6JjS7O/odffhm7pVW3df1wUuCgwJSQmsBO8kwf71nBEHM66JTHCdUAa64NY65YE+1VrlcTupaMXHgkf03XPnEa/RNrBdkuvauv+46QQbiIIE0BiAtPDzfqyEr1B3oxZvcddmCvBivTUaFg1JdJde9JwTMR2SKzYXVfhJOvt6T9eClwUyAWJCUwOy8/nZfWujoXLDo3YiqqxlVVj7b6VYaMrxW4eG3NSxOrv0hs5rtOhwEWBbSExgWnBLcmKO7DakhDTNUmpKfF6v47akdR75uiVYbtqTFr9hv7630nUmKRS4KLAGE4zp4ed4Z74KyExyYZ/3h38Z9a8Qkf3xP6oD97Pi3m1TdJVONvpOOKrcsZSivhHIlJyRQ90gWMC04FbkpVDdktCGq5Jy4HQe0dqSmL3wela2TVaU2LUmMTo1RvZD0SqmwjiokAPBz/7gvmCW5KXY3BLKlquSWhc6P6u9tb7GY5J9J45ah2TPr2f8+I/R3bSQ4mLAhocE5gG3JKsHItbUlG7JqGzEK0pibTr/iE1JXq/TczOmpJIjYk3/rX0Qg8FLgpojmYGBvMCtyQvx+SWVDRck+xOSGy/lBoTXYsSiY9jMhWlsLLsSYNjAvnBLclNuW8BU+BELvtrNHRiYNWU6LZha/RdhRP72VVTordTak8ghYJLjk8bEhPIDuuW5OXQlp9PZfUed+Hc5tii64C427ZI/3omjXVKgkDR9U1E9Yt0rmcSLpsSWxlW1DZsQ8FpntODjw/k5bEvnJOrfcs4Ig7qZn2DeUW/X1JOsVQkXN47pKg2S/FtoPdzIv7zWCcTUHKq57jBMYGs4Jbk5Vjdkpr3utI5uRy00qtzqr9jZVen+qWKIe121DEJ9Dr1T/e39MIEFLgoxw2JCeSD2pLcHP7N+hJYvcdduJWqN9E1JrruI1Zj0nd1jo5lxe5a6bWlL9IWEflBMpOJKUTc1TP/6HV//6PXF/sWA/ngkwPZ4EqcvHgvy1NITCrcZ/yVrNT7J/UUS6y/93FDrs5JOHWk+v0/W0V2gJy87duvy7Pf/o44kctX/8XZxb71wHaQmEAeqC3JzXHXlli84gvn/FV/YtCRKFRMlsj07Vfp27RITHbG2779uvzot76zXh9P5PIzJCgHC6dyIAvUluTl6GtLLN7rSufd5iodVffRdxVOo/ak5yqcsEik6yqcsMYkWlOiQ6rakh8cOxgwlC9+15vl62+6IyIiTuT8Pb907e//A07xHCI4JrA9uCW5OT23JGDxir/wVaLbciaGOCnGOaCcDknKKaY/9lyZs2Pe87Vv1Nte1rmid3L5md/BQTkUSExga6gtycup1ZZYuFfVe2rnp2KG1J50xGP1151T1Zs01pxZUzqRy1d/56zcizBIhsQEtgO3JDcn7ZaEuFe9T1tC3mCydUr69mvWmMgfr8R/PrIvTMaz335d3vbt76xfJpWgOBKU2UONCWwFtSV5OcnakgjOy2X3eiZVu6umRNWfJNWU6P7bMNF23a9qTH6Ir9h98Gff9Wb5s+9+U+OlrF8WJ8XKydX9X6b+ZK7gmMB4cEtyg1uiWLxyc+FlcZv8er8pGuipKamLCwbWmOTez4v4f86VOfvi3V/7RnWVTjuxlI17ckcuX/1t3JM5QToPo8EtyQtuSZvVe+9cOAnvp9N1dU69k9ov4WqdlJVhG/fWCfsDwWE75qrAzvjSd7/p9mWWtiHmnRR+JVfvxj2ZFXxsYDTuSewEP4wAt6SD2jkxHRMJpsShYyI7qCnpa3vxpRf5auTxMDnvvt5cpWM4JuG2c1I6h3syB3BMYBSLx/5i3xqOCdySbmrnxHRMnN3uXM9E1Zx01p5Is91Va9JyTJw4lqbfK1/6rje1HBOR4C2y2c+LFCsvV+/+L3FP9g2fGBgFbklWcEsSuXVOqp6gxiRaexKwg5qSllPzVRF/RZ3JPnn2tdflba995/ZlsdwT9ddw4WSJe7IfcExgMLglecEtSefWOZF2jUn1s1V70lVTop0UabdTVoYViW//QKaDh9F8/e5dETEMsuDtUuclm/6V4J7sCxwTGAxuSVZwS0aweHRz4d3iPLrAWcUYh2PbeNYTsJ7J3nn2tdfl2de+c9sRcUrC/s2rd/mZ32bV2F2CYwKDwC3JC27JOFYP7lw4J5dm7ciQq3DC6XJXjYnc/iqpxiTm6MDe+LO3vFlEjLdJReu1q1/C8/f8yjXLIuwQPi0wCNySrOCWbEnTOUmoMamYzCGJ78d6Jvundk0Sakxqx2TzcyFS3ni5fELdyeTgmEA6j32xbwnHBG7J9qwe3LnwTpbNmhKRVo1J6nomEjy210kJakxSnBTuNLx3vn73rl1jIu0ak7DPyXrNk8VCru7/w+tiZ4JPFBITSIYF1bJSnvqN+rLxXleukxMpxfuNS7L559U/3d9qi9zaHX7z67528E+CED6I5b2478eg3jd/dfeOfPHNb7p9GX3waocvf1i65IOfXuRmJVfv/q8oip0SEhNIY+2WFHtWcTTglmTmva7073NLca4cdxWODHBMguftdUwC5+aHSEzmgF7H5PYXwc9mjcntz9vt83f/Q5KTqSAxgSRwS7KCWzIR/n1u6bxcNlyP2hHRbcMh0T9jDknriVW/tQ+XDc+CL755XQTrU19LaTom1WO9JzmZChIT6Ae3JCu4JdOyeuAu/MItbx2L5lTXrDGJ1p6I7aw0+sVuB09Xd/80rskc+NJb3tS7jonpnISlSyIiJCeTQGICveCWZAa3ZHre60p/I0vxvkyuMTH7xa4xafRLpB3sW03Bf2q6Q4Z0vvjmNzdrTLSBpt8WwT6Nt4DgnEwBiQl0g1uSFYdbsjuWrvTFYimLTd1JX41J7qtwwhqToNYE12QefP3endE1JuHLvbmcmOQkIyQm0AluSV5W73IX+9Zwavj3uaVbrS4bTkijkGDAVTgSbJpX4fSJEZGfIjGZA391505vjUnjlffKKal+d+uqnD/PpcRZIDGBOLglWcEt2R+r5Z0L791SxJXdNSWRGpNOJ0XUFFrs/qCNa7J/Wm5I+IvQMQn31W+Javv2H+ucZIDEBKLgluQFt2TPLF3pC7d0PnBPemtPxKgpidWYBNt1v7fbLKC8d7745jc360eqX6Q4Js0ak8bb5sbJ1e6O4jghMQEb3JKs4JbMh9XyzoWXjXtSOx8ZrsIxHRNnt396gWsyA75+7067c4BjInL79glLit71q9xbZxtITMAEtyQvuCUzo3ZP/KXpkIhIvPZEul2P1NoTTJO981d3jMTEeM30lTuhEVb3SeNxxbv/a4phx0JiAm1wS7KCWzJfGu5JWGPStzJs2E5ZGVaM/udwTPbN1+/eHb6Oib46J3w7NA2yc5KTcfDJgBbuib8SEpNs+Oe55/1BcOUL533Tgvey/pbU7kaqC1Lf7Vi3Nz8/txL/+fGSYXve/fVv3DZU8li/Wi54G1iJZke/c3L5md88u8il9xTAMYEmuCW5KfctABJZutK/f+HW99uRoJCg5yqckJZjEqkxCWtNuHx4HoSJhXZKqp+JNSZhHC+cFh8KiQk0oLYkLyw/f3j4pVt6cetVY9c96VfSpK5vUvd7EYpg58WAq3JEojUmjTic0hkGnwi45bEvHJe65aT0z7vlvkXAFlz5wnm5qk19fSrG7Fe/Dom1P+fFf55q2H3w3Le+JW+9uRl9qibsb7wrXPPldsIpnVRwTKAGtyQvuCVHwNKV/v3O3V69IxK/Wsc3ZsmtRWbDKbVxDx1O6ewRax2T8CocuW3X/+T25Q/XMWk4Kc0Lufh+TYTEBNZQW5Kbkpv1HQ+rD9y58B9YOCf+sjFF7roKJ2zX/cGVPLrNKZ15oGtM5Pal6qwx6XsvCKd0UiExARHBLckNbslxsk5Q3CZBUQ6JqG1R/eZ+zQc47j68c1prmYxdx6RrzZrbOOfP/2OWrO+DxARwS/KDW3LkrD5w58Kv3FIWUjbWM2lc2dFxFU843Q77uUpn5zSuvKk6Gi7XuHVMYq+i80wC+yAxAdySzOCWnAgfdKV//2LpV7IUkbJ161nzXjpqyq1rTbzn7sM7pnZDwo4JakyCJ2RV2B5ITE4d3JLc4JacGh90pX9hsfTeLcVJqWfbImHbqDEx+t0DkpNd8czNTbNjbI1J+AAVR0MhbDckJifOgqQkK7glJ0yYoHgp0+6Z45v91VT8B0hO9kbwWjWuwwqdEqvGJHxAArgmcXjnnzjuCfdfzwjrlkCTl33hxJ9LNQEIF7oIiX0KWd9kcna1jonF498842+wAY7JCbN47C/2reGYwC2BFh90pf/gYrOSrJRRhyRWe/KTXKkzNW9942a3NSYBuCY2ZGsnDG5JVnBLoJ/KQfHqFKrloITOyudxTqaivonfEKdksx06Iy2HJPGvK65JGxyTEwW3JC+4JZBE5aC8uHDeuc3VPOEOQVVD6KT8pIj7Sf5+7YQJ1zGxwDVpwzv9RMEtyQpuCYznZV84v6lD6as9+ZwX/3/x0c3FMzc38txff0u8k73UmFTgmjTBMTlBcEvyglsCW/FBV/oXF0t/U1/NUzZrT4Iihp8ScT+5Z71HxFvfeGP365gY4Jo0ubtvAbB7PAuq5YR1SyAPH3alFylFRORTvnB3/LmsXCEit7NyLyI/5cT9wMY5+eo+hB4PrTVMRBqOSGsdE/WLHDUm0AbH5NRYL6gGmcAtgUn48MZF+dDC1euiiEj9p+8HRNz7FiI/sDeFR8FbnxqJydh1TEbWmGxCn+Oa3EJicmKw/HxWcEtgej7sSv/hTZIii/XqspvpuHvfgqLYkTxzc9NYsdW6V059GyS5bdf/qt0G3isH+mHsTonHvnBOrvYt41jwXpYkJrA3PvW0cP7OuYgv5Ksi/hWKYofw3F9/a72GSUdRa+NUjWxZHJsARbBrcExOCNySrOCWwH758L3Sf2Sx9G6x9D+4WLq/vyg5tTOA8F42kXvcDLpXjr43Tse9cmJwOmcNxa+nAjfrywq1JTAbPrxOkL1IKZ96WsjvSiHiHgif907q+pL6+t7NL4LtxuW/Xm4vKw732WzXCzDgeWwNQ3giuCf+SviiygXrlsD8+fjTQsSt10eBBm97/XV59vXv3CYee17HJITTOSQmpwG1JVmhtgQODpKUBnV9iUhyjUnYH9u/sz8Rv5Dlk18/K4c96rigxuQEoLYkK9SWwOHx0r1SXrq7FPFLEX8p1XopJ8gzb9zIW9+4adeEVERqTBpX40xUYyIisliRPOKYHDu4JVnBLYGj4URdlIZbIhIvgJUBV+VY21v8dT310zknffCnALUlefHPOz4zcHycSJLyzBvre+OISLMmZEY1JiIkJpzKOWa4Eicrjitx4Fg5kVM9z772euMeN+FNnLe6V46OF7THcOqXDZ90Vnbs4JbkBbcEToojdFHe85ffsItaU07F9J3qiRXJjsFJ+fg3zk72yj/WMTlWcEuy4rxcbrOu5ofecl1Y/W+IyGLRfJ1Wi+7Z6sNvnnbFPuyIl+6VUjknR5CkVG5JRWPdEWsdE68Sj12uY+IPd5xzwAzwSMEtyYvllnzobp1sFNVJUe/lgSxk/cWSUjSX2G+eu17/vqz2cSKPRERWm76Hf0kCA5n5+NNC5PAWcGvUllQopyPr5zSsWRnJKV82TGJyjHAlTja+77NvyNm/eUN+/BOvXS5ExDt5IF4KP8T6DfqTrN+UL8QhVnLlwHh5VG0//IvT/MKDjByQi/LcN74lb71R65ZUzGwdk+Bhl5/5zbOLcY8+bEhMjhDcknFUSUi9/dn19tgvsq0vL8w4o2vFWc/oys0X7qUIyQpswYyTlGfeuJHnvrl2S6KJfN9nM7I9WY2JkJjAMYFbkszbP/GaiKgkpIttEw/djlm/05zy6Y5zu13KQkTcJln5CskKDGCGp3qe+0b/XYRb/VbiIevPaetxse1t/7qecAEsicmRgVtiE7ohb3/ptWEPHjGbmszpiO0/rQNTbrYuSVQgmRm4KG977XV59rXvrBuxz4NMMDEI423Bqa5ncpIHfbTgljSoHJHBiYiBZdnmPrXSelxXnCFWcv5EqHSCowID2EOS8swbN/LT3wgKXvc9MRjBqRbAkpgcEafullSuSPKpmVTmnygkx88+Q3RSuvX25cMvnd4XKAxkh6d6nvvGt+SZ8BSODKwxSfg8TFljsglxknUmJCbHwom6JTldkV6mOrUis6ox2TZe6ar6FBIV6GJCF+XZb78ub3vtO+MT+ikmGiM41cSEBdaOhFO6g/BOkxGRxhdN4zvH3f6M7d/4YtS/N+I4Y5/eBCbYjsYL46Q6MDqe3sXWVfjNH5oPPHtdiog4j5sCBhMt4PbMGzfytk1dSd/ntO/zoN/3ZhwdT/LVmJwqOCZHgnvij/ozsPNkRJFr5rSXdUwGOC9bX3XUnVCVJCnQyZanep55eiPPfeNbk3weOvdP2R7DiV6ZQ2JyBCwe+wt/hI5JVTOyr2SkJneiENveJlEYGifx+VPiDY6zucrHObl8+KckKRBhhIvy3De+Jc88VQuppUwMRn4epq4xETnNK3NO7oCPkWNzS97+idf2n4xYTO8ozLrGZApdTkhSIIGEJOVt335dnv3Wd7ZPnIPtrBONkZzilTkkJgfOsbgl+z5V08mUp0Ri/dYXY1ecHTswjXgyMLGK6y2d3xTOkqSAReRUz9u+/bo8++3v3O43p8/DlpCYwMFx6G7JbN0RhTVz2uaLbKdW8hQJ1cRf/K6qRyFBgRgbF+WZpzfFc1/vWXJeZP+fh5G4E7wyh6tyDpjFY39xiFnJrN0RC9f+rnGqf5D1q+N1fJGZX4yWLmfEM46jz+Ewjy9lhmj9Xh+aMV6NOM14hV9I8f4fvxbn1pcgP/wCSQoEvHSvvP/Ra/Fu45wkfB7MU4pqnyGfh87PO4yGxOSAObRTOIfijrSovm3UH2Lngz/s1S9ExG+2ffC4wfHC3Tf7+444VaA6jmUlB/uID+Lp7EJ/8frIF2+KLtVfx/FBPDVeOp73UngvxQfefk2CAjX3P3pdeAnWbgo/V8H7S3+uqt288fmpd9j0mRMD9f4248FWkJgcKo99sW8JqRxsQlIxF0dBIk5HsK11NaTZzkSnY6PjmPFiuoxxMeNFxkvH87JOUN7/jmtxm2XxSVJOk/sfvS5E5Mp0OqTtmMQ+p32fB/1WNuPoeJK3xuQUITE5UA5hQbUtE5Lyaz9yp7j+kTvmL7/2w+237tci+1p835dvbre/8oa8499859J7aRTUiSjHQJpfZK1vnXDGFcYI+qNxtGNhbPtgFtea0WkHokNXPcMz9Da+qL0KFXFYal3bxAuo4rSqp4I4KyeFEymW77guvchlSYJyMtz/6HXh/SYpUZ+TKT8PnXG6Pg8wGIbuEJnx8vND60e+9iN3JEw+vvbDdwclGNl46W79WfiQXBciIrKQwnt5IE5domg5AoGtO8d75bTiWDPNkfFGxbHGK6Yrtq2exy2kvHFyWf5bkpRjpXX6RmTQ+y76+Rz5eUj6vG+Jo/gVDoE5uiUpCcmf/OybRGSPyUcUfxm2/pVs/rCtNstl+02yclfE+/XYe5EiW43JZna1VY1J0BWrMWnEiZxTj8br0NV1fFacakbbGq9QiybYJzZe3kvhvBTLv3VdehKUo6N2SiqM952ZyKv3Y+2QaFfS+DyMjpcR7+RB3ojzB8fk0JiZW7I5XVOKsfDRn/zsm+Sb33un/Mp/crf1u1kRuCWpfOjudeFlkyAajsocHYWcDkwjnsiwdUxS4gU6x8TZ/Ci9k0cLkfIhScpBc/+j14V4uUp+/+7482B9TvXEYDQnuCw9icmB4Z74K5n4duEpvP0Tr8k7PvHaZeUgiMzZEemklJfubvWhr5IUn3LKZ9PeqZW8TSLUs3/f8+w0EereXteikKAcHPd/4frCi5xH3xfB9qw/D2MhMYFZMwO3RJ+y+ZOffdOhJSIKv9zc5TQLH7p7Xfg7cu69FLNb4Glih2PrL/4pZq7t/UvvSVAOhfu/cH0hIueDE47Idvi+P5QaExITmDX7dEvChKRyRr7wM2/eh5ScbO2WdPHim9ZfqgfiKHTHC3Rum8DkKD7Momsh61qUPyZJmSP3f+H6wvvuz0+sf6tToMF2NgdmS07tRn4ndbAHzR7dkuq0zRd+5k3nR5CMBOR1S2J86C3XF6uhX7Bqe4eOwqDt2dSYbJdYkaDMjPs/f30l4anRKRKFXU0MMkBiArNkH27J2z/xmshnV/KFn3lzuevn3gGTuiUWL75lPQPc+eWKu0pc9vXFP3I2rBMXcVIKCcpeuf9zm8uB5+BwDImT8nnYAhITmB87dkvOPvlUvu9/e3oMp2o62I1bYvHid19fOC8PvNze46PxU27buRyOJGt7ii/sHr2zSqya7XWxLEnKzrj/c+1Tn1kdNDnQGhMhMYEZsiu3JHBIpn6qfbNzt8TiQ2+5Lm6cXB2Qo7C9Lon8gUn8wzCZAxPTJVIKCcrkVEnJtgnqoBqmrvhTJM5jofgVZscO3JITSkg27M8tsfg7/9H1lfdSjP5ijG1P6yj0J0LTJAp7mSFvHr+uRfk/SVJyUbskIdsmCrHtCRNdy5Gs423LCSYmi30LgG4mX+X1syv5gr93QkmJlHNKSkRE/uCvz5arhSz9+lJW8SJS/ZTNtvjbnzVVnwQ/q8eG/4L+MI4P4iTFk6a2Rhwdz6t4QXwznqHL+2asRpwwnkTiSSSeGPG8imfp8lLISq6K/+z6qnjn5rYFMJr7H7m+8Cs51++Txms6l8+D3O5jxhEVxzelwTBwTObMlG7JZ1fiP3uKH515uSWaF7/n+kJ8vaLsdg7HLmeIQ+N0zIB3oitxu45jO0LlZgsXZQD3P9J/GX31M3wdt17HJObIdewf01WRpGtbcExgTkzilnx2Jf6lmxNNSubnlmj+8JtnFyu3cU82fa0ZXUWXA1D9DGd2Q+KFYfWMNiWO1aVnvWp7rK5Rx2eNl0TiqX2CXxTipRCRq+KduCgp1ElJiHYm5HbMG69N9dN4P0iwf71P1RU4HVa8dqNbV8ORS9EFg8ExmSu53ZIve5Ev+1NNSDbM2y3RfOh7rosbCYpjD89RSNbVihfoHF1jsk28SH90hhw+drG57Pj/wEWpuP+R6wu3KW6tyeSwjYq3ae/k87AljrsLw1zI6pac7GmbBrN3SzT/6ptnpYi4D37P9ZWIFPXMrPoy9JE/vPqlDvYPfx3GqYLUcfztF2z9gMh2tY9Xz1OHrr6wtYth7B/q8sZx1HGcSOzuzqa74pRLM0KXhLrc7ay5MV4i4lZSeJHiwc9eizgp3QknKfc/tHFIVsH7I+X9a73X1PuilRRO/HloaOnYbn0eYDAkJnPksS8k1+XBJCUb/OW+FYzl5W+eLV9867r2RP8Brb/rXd01/ly4Uw91Kl6wvxlHx9NSgvhjZ8Bal5aWOgPW46V1NY5T4o5QbLzqMOtTPUXxn1+X4uRSROQUkpT7Hwockg7HQo+Xfv82Eg2JOF46nkQ+DyKDr9KK6arjBXGinwcYDDndDMm2bglJyS0v3T3o95FNAAAdlElEQVT49/oL33NdOCfn0nEX41GnVmLbCX/g9f6630oUZrmOiTVeQ7dTjnPdLp0cZ5Jy/8XrC3G3xds1Pa9vrlM0SafaBnwesp46GolfyPLJrx/X+6QPHJO5kcMt+bIX/8lVDjVHwuG6JSEP16d2yhffqm4DHzoBm/9aM/cNQ8+pt+KofbZ2FNT+fTNkM15Ee0qiMEpXLOEwdJnxRArvpHAi8uC/uC5lIeKcXJZPDvOPz/tevL4QEfFGQpLkKBjj5cJGsN2XKJjj35Eo9H0eenXF4mRKSk4Vhm9mbO2W4JK0OQK3RFMlJ9lmdFPMEF3/F39qnEY8GegIpcQLdA4ZryRdsW3reTY1KSIic05U7r9wXSwWUjRuTlmRMF4VWRyO1ERoqs9Dqt6R4JjAftnWLSEpMTgOt0Tzh18/u3jhe69Lt17zpOirManaO5khxuIF7bGJUCOOa0szj0/HC7Y74w1IrPriNOJJRwIjUvh1XYo8eP663Px6FonK+15oOiPV2NSMSPj0eOn3r+WgJTkvkc/D0ES07/OQlFjBYBi+GbGNW+I/uVpfEgyKw7pEeAwvfu/6rsUn4SgYurIc377HK9Kv//DJ+t494p08WizW2+Wr+ROW+y9cF4vV+rvIOkVjJpYSSVBj2z1/4Ld5PWbnwGzBqd3AT4TEZFa4J60LJPuhnqSLWdysbxe8+L2buhOR5C/szi/+xC/ynZ6q6YoT0Tt1ojDqD1+wva0uf9ssqw23kEcrEVmsNivTdvjiK5FCFiLOywNZiKz8uvZlL+OVsL23U43bfB624QRXfRUhMZkNi8f+wg9du4RTNz0cv1sS8sL3XhciwaJ8Q76o9/3Fn9ify+HYWpc1XjLeMcimS/UfmiM0F4dj0GXFqXrHcKKJCUvSzwSSkuwc3IJq2/LwL89KEVk6t/4yrf9tfu+ciLjbnzXBF2nj3PzmZxgnfGgjjo5nxJEwTqouZ8RzRjxDV+vvQhhHx1Pj5XScMF5svMJjU3Fa8UTFSxyvRnx9nCPGK1VXK14wji1d1njpFymMo8ZxyHg1ULoa8Yzxcl26dDylq/X+DeOJ2PpG4DeF0KcGickMWDz2F4MeQFKSwHEWvfbx8C/Pypevz5wXKcXLsHt6BP2NxwRxwodad+vV2604wWMbcSbQ1Qpl6G3oisVT2s14hq5wF2/o8uGDIscdvVtu6nHG4oW7puiKvb6Wruq59P6J4zVIV+z4EsZrUDylq/dzZcWDZDLldbANg2pLSEpSOJnaki4++B9fX4kPiqmdpJ8qsLb1t0UYx/XES+wPdW1TbLt1vKFxBm4f1Hh17N/3PI33W1+8BL3ReAN1Ve1cn4dWnEx/WU+x8FUEx2TvDHJLSEoSOU23RPPy/3e29CKXrRm7nmkHs7zojDP8p+OJEc+reEF8K445ozVm2o3n8s3HNuIZx1nPaK14XsXri6N1WccZ09UxXpauQeMVi7PNeIkRr2e8eh0xK55xfFpvGKflsCldjXiRccz1eWjoCmNsg9sUL58gJCZ7Jrm2hKQklZOrLeni4dfOLhYil41z9upcezgDHFNj4sIHiDR27KvlqOME8RpxwnhhnDBel66Qxk5tXa14CePViKOPU9Q/NV6NeB266tdA1LbSZdVM9I2Xfn0bxzJivKK1HPo4jfGSME7qeKlguWpMWvHCpx4wXjAOEpN9sl5QrRf/SZKSdHBLNH/4tbMLv3FORIwZYoWegYrYM1orjtcPau7cmCjrmaaK16JHlzIJ2nefteL0OQqx4xygK9ylMUMPn1MLteKpY9p7zYTlKKh4uXT1jpeKU2/OabxGcqqFryIkJnvFJbglLJw2CNySCA+/dnYhTpYiOApH6SjEdKlgs3QU+nSJihfum6DLupqmIUUdd7Iu4/U1Pw8wGMZuXzz2hXPBmhMGJCVDOa11S8bwwvdfF+I37zvjS92LzKe40Ihfx3E98XLp6tGbNF6x7Sl0qf5c49WKF+gcMl56e9B6IQnPk+v9a+oaMV7bcKqFryI4Jnuj1y35LEnJQHBLEnj4F2elOFlOPQOu/m1dYxL+C+OE8Zq7JDlCfbpcGEfHGzBedVvr0vG6dIkRL6Zr4Hi1dImtqxXPGfESxku/vo14XeMV0xUSxtHxDF1djpCpS8fTh2eM12hOuPBVhMRkP/TdrI9C1xFQW5LKw79YL8TW+kVwjrxxqjxXDUJwrr/3qo2BuhpSVC2BFecgakzCOAk1E126WvHCXVWchq7I6xs7zlqrdXyJ4zVIV+z4EsZrUDyla+oaE+fl0XYRDhvuLrwHOt0SkpIx4JYMZJOcuBe+//pKqiQ5mOW1Zu7q99Uv/WZfv/m1D/fTs8qOOI14ItF75Zi6dEwrXqCzEU/pMie6Oo6hK+pQ9B2n6tfPq+M4tX+vLiuetMfRjGeMV6cufZwqjnWqphXH0hVsR+N16epwMcz3r44X05VynCNZLXBMYJd0uSUkJSPxJz272IaHf3G2FL/5EuyacYb/gv76MXqG7FW8IH7SuhxWPInEEyOeoatznY+YrlicFF2J8WSMLj1eMV3hWGYer0Y8FafXEbPixXSJEc+reIYu0XEMvdH3b4ou6zgtXUNxUj759fx3jD4kSEx2TNQt+bInKRnLS/cu9i3hkHn4F2dL2dydNqlmovrdpmHObqvf6Zmxk/Y5eteM1yLobzkwLtCYqivoj+lyYZwwXmtwenSJEU+MeM6I16GrFS8YRzNOZLz069s4lp7xah1noLVPV2Mo1ftkzHiJjhPG69IVidOKp3RNWWNyypcJV5CY7JKYW/Jlv74CB0ZAbUkOHn5145yo3Lg1oxVjhigieuaqd25MlPVMU8VrYfTHHIA6jnToCrajjkLsOAfoCndpzNDD5wx1bRNPVLzIOHY6YlpOYxC0qLauMN5oXbE4KeMVbB96jcmps0VeB0NxT/yV6MSEpGQ7XrrLezgjL/zQ5v46kRlkA7f+/t375a2xbaUry+WtQZxtLm8drSsWZ9/jFenPNV698QbGqbZb79+E98Dgz8MQnJSPf+NsOfRhxwaOya6IuCWcvtmKct8Cjo2Hf362FCfl3GpMzOfyzcc24hm6wpqJXl26T8cJ9mn09YzX1rq6xqsOqOJlGK+qs/FcHcc3qsZkSDyv4hlxOl8PvW8wRtaY9ulqvB5qHIfAaZw1JCY7wqwtYa2SLeE0zhQ8/POzpdusozDqnHpwQj5XjUnsKqHeGhMVR8Ln6tDlwjg6XnDMqTU5jXhixOvSJW1drXgtET3jFdMltq5WPGfESxgv/aI04o0YL/2+O/R1TE696LWCxGQXWG4Jxa7bwiXCE/Lwz9cFsXqmvWmufxoz4OYOygEQNePsiheiZ+zVT8MBqONYoQyNdRwdz9BlxvPtrtaMPTjumCNkatQauuL16Kr6R9eEaGcicpy11n2MV+Q9cDA1Jie+qFoIickOaLkl1JVkALdkah7++dnSBVfr4Cg0w4xxFKyZ+6SOggo2N0chabxExXNGvC5dzogX0zVwvEynLhavB07j3DJi+GAQxj1xuAfO1pTy0t3lvkWcCh/4oc0ibK2/zKpt9HuRtOJYa3ubeIn9dZw+LT3buXRNMl4i+e+VY8XrihPRGx7nbIptg+1cunqh6LUBjsnEtNwS6koygFuySz69qTkxZ66xP0Lh7LH6dddMU8cLCfobTxfESZq5OvUvjCNNXVW7ES+Mk6pLVDzXfM6h4xUea4qT03guY7y6dHU6HNZxBlpbcZSuxlCq4x4yXvr1bbz2I8ar8YsOXVPVmMAaEpMp0bUl1JXkgNqSPfDwK2dLL+urdXrPqTdOyLe7wlqOxu86zvV31SaEdQR1nMR4jfqFMJ6ha/KaiW3iiYrXVzOhajBaYxH0N4M39Y6uWYnpisVJHC8rXpbxUrqst/42NSacxmnCvXImpOGWUFeSCdySfeFELsUFibZrWt1OlOXtGo+9/ekiE8owjkuIF8ymdZxWPEufEcfpGGo71GWdKojG69Ll2rpMfXqXAeNVtc14Q3WFjkK7qzleVjwdM0FXI54z4lnHqaUbr2nr/WvpkqZz0jW+5uehD5agb4FjMhXKLcEpyQJuyR55+JWzUrwsWzNQ7SgE09St1uUI4lnrU3TGM3TF1gvpWueitY5G+Fi13RlvhK7B4xXO1q3XZgtdVWfjuTri9DpY28bzzXhWnMbrIc04Xu8bjJE1pn26vKGr0tsHbkkbEpOJ0G4JdSU5wC3ZNw+/clY6L0uRjnPqwQn5XDUm1tU9EsQZVGMi0q5BULqcEaflpkR0WVd/NOKJES9xvMJjTa59cSqetON16hIjnlPxEsdLv76NeCPGq0uXBHGs8XJGnOpBfbqy1ZjglpikDh8MQV2J41+62aeaY4ErcWbECz98XfjqPW59GW+2vUjceo99iXf0N+Jt4nc9f992Hcf1xEvsD3UNuprD2t4mXqQ/+nrEtMTiuDy6so5XTFds/23Ga2CcGI9/84y/wQY4JhPQcEs+S11JHnBL5sTDr5yVzsll3wwZR8GIk6Cr8St13EPGS/9BbrhTPeO1C0ehb7wGOWwxXa6ty3LYGmL7xiumyxm6IjjhFE6MhOGDQYRuCQWvucAtmSkv/I3rCy+bRFx9qYsYDoexT3Rbfzu5AY5JYn+nrgHxcunayhHa8Xi14g3ddls4JpH+QY7QwHjb6LLALYmDY5KZBQWvE4BbMlce/vuzC7e+WueW4Iu55XDIgBlwGC+ME8brmAFHa0x0PENX+NCWw2Hp0vGULqfjhPGCJ9zGEWrFSxivRhwdz+kASnuirkaMWDzX3CV1vPTrO6jGxBivBmEcfZyGriE1Jn6xrtMCm57hg6G4J5s67s+uSExy8dJd3qcz54W/cX3hw1OY6g8GjkKixli8QOcYXRVzdRQG13L06N1bTU5su/k8rPLaA45JRhaP/YWIsJBaVnBLDoGH//7swlWXPRoz5KijEP5LmCEfo6Pgwhjqj1kYRw3F8TgKXbrU6xuOV5fD1lf7kuqw9Y1X631ijKOGy4P7iY0djKByS7gXTkZwSw6KF370+sJ7Oa++WXI5HFNe3ZPy/LF9ZnXV0TbxEvvn6ghZcbZ5fXNdpdUCtyQJHJNM1G4J98LJCG7JofHwS03npM/hEBk+A65+jpkBi2vHi86AdbwwTqqu8F8YR8czdFnjpfWaM/NAfONvZN94xXSJiueMeFWcnvHSYsM4aigGOUKNOB2vbxjHPM7Y8enxN+Kk1JjglqQRGT4YSu2WsGZJPnBLDpYXfnR9tQ6OQmKciN5DcBRy65rDeFXkrDFxIpef+c2zC4FecEwy0HBLIBflvgXAeB5+6ezCLeTy0B2FJF2i4jkjXhhH63JGPEOXGorZOAqWrlRHwdLViqPjOSNe4nh1OUbNg1ZxIuOV7Pw5KUlK0iExyYB3ck7Ba244jXPoPPzi2YX4dYLpN/95kVH3yqn+bXOvnDCO6Dhal4rXqavaJ0FXfQw6TvC4pOPbwXh13isnjKeHNuhvjE0QpxXPG/EMvWGcRjwjjoRxlC4rXuO10K/HNrpEOIUzEBKTbVnfrI+kJC/crO9I+PSXzpYiUoaz+qSZppq5ijFztRyOME5XjYnlcCTXmKTqMhyAPsdkG0eoEc+1Qpvj1aurY7xaDoeOZxxfp2MS0yVGPP0ihXHUOA52hHQ8Q5dzRryILr+QJffDGQaJyZa4jVtCwWtOcEuOiU//2To5EWnPtOs+6Z65SriPEad3hh6iHQArnqGrFarLAbDiKV1emo8VQ2/DUZCmtpbDYelSwRoa+nQNidehy4yjnYlqU+sKx1LUY/c4XqnxnMglSclwrDwRUtksP8/lwVlh+fkj5QM/dn0lXgrTFdm0/aZ7bjfVCx8/Kl6HrpTn79Ql8xuvreNF+huvg5NmPjHkuF3eeFachZPyj7g0eBQ4JluAWzIFuCXHyqf/3dlSnJStc/TWDFTNkKO1AzqG6m/NxmNxpK2lNdM24uXQFU7rW+6FMUazrDGxtiPHZMbzKl5kHPXYJMeJvL4p8brGqyvODXUlo8ExGQtuyRTglpwAH/ix6yvZ3FPKS2TGGpuNh+0JZ8BVuxFvE3+uDkcux2AbXVuPV+z4DskREhF/h7qSbcAxGYlzcs5iarnBLTkFPv3v1jUn9Zd49dOHjeABeiZb/eyaAet4ISnxDF2tUIbeOk7w2HCmbe3TiKGepOEESFPbkJqQVpzg92NqTLKPV9/xTTBejXiR98mgGpPNhnPUlWwLjskYKreExdRygltyYnzgx6793B2FRhwchXicgY5CbP++52mM3zaOmOEYjRkvQ1f5+H+krmRbcExG4Jyc+0+ymFpecEtODedkadZySPBTJNjBcB3Cf0Ec01EwZtrhcw2tMWn0JeqKxWnEE2nP0PuOb9vxMnQNrjFJjSdGPK/iqThJjpjS1Rg/PY49x9l4PQbo8p66khzgmAzlsS/cV/wViUlWcEtOlBf+5nXhRa68JMyoE7bDOHuvMdkmXmJ/HcdyTLriTOQItXQlPn8sXufxzcwRuiOyfPV/4hRODnBMBuKcnLOYWm5wS06Vh396VjqRpYg9o9bbDQdA2i5CYx89Axb1IP10emYcxvNGqC4HYNt4hq7G8eo4YbzY15M1RuH2AF11vD5dsTjamYjEq3fNPV7hQ1OOT42XjudJSrKCYzIE3JIpwC2BtXPims7JnGbIs3AUYrrkMByFSdcxiWnp0RuNN0DXgqQkOzgmA8AtmQLcEtg4J16W9SxVOQBWLYd1rl/3t1yCWBxRcVS8KXWFU3jLvWjpMmbsrXiGLvEyerxExwnjGeM1OJ5X8SLjWMcJ41mvkY4TeX2txw4ZrxVJySTgmKSCWzINL93lPQg1L/zN62Ll5Ioak4njdIzXTnQdgSO0cCQlU4FjkghuyRTglkCTh396VoqTy0ZGEWybDoD0z6gb6JmxFU9UPG+E8jpAh6PQFS+MoZ6k4SgM0WV1hXE6xqvzOI142cZLpOVMWPuMGa86nm9JMONU/bEakxVJyaQwW00Bt2QacEsgwvt//PrCiZznnCHjKPT353QUdlpjMjCO9fqm6losSEqmBsckAdySKcAtgTj/+v89u3CL9dU64bQ3uWZCup0JazaeWoMQxrNqTLp0xeK0dKkZemftixjxho6XoeuQakzq8dPjGGrreX1bzomha0VSshNITPp47Av5si9Yej475b4FwLx5+IWzcrGQpTiR6p/bzF6d3Lbrf5vH1ftUj6sIZsCtONKM04rn2hPqRpweXS6Mo+PFdImhS4x4YsTrGi8ZoCv8p8arES+mS42XdiHCOK14CeOlX99GvHD8UsZLjHjB63tnIcsnJCU7gcSkB9ySSSjlpXvlvkXA/Hn4hbNy4W6dk4YDIO2ZcON3egYs4YPacSwHIIzTCuV1gA5HwdDVGc/QFe7S65hY8SzXIdweoKuO16crFifF6Qi17mK8LF2b9mqFU7JLSEy6wC2ZCE7jQDp1coKjMGtHIdTlDF21g5WqS8fThzdivIY6bAsn5eKOLJ/8NknJLml9NuAW98Rf+U+uSEzywoJqMIoX3nFd3Hg5d06Kxicy8keza9uLzP6mcju93Dmxv47jMscLdA4ZryRdse1+XZef+e2zC4GdQ2ISgytxJsIvOY0D2/CBd1xfeZFiVn/YE//w5UoUWnEGJGXh9qmMVyOedCQwt22Skj3CqZwI1JZMArUlsDWf/sLZ0m+Kp8P6AV2+cDDrmFjxDF2tp1Q1E80HdegSadfQBPFa9Og6hPGq3iPRGpNmHJKSPYNjEsH9dzeexCQ3uCWQj/f/p9cX4uV8K0chbCc6Clmdjp7nT4onEQdgj6dWto4X6Y86PHkco/KPfvtsKbB3cEwMFo/9BUlJdnBLICv/+v85uxAvl+Z6IdXPiKPQd3XPNuuYmOtm9MRp6dLxfEc8MeIZuqQjTiueGPFEPZcxXlqvdmm0HRLGaTkmht7G2ARxzHgxXWLE83JJUjIfcEwMcEumALcEpuGFn7guViu5EumYUYsMcxKsGbXIsFqOHddMDHncznTF9p/BeNXxFpy6mRs4JgrckknALYHJePhvz8rVSpbe3dadiKgZdYiesVc/u2bUwT6NOGo76igYulrSDL0tByD8pxyTlq4B8YbqqvqjuqyHdDkcmXQNGa/NLiQlMwTHRIFbMgW4JbAbPvC3NlfsiGStwThpR2Fk/cbea0w64jmRy1f/GQnJXMExCcAtmQTcEtgZn/6/z5bi5dKcIeuZedUXuiS6T5r7NmbuKo5Ifxyr9iJ0EUbVmFjxDF3SpSsyXtruiY1pjhqTrvHqqzHpPE79+pKUzB4ckwDckinALYHdU/zEdXFH5HxVuSciR1VjkqRrGwcil67Y/ntwhNyChORQIDGpeOwL+cTN1b5lHBms8gp75f0/cX3hRc5FJP8fUt3OeOqoFS/QOSSxasSJHWdse5tTUUPjTD9el5/5HZKSQ4HEpOLjb1xJOLuCDOCWwP554Seuixsn5xLUnjR+imqfqKPQmygM3d4msYr0R1+PyPM7kctXSUgODhITEZGPPy1EHG5JVvylvHTvYt8qACre/1PXF96v3ZNcCcegP5QJ2yebCAVxxzhCOp44KZ2TRyQlhwmJiQhuyRS8dJf3FsyO4ieuC3dHzsX31J4MSRRi27kTBSP+tjUmg3X16N3beAVtt8AlOXT444FbMgGcwoF5UyUo3ktxio7CtqdWeuMkblunjsbqck4uX/0XJCTHAIkJbklmOIUDh0Px3HXh/Kb+ZAeOQiOezOcqH0vX7BIrFSd0ZEhKjovTTkxwSzJDUgKHSfHcdSEJCYrup8ZknK5GvDFx1onTpYjIq/8LCcmxceKJCW5JPjh9A8dB8dx14TZX8bT+EI90OKyEY5s/7Em6tkmshuqyEiuZJhFyTi5JRo6b001McEtyUW6cknLfQgByUzx3XYiTc3FpC7VNeSpk61NHu3Q6ErdTx8uJXMoCd+RUOOHEBLdkS0oR/4hTN3AKFO9c16L43CvJ9uzf9zw7rTEZut12OoYVtS6kFC+PXv1fSUZOjdNMTHBLtsBfioiQkMCpUrzzth5lUI1J7A9ysH2yNSZBIuQWcikrKV/9l2elwElyookJbkkipYh/VLdIRgAaFO/cnOrZNEVkukTB2D7IdUza8UoReSQLEdwREDnVxAQAIDPFO68LERGp1keRIzu1MiROf6K0rhn5lyQi0IbEBABgAoq/fV2ISCFeHogqnt35qZrE/gmLd8uFk0erBadooB8SEwCAHVD87evCS3Da57hrTEpx8khEylf/FYkIDIPEBABgD2wcFZGFFN7Jg6pbRHqThq2KbYP+LInQ5oZ5IiIrEhHIAIkJAMCMKJ7fJCzrq34eiIj4gSvSmkWy253yKZ2TRysRWSxEVispX31IAgLTQGICAHAAFPfrhKXuWonIQkRWIuI2SUz9rb4Q8eouym59iqWBX6zdjpDVQsq7IvKGiJCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADA4fL/Axgs/rJgxZZZAAAAAElFTkSuQmCC\",\n                        },\n                    },\n                    {\n                        \"type\": \"image_url\",\n                        \"image_url\": {\n                            \"url\": \"https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241022/emyrja/dog_and_girl.jpeg\",\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"name\": \"assistant\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"The capital of France is Paris.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"name\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"What is the capital of Germany?\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"name\": \"assistant\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"The capital of Germany is Berlin.\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"user\",\n                \"name\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"What is the capital of Japan?\",\n                    },\n                ],\n            },\n            {\n                \"role\": \"assistant\",\n                \"name\": \"assistant\",\n                \"content\": None,\n                \"tool_calls\": [\n                    {\n                        \"id\": \"1\",\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": \"get_capital\",\n                            \"arguments\": '{\"country\": \"Japan\"}',\n                        },\n                    },\n                ],\n            },\n            {\n                \"role\": \"tool\",\n                \"tool_call_id\": \"1\",\n                \"content\": \"The capital of Japan is Tokyo.\",\n            },\n            {\n                \"role\": \"assistant\",\n                \"name\": \"assistant\",\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"The capital of Japan is Tokyo.\",\n                    },\n                ],\n            },\n        ]\n        self.tools = [\n            {\n                \"type\": \"function\",\n                \"function\": {\n                    \"name\": \"get_location\",\n                    \"description\": \"Get the location of the user\",\n                    \"parameters\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"city\": {\n                                \"type\": \"string\",\n                                \"description\": \"The city to get the location \"\n                                \"for\",\n                            },\n                        },\n                        \"required\": [\"city\"],\n                    },\n                },\n            },\n        ]\n\n    async def test_openai_token_counter(self) -> None:\n        \"\"\"Test the OpenAI token counter.\"\"\"\n        counter = OpenAITokenCounter(\n            model_name=\"gpt-4o-mini\",\n        )\n        n_tokens = await counter.count(self.messages)\n        self.assertEqual(n_tokens, 2016)\n\n        n_tokens = await counter.count(self.messages, self.tools)\n        self.assertEqual(n_tokens, 2058)\n\n        counter = OpenAITokenCounter(\n            model_name=\"o3-mini\",\n        )\n        n_tokens = await counter.count(self.messages)\n        self.assertEqual(n_tokens, 1796)\n\n        n_tokens = await counter.count(self.messages, self.tools)\n        self.assertEqual(n_tokens, 1841)\n"
  },
  {
    "path": "tests/tool_dashscope_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unit tests for DashScope tools\"\"\"\n\nimport base64\nfrom unittest.mock import Mock, patch, MagicMock\n\nfrom agentscope.message import ImageBlock, TextBlock, AudioBlock\nfrom agentscope.tool import ToolResponse\nfrom agentscope.tool import (\n    dashscope_text_to_image,\n    dashscope_image_to_text,\n    dashscope_text_to_audio,\n)\n\n\nclass TestDashScopeTextToImage:\n    \"\"\"Test cases for dashscope_text_to_image function\"\"\"\n\n    def test_text_to_image_success_url_mode(self) -> None:\n        \"\"\"Test successful image generation in URL mode\"\"\"\n        mock_dashscope = MagicMock()\n        mock_dashscope.ImageSynthesis.call.return_value.output = {\n            \"results\": [\n                {\"url\": \"https://example.com/image1.jpg\"},\n                {\"url\": \"https://example.com/image2.png\"},\n            ],\n        }\n\n        with patch.dict(\"sys.modules\", {\"dashscope\": mock_dashscope}):\n            result = dashscope_text_to_image(\n                prompt=\"A beautiful landscape\",\n                api_key=\"test_key\",\n                n=2,\n                use_base64=False,\n            )\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            ImageBlock(\n                type=\"image\",\n                source={\n                    \"type\": \"url\",\n                    \"url\": \"https://example.com/image1.jpg\",\n                },\n            ),\n            ImageBlock(\n                type=\"image\",\n                source={\n                    \"type\": \"url\",\n                    \"url\": \"https://example.com/image2.png\",\n                },\n            ),\n        ]\n\n    def test_text_to_image_success_base64_mode(self) -> None:\n        \"\"\"Test successful image generation in base64 mode\"\"\"\n        fake_image_data = b\"fake_image_data\"\n        expected_base64 = base64.b64encode(fake_image_data).decode(\"utf-8\")\n\n        mock_dashscope = MagicMock()\n        mock_dashscope.ImageSynthesis.call.return_value.output = {\n            \"results\": [{\"url\": \"https://example.com/image1.jpg\"}],\n        }\n\n        with (\n            patch.dict(\n                \"sys.modules\",\n                {\"dashscope\": mock_dashscope},\n            ),\n            patch(\n                \"agentscope.tool._multi_modality._dashscope_tools.\"\n                \"_get_bytes_from_web_url\",\n            ) as mock_get_bytes,\n        ):\n            mock_get_bytes.return_value = expected_base64\n            result = dashscope_text_to_image(\n                prompt=\"A beautiful landscape\",\n                api_key=\"test_key\",\n                n=1,\n                use_base64=True,\n            )\n\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            ImageBlock(\n                type=\"image\",\n                source={\n                    \"type\": \"base64\",\n                    \"media_type\": \"image/jpg\",\n                    \"data\": expected_base64,\n                },\n            ),\n        ]\n\n    def test_text_to_image_empty_results(self) -> None:\n        \"\"\"Test when empty results are returned\"\"\"\n        mock_dashscope = MagicMock()\n        mock_dashscope.ImageSynthesis.call.return_value.output = {\n            \"results\": [],\n        }\n\n        with patch.dict(\"sys.modules\", {\"dashscope\": mock_dashscope}):\n            result = dashscope_text_to_image(\n                prompt=\"A beautiful landscape\",\n                api_key=\"test_key\",\n            )\n\n        assert isinstance(result, ToolResponse)\n        assert len(result.content) == 0\n\n    def test_text_to_image_none_results(self) -> None:\n        \"\"\"Test when None results are returned\"\"\"\n        mock_dashscope = MagicMock()\n        mock_dashscope.ImageSynthesis.call.return_value.output = {}\n\n        with patch.dict(\"sys.modules\", {\"dashscope\": mock_dashscope}):\n            result = dashscope_text_to_image(\n                prompt=\"A beautiful landscape\",\n                api_key=\"test_key\",\n            )\n\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"Failed to generate images: 'results'\",\n            ),\n        ]\n\n    def test_text_to_image_exception(self) -> None:\n        \"\"\"Test exception handling\"\"\"\n        mock_dashscope = MagicMock()\n        mock_dashscope.ImageSynthesis.call.side_effect = Exception(\"API Error\")\n\n        with patch.dict(\"sys.modules\", {\"dashscope\": mock_dashscope}):\n            result = dashscope_text_to_image(\n                prompt=\"A beautiful landscape\",\n                api_key=\"test_key\",\n            )\n\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"Failed to generate images: API Error\",\n            ),\n        ]\n\n\nclass TestDashScopeImageToText:\n    \"\"\"Test cases for dashscope_image_to_text function\"\"\"\n\n    def test_image_to_text_single_url_success(self) -> None:\n        \"\"\"Test successful processing of single image URL\"\"\"\n        mock_dashscope = MagicMock()\n        mock_dashscope.MultiModalConversation.call.return_value.output = {\n            \"choices\": [\n                {\"message\": {\"content\": \"This is a beautiful landscape\"}},\n            ],\n        }\n\n        with patch.dict(\"sys.modules\", {\"dashscope\": mock_dashscope}):\n            result = dashscope_image_to_text(\n                image_urls=\"https://example.com/image.jpg\",\n                api_key=\"test_key\",\n            )\n\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"This is a beautiful landscape\",\n            ),\n        ]\n\n    def test_image_to_text_multiple_urls_success(self) -> None:\n        \"\"\"Test successful processing of multiple image URLs\"\"\"\n        mock_dashscope = MagicMock()\n        mock_dashscope.MultiModalConversation.call.return_value.output = {\n            \"choices\": [\n                {\"message\": {\"content\": \"Multiple images description\"}},\n            ],\n        }\n\n        with patch.dict(\"sys.modules\", {\"dashscope\": mock_dashscope}):\n            result = dashscope_image_to_text(\n                image_urls=[\n                    \"https://example.com/1.jpg\",\n                    \"https://example.com/2.jpg\",\n                ],\n                api_key=\"test_key\",\n            )\n\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"Multiple images description\",\n            ),\n        ]\n\n    def test_image_to_text_local_file_success(self) -> None:\n        \"\"\"Test successful processing of local file\"\"\"\n        mock_dashscope = MagicMock()\n        mock_dashscope.MultiModalConversation.call.return_value.output = {\n            \"choices\": [{\"message\": {\"content\": \"Local image description\"}}],\n        }\n\n        with patch.dict(\"sys.modules\", {\"dashscope\": mock_dashscope}), patch(\n            \"agentscope.tool._multi_modality._dashscope_tools.os.path.exists\",\n        ) as mock_exists, patch(\n            \"agentscope.tool._multi_modality._dashscope_tools.os.path.isfile\",\n        ) as mock_isfile, patch(\n            \"agentscope.tool._multi_modality._dashscope_tools.os.path.abspath\",\n        ) as mock_abspath:\n            mock_exists.return_value = True\n            mock_isfile.return_value = True\n            mock_abspath.return_value = \"/absolute/path/to/image.jpg\"\n\n            result = dashscope_image_to_text(\n                image_urls=\"./local_image.jpg\",\n                api_key=\"test_key\",\n            )\n\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"Local image description\",\n            ),\n        ]\n\n    def test_image_to_text_invalid_local_path(self) -> None:\n        \"\"\"Test error handling for invalid local path\"\"\"\n        with patch(\n            \"agentscope.tool._multi_modality._dashscope_tools.os.path.exists\",\n        ) as mock_exists, patch(\n            \"agentscope.tool._multi_modality._dashscope_tools.os.path.isfile\",\n        ) as mock_isfile:\n            mock_exists.return_value = True\n            mock_isfile.return_value = False\n\n            result = dashscope_image_to_text(\n                image_urls=\"./invalid_path\",\n                api_key=\"test_key\",\n            )\n\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text='Error: The input image url \"./invalid_path\" is not a '\n                \"file.\",\n            ),\n        ]\n\n    def test_image_to_text_list_content_response(self) -> None:\n        \"\"\"Test handling of list content in response\"\"\"\n        mock_dashscope = MagicMock()\n        mock_dashscope.MultiModalConversation.call.return_value.output = {\n            \"choices\": [\n                {\"message\": {\"content\": [{\"text\": \"Image description\"}]}},\n            ],\n        }\n\n        with patch.dict(\"sys.modules\", {\"dashscope\": mock_dashscope}):\n            result = dashscope_image_to_text(\n                image_urls=\"https://example.com/image.jpg\",\n                api_key=\"test_key\",\n            )\n\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"Image description\",\n            ),\n        ]\n\n    def test_image_to_text_none_content(self) -> None:\n        \"\"\"Test handling of None content in response\"\"\"\n        mock_dashscope = MagicMock()\n        mock_dashscope.MultiModalConversation.call.return_value.output = {\n            \"choices\": [{\"message\": {\"content\": None}}],\n        }\n\n        with patch.dict(\"sys.modules\", {\"dashscope\": mock_dashscope}):\n            result = dashscope_image_to_text(\n                image_urls=\"https://example.com/image.jpg\",\n                api_key=\"test_key\",\n            )\n\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"Error: Failed to generate text\",\n            ),\n        ]\n\n    def test_image_to_text_exception(self) -> None:\n        \"\"\"Test exception handling\"\"\"\n        mock_dashscope = MagicMock()\n        mock_dashscope.MultiModalConversation.call.side_effect = Exception(\n            \"API Error\",\n        )\n\n        with patch.dict(\"sys.modules\", {\"dashscope\": mock_dashscope}):\n            result = dashscope_image_to_text(\n                image_urls=\"https://example.com/image.jpg\",\n                api_key=\"test_key\",\n            )\n\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"Failed to generate text: API Error\",\n            ),\n        ]\n\n\nclass TestDashScopeTextToAudio:\n    \"\"\"Test cases for dashscope_text_to_audio function\"\"\"\n\n    def test_text_to_audio_success(self) -> None:\n        \"\"\"Test successful audio generation\"\"\"\n        fake_audio_data = b\"fake_audio_data\"\n        expected_base64 = base64.b64encode(fake_audio_data).decode(\"utf-8\")\n\n        mock_dashscope = MagicMock()\n        mock_response = Mock()\n        mock_response.get_audio_data.return_value = fake_audio_data\n        mock_dashscope.audio.tts.SpeechSynthesizer.call.return_value = (\n            mock_response\n        )\n\n        with patch.dict(\"sys.modules\", {\"dashscope\": mock_dashscope}):\n            result = dashscope_text_to_audio(\n                text=\"Hello world\",\n                api_key=\"test_key\",\n            )\n\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            AudioBlock(\n                type=\"audio\",\n                source={\n                    \"type\": \"base64\",\n                    \"media_type\": \"audio/wav\",\n                    \"data\": expected_base64,\n                },\n            ),\n        ]\n\n    def test_text_to_audio_no_data(self) -> None:\n        \"\"\"Test when no audio data is returned\"\"\"\n        mock_dashscope = MagicMock()\n        mock_response = Mock()\n        mock_response.get_audio_data.return_value = None\n        mock_dashscope.audio.tts.SpeechSynthesizer.call.return_value = (\n            mock_response\n        )\n\n        with patch.dict(\"sys.modules\", {\"dashscope\": mock_dashscope}):\n            result = dashscope_text_to_audio(\n                text=\"Hello world\",\n                api_key=\"test_key\",\n            )\n\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"Error: Failed to generate audio\",\n            ),\n        ]\n\n    def test_text_to_audio_api_exception(self) -> None:\n        \"\"\"Test exception handling during API call\"\"\"\n        mock_dashscope = MagicMock()\n        mock_dashscope.audio.tts.SpeechSynthesizer.call.side_effect = (\n            Exception(\n                \"TTS API Error\",\n            )\n        )\n\n        with patch.dict(\"sys.modules\", {\"dashscope\": mock_dashscope}):\n            result = dashscope_text_to_audio(\n                text=\"Hello world\",\n                api_key=\"test_key\",\n            )\n\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"Failed to generate audio: TTS API Error\",\n            ),\n        ]\n"
  },
  {
    "path": "tests/tool_openai_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unit tests for OpenAI tools\"\"\"\n\nimport base64\nfrom io import BytesIO\nfrom unittest.mock import Mock, patch, mock_open, MagicMock\n\nfrom agentscope.message import ImageBlock, TextBlock, AudioBlock\nfrom agentscope.tool import ToolResponse\nfrom agentscope.tool import (\n    openai_text_to_image,\n    openai_edit_image,\n    openai_create_image_variation,\n    openai_image_to_text,\n    openai_text_to_audio,\n    openai_audio_to_text,\n)\n\n\nclass TestOpenAITextToImage:\n    \"\"\"Test cases for openai_text_to_image function\"\"\"\n\n    def test_text_to_image_success_url_mode(self) -> None:\n        \"\"\"Test successful image generation in URL mode\"\"\"\n        mock_openai = MagicMock()\n        mock_client = Mock()\n        mock_openai.OpenAI.return_value = mock_client\n        mock_response = Mock()\n        mock_response.data = [\n            Mock(url=\"https://example.com/image1.jpg\"),\n            Mock(url=\"https://example.com/image2.png\"),\n        ]\n        mock_client.images.generate.return_value = mock_response\n\n        with patch.dict(\"sys.modules\", {\"openai\": mock_openai}):\n            result = openai_text_to_image(\n                prompt=\"A beautiful landscape\",\n                api_key=\"test_key\",\n                n=2,\n                response_format=\"url\",\n            )\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            ImageBlock(\n                type=\"image\",\n                source={\n                    \"type\": \"url\",\n                    \"url\": \"https://example.com/image1.jpg\",\n                },\n            ),\n            ImageBlock(\n                type=\"image\",\n                source={\n                    \"type\": \"url\",\n                    \"url\": \"https://example.com/image2.png\",\n                },\n            ),\n        ]\n\n    def test_text_to_image_success_base64_mode(self) -> None:\n        \"\"\"Test successful image generation in base64 mode\"\"\"\n        mock_openai = MagicMock()\n        mock_client = Mock()\n        mock_openai.OpenAI.return_value = mock_client\n        fake_base64_data = base64.b64encode(b\"fake_image_data\").decode(\"utf-8\")\n        mock_response = Mock()\n        mock_response.data = [Mock(b64_json=fake_base64_data)]\n        mock_client.images.generate.return_value = mock_response\n\n        with patch.dict(\"sys.modules\", {\"openai\": mock_openai}):\n            result = openai_text_to_image(\n                prompt=\"A beautiful landscape\",\n                api_key=\"test_key\",\n                n=1,\n                response_format=\"b64_json\",\n            )\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            ImageBlock(\n                type=\"image\",\n                source={\n                    \"type\": \"base64\",\n                    \"media_type\": \"image/png\",\n                    \"data\": fake_base64_data,\n                },\n            ),\n        ]\n\n    def test_text_to_image_gpt_image_1_force_base64(self) -> None:\n        \"\"\"Test gpt-image-1 forces base64 format\"\"\"\n        mock_openai = MagicMock()\n        mock_client = Mock()\n        mock_openai.OpenAI.return_value = mock_client\n        fake_base64_data = base64.b64encode(b\"fake_image_data\").decode(\"utf-8\")\n        mock_response = Mock()\n        mock_response.data = [Mock(b64_json=fake_base64_data)]\n        mock_client.images.generate.return_value = mock_response\n\n        with patch.dict(\"sys.modules\", {\"openai\": mock_openai}):\n            result = openai_text_to_image(\n                prompt=\"A beautiful landscape\",\n                api_key=\"test_key\",\n                model=\"gpt-image-1\",\n                response_format=\"url\",  # Should be ignored\n            )\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            ImageBlock(\n                type=\"image\",\n                source={\n                    \"type\": \"base64\",\n                    \"media_type\": \"image/png\",\n                    \"data\": fake_base64_data,\n                },\n            ),\n        ]\n\n    def test_text_to_image_exception(self) -> None:\n        \"\"\"Test exception handling\"\"\"\n        mock_openai = MagicMock()\n        mock_client = Mock()\n        mock_openai.OpenAI.return_value = mock_client\n        mock_client.images.generate.side_effect = Exception(\"API Error\")\n\n        with patch.dict(\"sys.modules\", {\"openai\": mock_openai}):\n            result = openai_text_to_image(\n                prompt=\"A beautiful landscape\",\n                api_key=\"test_key\",\n            )\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"Failed to generate image: API Error\",\n            ),\n        ]\n\n\nclass TestOpenAIEditImage:\n    \"\"\"Test cases for openai_edit_image function\"\"\"\n\n    def test_edit_image_success_url_input(self) -> None:\n        \"\"\"Test successful image editing with URL input\"\"\"\n        mock_openai = MagicMock()\n        mock_client = Mock()\n        mock_openai.OpenAI.return_value = mock_client\n        mock_api_response = Mock()\n        mock_api_response.data = [Mock(url=\"https://example.com/edited.jpg\")]\n        mock_client.images.edit.return_value = mock_api_response\n\n        with patch.dict(\"sys.modules\", {\"openai\": mock_openai}), patch(\n            \"agentscope.tool._multi_modality._openai_tools.requests.get\",\n        ) as mock_requests, patch(\"PIL.Image.open\") as mock_image_open:\n            mock_response = Mock()\n            mock_response.content = b\"fake_image_data\"\n            mock_response.raise_for_status.return_value = None\n            mock_requests.return_value = mock_response\n\n            mock_img = Mock()\n            mock_img.mode = \"RGB\"\n            mock_img.convert.return_value = mock_img\n            mock_image_open.return_value = mock_img\n\n            result = openai_edit_image(\n                image_url=\"https://example.com/image.jpg\",\n                prompt=\"Add a sunset\",\n                api_key=\"test_key\",\n                response_format=\"url\",\n            )\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            ImageBlock(\n                type=\"image\",\n                source={\n                    \"type\": \"url\",\n                    \"url\": \"https://example.com/edited.jpg\",\n                },\n            ),\n        ]\n\n    def test_edit_image_success_local_file(self) -> None:\n        \"\"\"Test successful image editing with local file\"\"\"\n        mock_openai = MagicMock()\n        mock_client = Mock()\n        mock_openai.OpenAI.return_value = mock_client\n        fake_base64_data = base64.b64encode(b\"fake_edited_data\").decode(\n            \"utf-8\",\n        )\n        mock_api_response = Mock()\n        mock_api_response.data = [Mock(b64_json=fake_base64_data)]\n        mock_client.images.edit.return_value = mock_api_response\n\n        with (\n            patch.dict(\"sys.modules\", {\"openai\": mock_openai}),\n            patch(\"PIL.Image.open\") as mock_image_open,\n        ):\n            mock_img = Mock()\n            mock_img.mode = \"RGBA\"  # Already RGBA\n            mock_image_open.return_value = mock_img\n\n            result = openai_edit_image(\n                image_url=\"./local_image.jpg\",\n                prompt=\"Add a sunset\",\n                api_key=\"test_key\",\n                model=\"gpt-image-1\",  # Forces base64\n            )\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            ImageBlock(\n                type=\"image\",\n                source={\n                    \"type\": \"base64\",\n                    \"media_type\": \"image/png\",\n                    \"data\": fake_base64_data,\n                },\n            ),\n        ]\n\n    def test_edit_image_exception(self) -> None:\n        \"\"\"Test exception handling\"\"\"\n        mock_openai = MagicMock()\n        mock_client = Mock()\n        mock_openai.OpenAI.return_value = mock_client\n        mock_client.images.edit.side_effect = Exception(\"Edit Error\")\n\n        with (\n            patch.dict(\"sys.modules\", {\"openai\": mock_openai}),\n            patch(\"PIL.Image.open\") as mock_image_open,\n        ):\n            mock_img = Mock()\n            mock_img.mode = \"RGB\"\n            mock_img.convert.return_value = mock_img\n            mock_image_open.return_value = mock_img\n\n            result = openai_edit_image(\n                image_url=\"./image.jpg\",\n                prompt=\"Edit image\",\n                api_key=\"test_key\",\n            )\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"Failed to generate image: Edit Error\",\n            ),\n        ]\n\n    def test_edit_image_file_not_found(self) -> None:\n        \"\"\"Test file not found error handling\"\"\"\n        mock_openai = MagicMock()\n        mock_client = Mock()\n        mock_openai.OpenAI.return_value = mock_client\n\n        with patch.dict(\"sys.modules\", {\"openai\": mock_openai}):\n            result = openai_edit_image(\n                image_url=\"./nonexistent_image.jpg\",\n                prompt=\"Edit image\",\n                api_key=\"test_key\",\n            )\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"Failed to generate image: [Errno 2] No such file or \"\n                \"directory: './nonexistent_image.jpg'\",\n            ),\n        ]\n\n\nclass TestOpenAICreateImageVariation:\n    \"\"\"Test cases for openai_create_image_variation function\"\"\"\n\n    def test_create_variation_success_url_mode(self) -> None:\n        \"\"\"Test successful image variation creation in URL mode\"\"\"\n        mock_openai = MagicMock()\n        mock_client = Mock()\n        mock_openai.OpenAI.return_value = mock_client\n        mock_response = Mock()\n        mock_response.data = [\n            Mock(url=\"https://example.com/variation1.jpg\"),\n            Mock(url=\"https://example.com/variation2.jpg\"),\n        ]\n        mock_client.images.create_variation.return_value = mock_response\n\n        with patch.dict(\"sys.modules\", {\"openai\": mock_openai}), patch(\n            \"agentscope.tool._multi_modality._openai_tools._parse_url\",\n        ) as mock_parse_url:\n            mock_parse_url.return_value = BytesIO(b\"fake_image_data\")\n\n            result = openai_create_image_variation(\n                image_url=\"https://example.com/image.jpg\",\n                api_key=\"test_key\",\n                n=2,\n                response_format=\"url\",\n            )\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            ImageBlock(\n                type=\"image\",\n                source={\n                    \"type\": \"url\",\n                    \"url\": \"https://example.com/variation1.jpg\",\n                },\n            ),\n            ImageBlock(\n                type=\"image\",\n                source={\n                    \"type\": \"url\",\n                    \"url\": \"https://example.com/variation2.jpg\",\n                },\n            ),\n        ]\n\n    def test_create_variation_success_base64_mode(self) -> None:\n        \"\"\"Test successful image variation creation in base64 mode\"\"\"\n        mock_openai = MagicMock()\n        mock_client = Mock()\n        mock_openai.OpenAI.return_value = mock_client\n        fake_base64_data = base64.b64encode(b\"fake_variation\").decode(\"utf-8\")\n        mock_response = Mock()\n        mock_response.data = [Mock(b64_json=fake_base64_data)]\n        mock_client.images.create_variation.return_value = mock_response\n\n        with patch.dict(\"sys.modules\", {\"openai\": mock_openai}), patch(\n            \"agentscope.tool._multi_modality._openai_tools._parse_url\",\n        ) as mock_parse_url:\n            mock_parse_url.return_value = BytesIO(b\"fake_image_data\")\n\n            result = openai_create_image_variation(\n                image_url=\"./image.jpg\",\n                api_key=\"test_key\",\n                response_format=\"b64_json\",\n            )\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            ImageBlock(\n                type=\"image\",\n                source={\n                    \"type\": \"base64\",\n                    \"media_type\": \"image/png\",\n                    \"data\": fake_base64_data,\n                },\n            ),\n        ]\n\n    def test_create_variation_exception(self) -> None:\n        \"\"\"Test exception handling\"\"\"\n        mock_openai = MagicMock()\n        mock_client = Mock()\n        mock_openai.OpenAI.return_value = mock_client\n        mock_client.images.create_variation.side_effect = Exception(\n            \"Variation Error\",\n        )\n\n        with patch.dict(\"sys.modules\", {\"openai\": mock_openai}), patch(\n            \"agentscope.tool._multi_modality._openai_tools._parse_url\",\n        ) as mock_parse_url:\n            mock_parse_url.return_value = BytesIO(b\"fake_image_data\")\n\n            result = openai_create_image_variation(\n                image_url=\"./image.jpg\",\n                api_key=\"test_key\",\n            )\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"Failed to generate image: Variation Error\",\n            ),\n        ]\n\n\nclass TestOpenAIImageToText:\n    \"\"\"Test cases for openai_image_to_text function\"\"\"\n\n    def test_image_to_text_single_url_success(self) -> None:\n        \"\"\"Test successful processing of single image URL\"\"\"\n        mock_openai = MagicMock()\n        mock_client = Mock()\n        mock_openai.OpenAI.return_value = mock_client\n        mock_message = Mock()\n        mock_message.content = \"This is a beautiful landscape\"\n        mock_response = Mock()\n        mock_response.choices = [Mock(message=mock_message)]\n        mock_client.chat.completions.create.return_value = mock_response\n\n        with (\n            patch.dict(\"sys.modules\", {\"openai\": mock_openai}),\n            patch(\n                \"agentscope.tool._multi_modality._openai_tools.\"\n                \"_to_openai_image_url\",\n            ) as mock_to_url,\n        ):\n            mock_to_url.return_value = \"https://example.com/image.jpg\"\n\n            result = openai_image_to_text(\n                image_urls=\"https://example.com/image.jpg\",\n                api_key=\"test_key\",\n            )\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"This is a beautiful landscape\",\n            ),\n        ]\n\n    def test_image_to_text_multiple_urls_success(self) -> None:\n        \"\"\"Test successful processing of multiple image URLs\"\"\"\n        mock_openai = MagicMock()\n        mock_client = Mock()\n        mock_openai.OpenAI.return_value = mock_client\n        mock_message = Mock()\n        mock_message.content = \"Multiple images description\"\n        mock_response = Mock()\n        mock_response.choices = [Mock(message=mock_message)]\n        mock_client.chat.completions.create.return_value = mock_response\n\n        with patch.dict(\"sys.modules\", {\"openai\": mock_openai}), patch(\n            \"agentscope.tool._multi_modality._openai_tools.\"\n            \"_to_openai_image_url\",\n        ) as mock_to_url:\n            mock_to_url.side_effect = [\n                \"https://example.com/1.jpg\",\n                \"https://example.com/2.jpg\",\n            ]\n\n            result = openai_image_to_text(\n                image_urls=[\n                    \"https://example.com/1.jpg\",\n                    \"https://example.com/2.jpg\",\n                ],\n                api_key=\"test_key\",\n            )\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"Multiple images description\",\n            ),\n        ]\n\n    def test_image_to_text_exception(self) -> None:\n        \"\"\"Test exception handling\"\"\"\n        mock_openai = MagicMock()\n        mock_client = Mock()\n        mock_openai.OpenAI.return_value = mock_client\n        mock_client.chat.completions.create.side_effect = Exception(\n            \"Vision API Error\",\n        )\n\n        with (\n            patch.dict(\n                \"sys.modules\",\n                {\"openai\": mock_openai},\n            ),\n            patch(\n                \"agentscope.tool._multi_modality._openai_tools.\"\n                \"_to_openai_image_url\",\n            ) as mock_to_url,\n        ):\n            mock_to_url.return_value = \"https://example.com/image.jpg\"\n\n            result = openai_image_to_text(\n                image_urls=\"https://example.com/image.jpg\",\n                api_key=\"test_key\",\n            )\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"Failed to generate text: Vision API Error\",\n            ),\n        ]\n\n\nclass TestOpenAITextToAudio:\n    \"\"\"Test cases for openai_text_to_audio function\"\"\"\n\n    def test_text_to_audio_success(self) -> None:\n        \"\"\"Test successful audio generation\"\"\"\n        fake_audio_data = b\"fake_audio_data\"\n        mock_openai = MagicMock()\n        mock_client = Mock()\n        mock_openai.OpenAI.return_value = mock_client\n        mock_response = Mock()\n        mock_response.content = fake_audio_data\n        mock_client.audio.speech.create.return_value = mock_response\n\n        with patch.dict(\"sys.modules\", {\"openai\": mock_openai}):\n            result = openai_text_to_audio(\n                text=\"Hello world\",\n                api_key=\"test_key\",\n            )\n        expected_base64 = base64.b64encode(fake_audio_data).decode(\"utf-8\")\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            AudioBlock(\n                type=\"audio\",\n                source={\n                    \"type\": \"base64\",\n                    \"media_type\": \"audio/mp3\",\n                    \"data\": expected_base64,\n                },\n            ),\n        ]\n\n    def test_text_to_audio_different_format(self) -> None:\n        \"\"\"Test audio generation with different format\"\"\"\n        fake_audio_data = b\"fake_wav_data\"\n        mock_openai = MagicMock()\n        mock_client = Mock()\n        mock_openai.OpenAI.return_value = mock_client\n        mock_response = Mock()\n        mock_response.content = fake_audio_data\n        mock_client.audio.speech.create.return_value = mock_response\n\n        with patch.dict(\"sys.modules\", {\"openai\": mock_openai}):\n            result = openai_text_to_audio(\n                text=\"Hello world\",\n                api_key=\"test_key\",\n                res_format=\"wav\",\n                voice=\"echo\",\n                speed=1.2,\n            )\n        expected_base64 = base64.b64encode(fake_audio_data).decode(\"utf-8\")\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            AudioBlock(\n                type=\"audio\",\n                source={\n                    \"type\": \"base64\",\n                    \"media_type\": \"audio/wav\",\n                    \"data\": expected_base64,\n                },\n            ),\n        ]\n\n    def test_text_to_audio_exception(self) -> None:\n        \"\"\"Test exception handling\"\"\"\n        mock_openai = MagicMock()\n        mock_client = Mock()\n        mock_openai.OpenAI.return_value = mock_client\n        mock_client.audio.speech.create.side_effect = Exception(\n            \"TTS API Error\",\n        )\n\n        with patch.dict(\"sys.modules\", {\"openai\": mock_openai}):\n            result = openai_text_to_audio(\n                text=\"Hello world\",\n                api_key=\"test_key\",\n            )\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"Error: Failed to generate audio. TTS API Error\",\n            ),\n        ]\n\n\nclass TestOpenAIAudioToText:\n    \"\"\"Test cases for openai_audio_to_text function\"\"\"\n\n    def test_audio_to_text_local_file_success(self) -> None:\n        \"\"\"Test successful processing of local audio file\"\"\"\n        mock_openai = MagicMock()\n        mock_client = Mock()\n        mock_openai.OpenAI.return_value = mock_client\n        mock_transcription = Mock()\n        mock_transcription.text = \"Hello, this is a test transcription\"\n        mock_client.audio.transcriptions.create.return_value = (\n            mock_transcription\n        )\n\n        with (\n            patch.dict(\"sys.modules\", {\"openai\": mock_openai}),\n            patch(\n                \"agentscope.tool._multi_modality._openai_tools.os.path.exists\",\n            ) as mock_exists,\n            patch(\n                \"builtins.open\",\n                mock_open(read_data=b\"fake_audio_data\"),\n            ),\n        ):\n            mock_exists.return_value = True\n\n            result = openai_audio_to_text(\n                audio_file_url=\"./audio.mp3\",\n                api_key=\"test_key\",\n            )\n\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"Hello, this is a test transcription\",\n            ),\n        ]\n\n    def test_audio_to_text_url_success(self) -> None:\n        \"\"\"Test successful processing of audio URL\"\"\"\n        mock_openai = MagicMock()\n        mock_client = Mock()\n        mock_openai.OpenAI.return_value = mock_client\n        mock_transcription = Mock()\n        mock_transcription.text = \"URL audio transcription\"\n        mock_client.audio.transcriptions.create.return_value = (\n            mock_transcription\n        )\n\n        with patch.dict(\"sys.modules\", {\"openai\": mock_openai}), patch(\n            \"agentscope.tool._multi_modality._openai_tools.requests.get\",\n        ) as mock_requests:\n            # Mock requests\n            mock_response = Mock()\n            mock_response.content = b\"fake_audio_data\"\n            mock_response.raise_for_status.return_value = None\n            mock_requests.return_value = mock_response\n\n            result = openai_audio_to_text(\n                audio_file_url=\"https://example.com/audio.mp3\",\n                api_key=\"test_key\",\n                language=\"zh\",\n                temperature=0.5,\n            )\n\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"URL audio transcription\",\n            ),\n        ]\n\n    def test_audio_to_text_file_not_found(self) -> None:\n        \"\"\"Test error handling when local file not found\"\"\"\n        with patch(\n            \"agentscope.tool._multi_modality._openai_tools.os.path.exists\",\n        ) as mock_exists:\n            mock_exists.return_value = False\n\n            result = openai_audio_to_text(\n                audio_file_url=\"./nonexistent.mp3\",\n                api_key=\"test_key\",\n            )\n\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"Error: Failed to transcribe audio: File not found: \"\n                \"./nonexistent.mp3\",\n            ),\n        ]\n\n    def test_audio_to_text_api_exception(self) -> None:\n        \"\"\"Test exception handling during API call\"\"\"\n        mock_openai = MagicMock()\n        mock_client = Mock()\n        mock_openai.OpenAI.return_value = mock_client\n        mock_client.audio.transcriptions.create.side_effect = Exception(\n            \"Transcription Error\",\n        )\n\n        with (\n            patch.dict(\"sys.modules\", {\"openai\": mock_openai}),\n            patch(\n                \"agentscope.tool._multi_modality._openai_tools.requests.get\",\n            ) as mock_requests,\n        ):\n            # Mock requests\n            mock_response = Mock()\n            mock_response.content = b\"fake_audio_data\"\n            mock_response.raise_for_status.return_value = None\n            mock_requests.return_value = mock_response\n\n            result = openai_audio_to_text(\n                audio_file_url=\"https://example.com/audio.mp3\",\n                api_key=\"test_key\",\n            )\n\n        assert isinstance(result, ToolResponse)\n        assert result.content == [\n            TextBlock(\n                type=\"text\",\n                text=\"Error: Failed to transcribe audio: Transcription Error\",\n            ),\n        ]\n"
  },
  {
    "path": "tests/tool_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The tool module unit tests\"\"\"\n\nimport os\nimport platform\nimport sys\nimport tempfile\nfrom unittest import IsolatedAsyncioTestCase\n\nimport shortuuid\n\nfrom agentscope.tool import (\n    execute_python_code,\n    execute_shell_command,\n    view_text_file,\n    write_text_file,\n    insert_text_file,\n)\n\n\nclass ToolTest(IsolatedAsyncioTestCase):\n    \"\"\"Test cases for the tool module.\"\"\"\n\n    def setUp(self) -> None:\n        \"\"\"Set up the test environment.\"\"\"\n        self.path_file = \"./tmp.txt\"\n        if os.path.exists(self.path_file):\n            os.remove(self.path_file)\n\n    async def test_execute_python_code(self) -> None:\n        \"\"\"Test executing Python code.\"\"\"\n\n        # empty output\n        res = await execute_python_code(code=\"a = 1 + 1\")\n        self.assertEqual(\n            \"<returncode>0</returncode>\"\n            \"<stdout></stdout>\"\n            \"<stderr></stderr>\",\n            res.content[0][\"text\"],\n        )\n\n        # with output\n        res = await execute_python_code(code=\"print('Hello, World!')\")\n\n        actual = res.content[0][\"text\"].replace(\"\\r\\n\", \"\\n\")\n        self.assertEqual(\n            \"<returncode>0</returncode>\"\n            \"<stdout>Hello, World!\\n</stdout>\"\n            \"<stderr></stderr>\",\n            actual,\n        )\n\n        # with exception\n        res = await execute_python_code(code=\"raise Exception('Test error')\")\n        actual = res.content[0][\"text\"].replace(\"\\r\\n\", \"\\n\")\n\n        self.assertTrue(\n            actual.startswith(\n                \"<returncode>1</returncode>\"\n                \"<stdout></stdout>\"\n                \"<stderr>Traceback (most recent call last):\\n  File \",\n            ),\n        )\n        self.assertTrue(\n            actual.endswith(\n                '.py\", line 1, in <module>\\n'\n                \"    raise Exception('Test error')\\n\"\n                \"Exception: Test error\\n\"\n                \"</stderr>\",\n            ),\n        )\n\n        # with timeout\n        code = \"\"\"print(\"123\")\nimport time\ntime.sleep(5)\nprint(\"456\")\"\"\"\n\n        res = await execute_python_code(code)\n        actual = res.content[0][\"text\"].replace(\"\\r\\n\", \"\\n\")\n        self.assertEqual(\n            \"<returncode>0</returncode>\"\n            \"<stdout>123\\n456\\n</stdout>\"\n            \"<stderr></stderr>\",\n            actual,\n        )\n\n        res = await execute_python_code(code, timeout=2)\n        actual = res.content[0][\"text\"].replace(\"\\r\\n\", \"\\n\")\n        self.assertEqual(\n            \"<returncode>-1</returncode>\"\n            \"<stdout>123\\n</stdout>\"\n            \"<stderr>TimeoutError: The code execution exceeded the \"\n            \"timeout of 2 seconds.</stderr>\",\n            actual,\n        )\n\n    async def test_execute_shell_command(self) -> None:\n        \"\"\"Test executing shell command.\"\"\"\n        # empty output\n        python_echo_cmd = f\"{sys.executable} -c \\\"print('Hello, World!')\\\"\"\n        res = await execute_shell_command(command=python_echo_cmd)\n        actual = res.content[0][\"text\"].replace(\"\\r\\n\", \"\\n\")\n        self.assertEqual(\n            \"<returncode>0</returncode>\"\n            \"<stdout>Hello, World!\\n</stdout>\"\n            \"<stderr></stderr>\",\n            actual,\n        )\n\n        # with exception\n        res = await execute_shell_command(command=\"non_existent_command\")\n        assert any(\n            keyword in res.content[0][\"text\"].lower()\n            for keyword in [\"not found\", \"is not recognized\"]\n        )\n\n        # without timeout\n        normal_cmd = (\n            f\"{sys.executable} -c \\\"\"  # fmt: skip\n            f\"import time; print('123'); \"\n            f\"time.sleep(0.1); print('456')\\\"\"\n        )\n\n        res = await execute_shell_command(\n            command=normal_cmd,\n        )\n        actual = res.content[0][\"text\"].replace(\"\\r\\n\", \"\\n\")\n        self.assertEqual(\n            \"<returncode>0</returncode>\"\n            \"<stdout>123\\n456\\n</stdout>\"\n            \"<stderr></stderr>\",\n            actual,\n        )\n\n        # with timeout\n        if platform.system() == \"Windows\":\n            return\n        else:\n            timeout_cmd = 'echo \"123\"; sleep 5; echo \"456\"'\n\n        res = await execute_shell_command(\n            command=timeout_cmd,\n            timeout=2,\n        )\n        actual = res.content[0][\"text\"].replace(\"\\r\\n\", \"\\n\")\n        self.assertEqual(\n            \"<returncode>-1</returncode>\"\n            \"<stdout>123\\n</stdout>\"\n            \"<stderr>TimeoutError: The command execution exceeded \"\n            \"the timeout of 2 seconds.</stderr>\",\n            actual,\n        )\n\n    async def test_view_text_file(self) -> None:\n        \"\"\"Test viewing text file.\"\"\"\n        with tempfile.TemporaryDirectory() as temp_dir:\n            temp_file = os.path.join(temp_dir, f\"tmp_{shortuuid.uuid()}.txt\")\n            with open(temp_file, \"w\", encoding=\"utf-8\") as f:\n                f.write(\"\"\"1\\n2\\n3\\n4\\n5\\n6\\n7\\n8\\n9\\n10\\n\"\"\")\n\n            # View the whole file\n            res = await view_text_file(file_path=temp_file)\n            self.assertEqual(\n                f\"The content of {temp_file}:\\n```\\n1: 1\\n2: 2\\n3: 3\\n\"\n                f\"4: 4\\n5: 5\\n6: 6\\n7: 7\\n8: 8\\n9: 9\\n10: 10\\n```\",\n                res.content[0][\"text\"],\n            )\n\n            # View a specific range\n            res = await view_text_file(temp_file, ranges=[3, 5])\n            self.assertEqual(\n                f\"The content of {temp_file} in [3, 5] lines:\\n\"\n                f\"```\\n3: 3\\n4: 4\\n5: 5\\n```\",\n                res.content[0][\"text\"],\n            )\n\n            # View a range that exceeds the file length\n            res = await view_text_file(temp_file, ranges=[8, 13])\n            self.assertEqual(\n                f\"The content of {temp_file} in [8, 13] lines:\\n\"\n                f\"```\\n8: 8\\n9: 9\\n10: 10\\n```\",\n                res.content[0][\"text\"],\n            )\n\n            # View a range that is invalid\n            res = await view_text_file(temp_file, ranges=[11, 13])\n            self.assertEqual(\n                f\"InvalidArgumentError: The range '[11, 13]' is out of \"\n                f\"bounds for the file '{temp_file}', which has only 10 lines.\",\n                res.content[0][\"text\"],\n            )\n\n            # View invalid file path\n            res = await view_text_file(file_path=\"non_existent_file.txt\")\n            self.assertEqual(\n                \"Error: The file non_existent_file.txt does not exist.\",\n                res.content[0][\"text\"],\n            )\n\n            # View a non-file path\n            res = await view_text_file(\"/\")\n            self.assertEqual(\n                \"Error: The path / is not a file.\",\n                res.content[0][\"text\"],\n            )\n\n        # Test tilde expansion: path with ~ should resolve to user home\n        home = os.path.expanduser(\"~\")\n        test_name = f\".agentscope_tool_test_{shortuuid.uuid()}.txt\"\n        tilde_path = f\"~/{test_name}\"\n        real_path = os.path.join(home, test_name)\n        try:\n            with open(real_path, \"w\", encoding=\"utf-8\") as f:\n                f.write(\"tilde expansion works\\n\")\n            res = await view_text_file(file_path=tilde_path)\n            self.assertIn(\"tilde expansion works\", res.content[0][\"text\"])\n            self.assertIn(\"1: tilde expansion works\", res.content[0][\"text\"])\n        finally:\n            if os.path.exists(real_path):\n                os.remove(real_path)\n\n    async def test_write_text_file(self) -> None:\n        \"\"\"Test writing to text file.\"\"\"\n        # create and write a new file\n        res = await write_text_file(\n            self.path_file,\n            \"a\\nb\\nc\\n\",\n            None,\n        )\n        self.assertEqual(\n            \"Create and write ./tmp.txt successfully.\",\n            res.content[0][\"text\"],\n        )\n\n        # replace content\n        res = await write_text_file(\n            self.path_file,\n            \"d\\n\",\n            [2, 2],\n        )\n        self.assertEqual(\n            \"Write ./tmp.txt successfully. The new content snippet:\\n\"\n            \"```\\n1: a\\n2: d\\n3: c\\n```\",\n            res.content[0][\"text\"],\n        )\n\n    async def test_insert_text_file(self) -> None:\n        \"\"\"Test inserting text into a file.\"\"\"\n        with open(self.path_file, \"w\", encoding=\"utf-8\") as f:\n            f.write(\"\\n\".join([str(_) for _ in range(50)]))\n        res = await insert_text_file(\n            self.path_file,\n            \"d\",\n            line_number=1,\n        )\n        self.assertEqual(\n            res.content[0][\"text\"],\n            \"Insert content into ./tmp.txt at line 1 successfully. \"\n            \"The new content between lines 1-7 is:\\n\"\n            \"```\\n\"\n            \"1: d\\n\"\n            \"2: 0\\n\"\n            \"3: 1\\n\"\n            \"4: 2\\n\"\n            \"5: 3\\n\"\n            \"6: 4\\n\"\n            \"7: 5\\n\"\n            \"```\",\n        )\n\n        res = await insert_text_file(\n            self.path_file,\n            \"e\",\n            line_number=25,\n        )\n        self.assertEqual(\n            res.content[0][\"text\"],\n            \"Insert content into ./tmp.txt at line 25 successfully. \"\n            \"The new content between lines 20-31 is:\\n\"\n            \"```\\n\"\n            \"20: 18\\n\"\n            \"21: 19\\n\"\n            \"22: 20\\n\"\n            \"23: 21\\n\"\n            \"24: 22\\n\"\n            \"25: e\\n\"\n            \"26: 23\\n\"\n            \"27: 24\\n\"\n            \"28: 25\\n\"\n            \"29: 26\\n\"\n            \"30: 27\\n\"\n            \"31: 28\\n\"\n            \"```\",\n        )\n\n        res = await insert_text_file(\n            self.path_file,\n            \"\\n\".join([\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"]),\n            line_number=25,\n        )\n        self.assertEqual(\n            res.content[0][\"text\"],\n            \"Insert content into ./tmp.txt at line 25 successfully. \"\n            \"The new content between lines 20-40 is:\\n\"\n            \"```\\n\"\n            \"20: 18\\n\"\n            \"21: 19\\n\"\n            \"22: 20\\n\"\n            \"23: 21\\n\"\n            \"24: 22\\n\"\n            \"25: a\\n\"\n            \"26: b\\n\"\n            \"27: c\\n\"\n            \"28: d\\n\"\n            \"29: e\\n\"\n            \"30: f\\n\"\n            \"31: g\\n\"\n            \"32: h\\n\"\n            \"33: i\\n\"\n            \"34: j\\n\"\n            \"35: e\\n\"\n            \"36: 23\\n\"\n            \"37: 24\\n\"\n            \"38: 25\\n\"\n            \"39: 26\\n\"\n            \"40: 27\\n\"\n            \"```\",\n        )\n\n        res = await insert_text_file(\n            self.path_file,\n            \"The\\nlast\\nline\",\n            63,\n        )\n        self.assertEqual(\n            res.content[0][\"text\"],\n            \"Insert content into ./tmp.txt at line 63 successfully. \"\n            \"The new content between lines 58-65 is:\\n\"\n            \"```\\n\"\n            \"58: 45\\n\"\n            \"59: 46\\n\"\n            \"60: 47\\n\"\n            \"61: 48\\n\"\n            \"62: 49\\n\"\n            \"63: The\\n\"\n            \"64: last\\n\"\n            \"65: line```\",\n        )\n\n        res = await insert_text_file(\n            self.path_file,\n            \"end\\nof\\ntest\",\n            100,\n        )\n        self.assertEqual(\n            res.content[0][\"text\"],\n            \"InvalidArgumentsError: The given line_number (100) is \"\n            \"not in the valid range [1, 66].\",\n        )\n\n    def tearDown(self) -> None:\n        \"\"\"Clean up after tests.\"\"\"\n        if os.path.exists(self.path_file):\n            os.remove(self.path_file)\n"
  },
  {
    "path": "tests/toolkit_basic_test.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=too-many-lines\n# mypy: disable-error-code=\"index\"\n\"\"\"Test toolkit module in agentscope.\"\"\"\nimport asyncio\nimport time\nfrom copy import deepcopy\nfrom functools import partial\nfrom typing import Union, Optional, Any, AsyncGenerator, Generator, Tuple\nfrom unittest import IsolatedAsyncioTestCase\n\nfrom pydantic import BaseModel, Field\n\nfrom agentscope.message import ToolUseBlock, TextBlock\nfrom agentscope.tool import ToolResponse, Toolkit\n\n\nasync def aenumerate(\n    agen: AsyncGenerator[ToolResponse, None],\n) -> AsyncGenerator[Tuple[int, ToolResponse], None]:\n    \"\"\"Asynchronous enumerate function.\"\"\"\n    n = 0\n    async for item in agen:\n        yield n, item\n        n += 1\n\n\nresponse1 = ToolResponse(\n    content=[TextBlock(type=\"text\", text=\"1\")],\n    stream=True,\n)\nresponse2 = ToolResponse(\n    content=[TextBlock(type=\"text\", text=\"12\")],\n    stream=True,\n)\nresponse3 = ToolResponse(\n    content=[TextBlock(type=\"text\", text=\"123\")],\n    is_last=True,\n)\n\n\nasync def async_func(raise_cancel: bool) -> ToolResponse:\n    \"\"\"An async function for testing.\"\"\"\n    if raise_cancel:\n        await asyncio.sleep(1)\n        raise asyncio.CancelledError(\"test\")\n    return response1\n\n\ndef sync_func(\n    arg1: int,\n    arg2: Optional[list[Union[str, int]]] = None,\n) -> ToolResponse:\n    \"\"\"A sync function for testing.\n\n    Long description.\n\n    Args:\n        arg1 (`int`):\n            Test argument 1.\n        arg2 (`Optional[list[Union[str, int]]]`, defaults to `None`):\n            Test argument 2.\n    \"\"\"\n    time.sleep(1)\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=f\"arg1: {arg1}, arg2: {arg2}\",\n            ),\n        ],\n    )\n\n\nclass TestCls:\n    \"\"\"A test class for testing.\"\"\"\n\n    def sync_func(self) -> ToolResponse:\n        \"\"\"A duplicate sync function for testing.\"\"\"\n        return ToolResponse(\n            content=[\n                TextBlock(\n                    type=\"text\",\n                    text=\"test\",\n                ),\n            ],\n        )\n\n\nasync def async_generator_func(\n    raise_cancel: bool,\n) -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"An async generator function for testing.\"\"\"\n    yield response1\n    yield deepcopy(response2)\n    if raise_cancel:\n        await asyncio.sleep(1)\n        raise asyncio.CancelledError(\"test\")\n    yield response3\n\n\nasync def async_func_return_async_generator(\n    raise_cancel: bool,\n) -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"Async function that returns async generator\"\"\"\n    return async_generator_func(raise_cancel=raise_cancel)\n\n\nasync def async_func_return_sync_generator() -> Generator[\n    ToolResponse,\n    None,\n    None,\n]:\n    \"\"\"Async function that returns sync generator\"\"\"\n    return sync_generator_func()\n\n\ndef sync_generator_func() -> Generator[ToolResponse, None, None]:\n    \"\"\"A sync generator function for testing.\"\"\"\n    yield response1\n    yield response2\n    yield response3\n\n\nclass StructuredModel(BaseModel):\n    \"\"\"Test structured model\"\"\"\n\n    arg3: int = Field(description=\"Test argument 3.\")\n\n\nclass MyBaseModel1(BaseModel):\n    \"\"\"A base model for testing nested $defs merging.\"\"\"\n\n    c: int = Field(description=\"Field c\")\n\n\nclass MyBaseModel2(BaseModel):\n    \"\"\"A base model that contains nested MyBaseModel1.\"\"\"\n\n    b: list[MyBaseModel1] = Field(description=\"List of MyBaseModel1\")\n\n\nclass ExtendedModelReusingBaseModel(BaseModel):\n    \"\"\"Extended model that reuses the same BaseModel from original function.\"\"\"\n\n    another_model: MyBaseModel2 = Field(description=\"Reusing MyBaseModel2\")\n    extra_field: str = Field(description=\"Extra field\")\n\n\nclass ToolkitBasicTest(IsolatedAsyncioTestCase):\n    \"\"\"Basic unittests for the toolkit module.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the test environment before each test.\"\"\"\n        self.toolkit = Toolkit()\n\n        self.sync_func_schema = {\n            \"type\": \"function\",\n            \"function\": {\n                \"name\": \"sync_func\",\n                \"parameters\": {\n                    \"properties\": {\n                        \"arg1\": {\n                            \"description\": \"Test argument 1.\",\n                            \"type\": \"integer\",\n                        },\n                        \"arg2\": {\n                            \"anyOf\": [\n                                {\n                                    \"items\": {\n                                        \"anyOf\": [\n                                            {\"type\": \"string\"},\n                                            {\"type\": \"integer\"},\n                                        ],\n                                    },\n                                    \"type\": \"array\",\n                                },\n                                {\"type\": \"null\"},\n                            ],\n                            \"default\": None,\n                            \"description\": \"Test argument 2.\",\n                        },\n                    },\n                    \"required\": [\"arg1\"],\n                    \"type\": \"object\",\n                },\n                \"description\": \"A sync function for testing.\\n\"\n                \"Long description.\",\n            },\n        }\n\n    async def test_duplicate_tool_registration(self) -> None:\n        \"\"\"Test duplicate tool function registration.\"\"\"\n        tool_call = ToolUseBlock(\n            type=\"tool_use\",\n            id=\"123\",\n            name=\"sync_func\",\n            input={\n                \"arg1\": 55,\n            },\n        )\n\n        # Add a function\n        self.toolkit.register_tool_function(\n            sync_func,\n        )\n        self.assertListEqual(\n            [self.sync_func_schema],\n            self.toolkit.get_json_schemas(),\n        )\n        async for chunk in await self.toolkit.call_tool_function(tool_call):\n            self.assertListEqual(\n                chunk.content,\n                [\n                    TextBlock(\n                        type=\"text\",\n                        text=\"arg1: 55, arg2: None\",\n                    ),\n                ],\n            )\n\n        test = TestCls()\n\n        # Try to add the same function with raise strategy\n        with self.assertRaises(ValueError):\n            self.toolkit.register_tool_function(test.sync_func)\n\n        # Try to add the same function with skip strategy\n        self.toolkit.register_tool_function(\n            test.sync_func,\n            namesake_strategy=\"skip\",\n        )\n        self.assertListEqual(\n            [self.sync_func_schema],\n            self.toolkit.get_json_schemas(),\n        )\n\n        # Try to add the same function with rename strategy\n        self.toolkit.register_tool_function(\n            test.sync_func,\n            namesake_strategy=\"rename\",\n        )\n        new_func_name = list(self.toolkit.tools.keys())[1]\n        new_func_schema = {\n            \"type\": \"function\",\n            \"function\": {\n                \"name\": new_func_name,\n                \"parameters\": {\n                    \"properties\": {},\n                    \"type\": \"object\",\n                },\n                \"description\": \"A duplicate sync function for testing.\",\n            },\n        }\n        self.assertListEqual(\n            [\n                self.sync_func_schema,\n                new_func_schema,\n            ],\n            self.toolkit.get_json_schemas(),\n        )\n        self.assertTrue(new_func_name.startswith(\"sync_func_\"))\n        res = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"123\",\n                name=new_func_name,\n                input={},\n            ),\n        )\n        async for chunk in res:\n            self.assertListEqual(\n                chunk.content,\n                [\n                    TextBlock(\n                        type=\"text\",\n                        text=\"test\",\n                    ),\n                ],\n            )\n        res = await self.toolkit.call_tool_function(tool_call)\n        async for chunk in res:\n            self.assertListEqual(\n                chunk.content,\n                [\n                    TextBlock(\n                        type=\"text\",\n                        text=\"arg1: 55, arg2: None\",\n                    ),\n                ],\n            )\n\n        # Try to add the same function with override strategy\n        self.toolkit.register_tool_function(\n            test.sync_func,\n            namesake_strategy=\"override\",\n        )\n        self.assertListEqual(\n            [\n                {\n                    \"type\": \"function\",\n                    \"function\": {\n                        \"name\": \"sync_func\",\n                        \"parameters\": {\n                            \"properties\": {},\n                            \"type\": \"object\",\n                        },\n                        \"description\": \"A duplicate sync function \"\n                        \"for testing.\",\n                    },\n                },\n                new_func_schema,\n            ],\n            self.toolkit.get_json_schemas(),\n        )\n        res = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"123\",\n                name=\"sync_func\",\n                input={},\n            ),\n        )\n        async for chunk in res:\n            self.assertListEqual(\n                chunk.content,\n                [\n                    TextBlock(\n                        type=\"text\",\n                        text=\"test\",\n                    ),\n                ],\n            )\n\n    async def test_basic_functionalities(self) -> None:\n        \"\"\"Test sync function:\n        1. register tool function\n        2. set/cancel extended model\n        3. get JSON schemas\n        4. call tool function\n        \"\"\"\n        self.toolkit.register_tool_function(\n            tool_func=sync_func,\n            preset_kwargs={\"arg1\": 55},\n        )\n        sync_func_schema = deepcopy(self.sync_func_schema)\n        sync_func_schema[\"function\"][\"parameters\"][\"properties\"].pop(\"arg1\")\n        sync_func_schema[\"function\"][\"parameters\"].pop(\"required\")\n        self.assertListEqual(\n            [sync_func_schema],\n            self.toolkit.get_json_schemas(),\n        )\n\n        # Test extended model\n        self.toolkit.set_extended_model(\n            \"sync_func\",\n            StructuredModel,\n        )\n        self.assertListEqual(\n            [\n                {\n                    \"type\": \"function\",\n                    \"function\": {\n                        \"name\": \"sync_func\",\n                        \"parameters\": {\n                            \"properties\": {\n                                \"arg2\": {\n                                    \"anyOf\": [\n                                        {\n                                            \"items\": {\n                                                \"anyOf\": [\n                                                    {\n                                                        \"type\": \"string\",\n                                                    },\n                                                    {\n                                                        \"type\": \"integer\",\n                                                    },\n                                                ],\n                                            },\n                                            \"type\": \"array\",\n                                        },\n                                        {\n                                            \"type\": \"null\",\n                                        },\n                                    ],\n                                    \"default\": None,\n                                    \"description\": \"Test argument 2.\",\n                                },\n                                \"arg3\": {\n                                    \"description\": \"Test argument 3.\",\n                                    \"type\": \"integer\",\n                                },\n                            },\n                            \"type\": \"object\",\n                            \"required\": [\n                                \"arg3\",\n                            ],\n                        },\n                        \"description\": \"A sync function for testing.\\n\"\n                        \"Long description.\",\n                    },\n                },\n            ],\n            self.toolkit.get_json_schemas(),\n        )\n\n        self.toolkit.set_extended_model(\"sync_func\", None)\n        self.assertListEqual(\n            [sync_func_schema],\n            self.toolkit.get_json_schemas(),\n        )\n\n        res = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"123\",\n                name=\"sync_func\",\n                input={\"arg2\": [1, 2, 3]},\n            ),\n        )\n        async for chunk in res:\n            self.assertEqual(\n                ToolResponse(\n                    id=chunk.id,\n                    content=[\n                        TextBlock(\n                            type=\"text\",\n                            text=\"arg1: 55, arg2: [1, 2, 3]\",\n                        ),\n                    ],\n                ),\n                chunk,\n            )\n\n    async def test_extended_model_reusing_same_base_model(self) -> None:\n        \"\"\"Test extended model reusing the same BaseModel from original\n        function.\"\"\"\n\n        def func_with_nested_model(a: MyBaseModel2) -> ToolResponse:\n            \"\"\"Function with nested BaseModel parameter.\"\"\"\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"a: {a}\",\n                    ),\n                ],\n            )\n\n        self.toolkit.register_tool_function(func_with_nested_model)\n\n        # Set extended model that reuses the same MyBaseModel2\n        self.toolkit.set_extended_model(\n            \"func_with_nested_model\",\n            ExtendedModelReusingBaseModel,\n        )\n\n        # Get and verify the schema - should not raise any conflicts\n        schemas = self.toolkit.get_json_schemas()\n        self.assertListEqual(\n            schemas,\n            [\n                {\n                    \"type\": \"function\",\n                    \"function\": {\n                        \"name\": \"func_with_nested_model\",\n                        \"parameters\": {\n                            \"$defs\": {\n                                \"MyBaseModel1\": {\n                                    \"description\": \"A base model for testing\"\n                                    \" nested $defs merging.\",\n                                    \"properties\": {\n                                        \"c\": {\n                                            \"description\": \"Field c\",\n                                            \"title\": \"C\",\n                                            \"type\": \"integer\",\n                                        },\n                                    },\n                                    \"required\": [\"c\"],\n                                    \"title\": \"MyBaseModel1\",\n                                    \"type\": \"object\",\n                                },\n                                \"MyBaseModel2\": {\n                                    \"description\": \"A base model that contains\"\n                                    \" nested MyBaseModel1.\",\n                                    \"properties\": {\n                                        \"b\": {\n                                            \"description\": \"List of \"\n                                            \"MyBaseModel1\",\n                                            \"items\": {\n                                                \"$ref\": \"#/$defs/MyBaseModel1\",\n                                            },\n                                            \"title\": \"B\",\n                                            \"type\": \"array\",\n                                        },\n                                    },\n                                    \"required\": [\"b\"],\n                                    \"title\": \"MyBaseModel2\",\n                                    \"type\": \"object\",\n                                },\n                            },\n                            \"properties\": {\n                                \"a\": {\"$ref\": \"#/$defs/MyBaseModel2\"},\n                                \"another_model\": {\n                                    \"$ref\": \"#/$defs/MyBaseModel2\",\n                                    \"description\": \"Reusing MyBaseModel2\",\n                                },\n                                \"extra_field\": {\n                                    \"description\": \"Extra field\",\n                                    \"type\": \"string\",\n                                },\n                            },\n                            \"required\": [\"a\", \"another_model\", \"extra_field\"],\n                            \"type\": \"object\",\n                        },\n                        \"description\": \"Function with nested BaseModel \"\n                        \"parameter.\",\n                    },\n                },\n            ],\n        )\n\n    async def test_detailed_arguments(self) -> None:\n        \"\"\"Verify the arguments in `register_tool_function`.\"\"\"\n\n        def func(\n            *args: Any,  # pylint: disable=unused-argument\n            **kwargs: Any,\n        ) -> ToolResponse:\n            \"\"\"A test function.\n\n            Note this function is test.\n            \"\"\"\n            return ToolResponse(content=[])\n\n        # Test positional and keyword arguments\n        self.toolkit.register_tool_function(\n            func,\n            include_var_positional=False,\n            include_var_keyword=False,\n        )\n\n        self.assertListEqual(\n            [\n                {\n                    \"type\": \"function\",\n                    \"function\": {\n                        \"name\": \"func\",\n                        \"parameters\": {\n                            \"properties\": {},\n                            \"type\": \"object\",\n                        },\n                        \"description\": \"A test function.\\n\"\n                        \"Note this function is test.\",\n                    },\n                },\n            ],\n            self.toolkit.get_json_schemas(),\n        )\n\n        self.toolkit.remove_tool_function(\"func\")\n\n        # Test func_description\n        self.toolkit.register_tool_function(func, func_description=\"你好\")\n        self.assertListEqual(\n            [\n                {\n                    \"type\": \"function\",\n                    \"function\": {\n                        \"name\": \"func\",\n                        \"parameters\": {\n                            \"properties\": {},\n                            \"type\": \"object\",\n                        },\n                        \"description\": \"你好\",\n                    },\n                },\n            ],\n            self.toolkit.get_json_schemas(),\n        )\n        self.toolkit.remove_tool_function(\"func\")\n\n        # Test long description\n        self.toolkit.register_tool_function(\n            func,\n            include_long_description=False,\n        )\n        self.assertListEqual(\n            [\n                {\n                    \"type\": \"function\",\n                    \"function\": {\n                        \"name\": \"func\",\n                        \"parameters\": {\n                            \"properties\": {},\n                            \"type\": \"object\",\n                        },\n                        \"description\": \"A test function.\",\n                    },\n                },\n            ],\n            self.toolkit.get_json_schemas(),\n        )\n\n    async def _verify_async_generator_wo_interruption(\n        self,\n        async_generator: AsyncGenerator[ToolResponse, None],\n    ) -> None:\n        \"\"\"Verify async generator without interruption.\"\"\"\n        async for index, chunk in aenumerate(async_generator):\n            if index == 0:\n                assert chunk == response1\n            elif index == 1:\n                assert chunk == response2\n            elif index == 2:\n                assert chunk == response3\n\n    async def test_async_func(self) -> None:\n        \"\"\"Test asynchronous tool function\"\"\"\n        self.toolkit.register_tool_function(async_func)\n        res = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"123\",\n                name=\"async_func\",\n                input={\"raise_cancel\": False},\n            ),\n        )\n        async for chunk in res:\n            self.assertEqual(\n                response1,\n                chunk,\n            )\n\n        res = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"123\",\n                name=\"async_func\",\n                input={\"raise_cancel\": True},\n            ),\n        )\n        async for chunk in res:\n            self.assertEqual(\n                ToolResponse(\n                    id=chunk.id,\n                    content=[\n                        TextBlock(\n                            type=\"text\",\n                            text=\"<system-info>\"\n                            \"The tool call has been interrupted by the \"\n                            \"user.</system-info>\",\n                        ),\n                    ],\n                    is_last=True,\n                    stream=True,\n                    is_interrupted=True,\n                ),\n                chunk,\n            )\n\n    async def test_register_async_generator_func(self) -> None:\n        \"\"\"Test asynchronous generator function\"\"\"\n        # Without interruption\n        self.toolkit.register_tool_function(async_generator_func)\n        res = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"123\",\n                name=\"async_generator_func\",\n                input={\"raise_cancel\": False},\n            ),\n        )\n        await self._verify_async_generator_wo_interruption(res)\n\n        # With interruption\n        res = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"123\",\n                name=\"async_generator_func\",\n                input={\"raise_cancel\": True},\n            ),\n        )\n        async for index, chunk in aenumerate(res):\n            if index == 0:\n                self.assertEqual(response1, chunk)\n            elif index == 1:\n                self.assertEqual(response2, chunk)\n            elif index == 2:\n                self.assertEqual(\n                    ToolResponse(\n                        id=chunk.id,\n                        content=[\n                            TextBlock(\n                                type=\"text\",\n                                text=\"12\",\n                            ),\n                            TextBlock(\n                                type=\"text\",\n                                text=\"<system-info>The tool call has been \"\n                                \"interrupted by the user.</system-info>\",\n                            ),\n                        ],\n                        stream=True,\n                        is_last=True,\n                        is_interrupted=True,\n                    ),\n                    chunk,\n                )\n\n    async def test_register_async_func_return_async_generator(self) -> None:\n        \"\"\"Test async function that returns async generator\"\"\"\n        # Without interruption\n        self.toolkit.register_tool_function(async_func_return_async_generator)\n        res = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"123\",\n                name=\"async_func_return_async_generator\",\n                input={\"raise_cancel\": False},\n            ),\n        )\n        await self._verify_async_generator_wo_interruption(res)\n\n        # With interruption\n        res = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"123\",\n                name=\"async_func_return_async_generator\",\n                input={\"raise_cancel\": True},\n            ),\n        )\n        async for index, chunk in aenumerate(res):\n            if index == 0:\n                self.assertEqual(response1, chunk)\n            elif index == 1:\n                self.assertEqual(response2, chunk)\n            elif index == 2:\n                self.assertEqual(\n                    ToolResponse(\n                        id=chunk.id,\n                        content=[\n                            TextBlock(\n                                type=\"text\",\n                                text=\"12\",\n                            ),\n                            TextBlock(\n                                type=\"text\",\n                                text=\"<system-info>The tool call has been \"\n                                \"interrupted by the user.</system-info>\",\n                            ),\n                        ],\n                        stream=True,\n                        is_last=True,\n                        is_interrupted=True,\n                    ),\n                    chunk,\n                )\n\n    async def test_register_async_func_return_sync_generator(self) -> None:\n        \"\"\"Test async function that returns sync generator\"\"\"\n        self.toolkit.register_tool_function(async_func_return_sync_generator)\n        res = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"123\",\n                name=\"async_func_return_sync_generator\",\n                input={},\n            ),\n        )\n        await self._verify_async_generator_wo_interruption(res)\n\n    async def test_register_sync_generator_func(self) -> None:\n        \"\"\"Text sync generator function\"\"\"\n        self.toolkit.register_tool_function(sync_generator_func)\n        res = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"123\",\n                name=\"sync_generator_func\",\n                input={},\n            ),\n        )\n        await self._verify_async_generator_wo_interruption(res)\n\n    async def test_create_tool_group(self) -> None:\n        \"\"\"Test tool group functionalities.\"\"\"\n\n        with self.assertRaises(ValueError):\n            self.toolkit.register_tool_function(\n                sync_func,\n                group_name=\"my_group\",\n            )\n\n        self.toolkit.create_tool_group(\n            \"my_group\",\n            \"Browser use related tools.\",\n            active=False,\n        )\n\n        self.toolkit.register_tool_function(\n            sync_func,\n            group_name=\"my_group\",\n        )\n\n        self.assertListEqual(\n            self.toolkit.get_json_schemas(),\n            [],\n        )\n\n        # Activate the tool group\n        self.toolkit.update_tool_groups([\"my_group\"], True)\n        self.assertListEqual(\n            self.toolkit.get_json_schemas(),\n            [self.sync_func_schema],\n        )\n\n        # Deactivate the tool group\n        self.toolkit.update_tool_groups([\"my_group\"], False)\n        self.assertListEqual(\n            self.toolkit.get_json_schemas(),\n            [],\n        )\n\n        # Unregister the tool group\n        self.toolkit.remove_tool_groups([\"my_group\"])\n        self.assertDictEqual(\n            self.toolkit.tools,\n            {},\n        )\n\n    async def test_postprocess_func(self) -> None:\n        \"\"\"Test postprocess function.\"\"\"\n        tool_use_block = ToolUseBlock(\n            type=\"tool_use\",\n            id=\"123\",\n            name=\"sync_func\",\n            input={\"arg1\": 10, \"arg2\": [\"test\"]},\n        )\n\n        def postprocess_func(\n            tool_use: ToolUseBlock,\n            tool_response: ToolResponse,\n        ) -> ToolResponse | None:\n            \"\"\"Postprocess function to modify tool response.\"\"\"\n\n            self.assertEqual(tool_use, tool_use_block)\n\n            if tool_response.content:\n                tool_response.content.append(\n                    TextBlock(type=\"text\", text=\"Processed\"),\n                )\n            return tool_response\n\n        self.toolkit.register_tool_function(\n            sync_func,\n            postprocess_func=postprocess_func,\n        )\n\n        res = await self.toolkit.call_tool_function(tool_use_block)\n\n        async for chunk in res:\n            self.assertEqual(\n                chunk.content,\n                [\n                    TextBlock(type=\"text\", text=\"arg1: 10, arg2: ['test']\"),\n                    TextBlock(type=\"text\", text=\"Processed\"),\n                ],\n            )\n\n    async def test_async_postprocess_func(self) -> None:\n        \"\"\"Test async postprocess function.\"\"\"\n        tool_use_block = ToolUseBlock(\n            type=\"tool_use\",\n            id=\"123\",\n            name=\"sync_func\",\n            input={\"arg1\": 10, \"arg2\": [\"test\"]},\n        )\n\n        async def async_postprocess_func(\n            tool_use: ToolUseBlock,\n            tool_response: ToolResponse,\n        ) -> ToolResponse | None:\n            \"\"\"Postprocess function to modify tool response.\"\"\"\n\n            self.assertEqual(tool_use, tool_use_block)\n\n            if tool_response.content:\n                tool_response.content.append(\n                    TextBlock(type=\"text\", text=\"Processed\"),\n                )\n            return tool_response\n\n        self.toolkit.register_tool_function(\n            sync_func,\n            postprocess_func=async_postprocess_func,\n        )\n\n        res = await self.toolkit.call_tool_function(tool_use_block)\n\n        async for chunk in res:\n            self.assertEqual(\n                chunk.content,\n                [\n                    TextBlock(type=\"text\", text=\"arg1: 10, arg2: ['test']\"),\n                    TextBlock(type=\"text\", text=\"Processed\"),\n                ],\n            )\n\n    async def test_register_with_valid_json_schema(self) -> None:\n        \"\"\"Test registering a tool with valid custom json_schema.\"\"\"\n        custom_schema = {\n            \"type\": \"function\",\n            \"function\": {\n                \"name\": \"legacy_name\",\n                \"description\": \"Custom description.\",\n                \"parameters\": {\n                    \"properties\": {\n                        \"arg1\": {\n                            \"type\": \"integer\",\n                            \"description\": \"Test argument 1.\",\n                        },\n                        \"arg2\": {\n                            \"type\": \"string\",\n                            \"description\": \"Test argument 2.\",\n                        },\n                    },\n                    \"required\": [\"arg1\"],\n                    \"type\": \"object\",\n                },\n            },\n        }\n\n        self.toolkit.register_tool_function(\n            sync_func,\n            json_schema=custom_schema,\n            func_name=\"renamed_sync_func\",\n            func_description=\"Overridden description.\",\n            preset_kwargs={\"arg1\": 10},\n        )\n\n        schemas = self.toolkit.get_json_schemas()\n        self.assertEqual(len(schemas), 1)\n        self.assertEqual(\n            schemas[0][\"function\"][\"name\"],\n            \"renamed_sync_func\",\n        )\n        self.assertEqual(\n            schemas[0][\"function\"][\"description\"],\n            \"Overridden description.\",\n        )\n        self.assertNotIn(\n            \"arg1\",\n            schemas[0][\"function\"][\"parameters\"][\"properties\"],\n        )\n        self.assertNotIn(\n            \"required\",\n            schemas[0][\"function\"][\"parameters\"],\n        )\n\n    async def test_register_with_invalid_json_schema(self) -> None:\n        \"\"\"Test that invalid json_schema raises AssertionError.\"\"\"\n        invalid_schemas = [\n            \"not a dict\",\n            {\"function\": {\"name\": \"test\"}},\n            {\"type\": \"object\", \"function\": {\"name\": \"test\"}},\n            {\"type\": \"function\"},\n            {\"type\": \"function\", \"function\": \"not a dict\"},\n        ]\n\n        for schema in invalid_schemas:\n            with self.assertRaises(\n                AssertionError,\n                msg=f\"Should raise for schema: {schema}\",\n            ):\n                self.toolkit.register_tool_function(\n                    sync_func,\n                    json_schema=schema,\n                )\n\n    async def test_register_with_custom_json_schema_without_overrides(\n        self,\n    ) -> None:\n        \"\"\"Test custom json_schema is used as-is when no overrides are set.\"\"\"\n        custom_schema = {\n            \"type\": \"function\",\n            \"function\": {\n                \"name\": \"custom_sync_func\",\n                \"description\": \"Custom schema description.\",\n                \"parameters\": {\n                    \"properties\": {\n                        \"arg1\": {\n                            \"type\": \"integer\",\n                            \"description\": \"Argument one.\",\n                        },\n                        \"arg2\": {\n                            \"type\": \"string\",\n                            \"description\": \"Argument two.\",\n                        },\n                    },\n                    \"required\": [\"arg1\", \"arg2\"],\n                    \"type\": \"object\",\n                },\n            },\n        }\n\n        self.toolkit.register_tool_function(\n            sync_func,\n            json_schema=custom_schema,\n            func_name=\"custom_sync_func\",\n        )\n\n        schemas = self.toolkit.get_json_schemas()\n        self.assertEqual(\n            schemas[0][\"function\"][\"description\"],\n            \"Custom schema description.\",\n        )\n        self.assertListEqual(\n            schemas[0][\"function\"][\"parameters\"][\"required\"],\n            [\"arg1\", \"arg2\"],\n        )\n\n    async def test_register_with_json_schema_preset_kwargs_keep_other_required(\n        self,\n    ) -> None:\n        \"\"\"Test preset kwargs remove only matched required parameters.\"\"\"\n        custom_schema = {\n            \"type\": \"function\",\n            \"function\": {\n                \"name\": \"sync_func\",\n                \"description\": \"Custom schema description.\",\n                \"parameters\": {\n                    \"properties\": {\n                        \"arg1\": {\"type\": \"integer\"},\n                        \"arg2\": {\"type\": \"string\"},\n                    },\n                    \"required\": [\"arg1\", \"arg2\"],\n                    \"type\": \"object\",\n                },\n            },\n        }\n\n        self.toolkit.register_tool_function(\n            sync_func,\n            json_schema=custom_schema,\n            preset_kwargs={\"arg1\": 10},\n        )\n\n        schemas = self.toolkit.get_json_schemas()\n        self.assertNotIn(\n            \"arg1\",\n            schemas[0][\"function\"][\"parameters\"][\"properties\"],\n        )\n        self.assertListEqual(\n            schemas[0][\"function\"][\"parameters\"][\"required\"],\n            [\"arg2\"],\n        )\n\n    async def test_partial_function(self) -> None:\n        \"\"\"Test the partial function registration.\"\"\"\n\n        def example_func(\n            a: int,\n            b: str,\n            c: list[str],\n            d: str = \"abc\",\n        ) -> ToolResponse:\n            \"\"\"Example function for partial testing\"\"\"\n            return ToolResponse(\n                content=[\n                    TextBlock(\n                        type=\"text\",\n                        text=f\"Received: a={a}, b={b}, c={c}, d={d}\",\n                    ),\n                ],\n            )\n\n        partial_func = partial(example_func, 1, c=[1, 2, 3])\n\n        self.toolkit.register_tool_function(partial_func)\n\n        self.assertListEqual(\n            self.toolkit.get_json_schemas(),\n            [\n                {\n                    \"type\": \"function\",\n                    \"function\": {\n                        \"name\": \"example_func\",\n                        \"parameters\": {\n                            \"properties\": {\n                                \"b\": {\n                                    \"type\": \"string\",\n                                },\n                                \"d\": {\n                                    \"default\": \"abc\",\n                                    \"type\": \"string\",\n                                },\n                            },\n                            \"required\": [\n                                \"b\",\n                            ],\n                            \"type\": \"object\",\n                        },\n                        \"description\": \"Example function for partial testing\",\n                    },\n                },\n            ],\n        )\n\n        res = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"123\",\n                name=\"example_func\",\n                input={\"b\": \"test\", \"d\": \"xyz\"},\n            ),\n        )\n\n        async for chunk in res:\n            self.assertEqual(\n                chunk.content[0][\"text\"],\n                \"Received: a=1, b=test, c=[1, 2, 3], d=xyz\",\n            )\n\n    async def test_func_name_parameter(self) -> None:\n        \"\"\"Test func_name parameter for custom tool renaming.\"\"\"\n        # Test 1: Regular function with func_name\n        self.toolkit.register_tool_function(\n            sync_func,\n            func_name=\"custom_sync_func\",\n        )\n        self.assertIn(\"custom_sync_func\", self.toolkit.tools)\n        self.assertNotIn(\"sync_func\", self.toolkit.tools)\n\n        # Verify the JSON schema uses the custom name\n        schemas = self.toolkit.get_json_schemas()\n        self.assertEqual(schemas[0][\"function\"][\"name\"], \"custom_sync_func\")\n\n        # Verify original_name is set when func_name is provided\n        tool_obj = self.toolkit.tools[\"custom_sync_func\"]\n        self.assertEqual(tool_obj.original_name, \"sync_func\")\n\n        # Test 2: Regular function without func_name (backward compatibility)\n        def another_func(x: int) -> ToolResponse:\n            \"\"\"Another test function.\"\"\"\n            return ToolResponse(content=[TextBlock(type=\"text\", text=str(x))])\n\n        self.toolkit.register_tool_function(another_func)\n        self.assertIn(\"another_func\", self.toolkit.tools)\n        tool_obj = self.toolkit.tools[\"another_func\"]\n        self.assertIsNone(tool_obj.original_name)\n\n        # Test 3: Partial function with func_name\n        partial_func = partial(sync_func, arg1=10)\n        self.toolkit.register_tool_function(\n            partial_func,\n            func_name=\"custom_partial_func\",\n        )\n        self.assertIn(\"custom_partial_func\", self.toolkit.tools)\n        tool_obj = self.toolkit.tools[\"custom_partial_func\"]\n        self.assertEqual(tool_obj.original_name, \"sync_func\")\n\n        # Test 4: func_name with namesake_strategy=\"rename\"\n        self.toolkit.register_tool_function(\n            sync_func,\n            func_name=\"custom_sync_func\",  # Already exists\n            namesake_strategy=\"rename\",\n        )\n        # Should create a new name with random suffix\n        renamed_tools = [\n            name\n            for name in self.toolkit.tools\n            if name.startswith(\"custom_sync_func_\")\n        ]\n        self.assertEqual(len(renamed_tools), 1)\n        renamed_name = renamed_tools[0]\n        tool_obj = self.toolkit.tools[renamed_name]\n        # original_name should be \"sync_func\" (the true original function name)\n        # because original_name records the actual function name, not the\n        # func_name\n        self.assertEqual(tool_obj.original_name, \"sync_func\")\n\n        # Test 5: func_name with namesake_strategy=\"rename\" but no func_name\n        # (should use original function name as original_name)\n        def test_func() -> ToolResponse:\n            \"\"\"Test function.\"\"\"\n            return ToolResponse(content=[TextBlock(type=\"text\", text=\"test\")])\n\n        self.toolkit.register_tool_function(test_func)\n        self.toolkit.register_tool_function(\n            test_func,\n            namesake_strategy=\"rename\",\n        )\n        # Find the renamed tool\n        renamed_test_tools = [\n            name\n            for name in self.toolkit.tools\n            if name.startswith(\"test_func_\")\n        ]\n        self.assertEqual(len(renamed_test_tools), 1)\n        renamed_test_name = renamed_test_tools[0]\n        tool_obj = self.toolkit.tools[renamed_test_name]\n        # original_name should be \"test_func\" (the original function name)\n        self.assertEqual(tool_obj.original_name, \"test_func\")\n\n        # Test 6: Verify tool can be called with custom name\n        res = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"123\",\n                name=\"custom_sync_func\",\n                input={\"arg1\": 42},\n            ),\n        )\n        async for chunk in res:\n            self.assertEqual(\n                chunk.content[0][\"text\"],\n                \"arg1: 42, arg2: None\",\n            )\n\n    async def asyncTearDown(self) -> None:\n        \"\"\"Clean up after each test.\"\"\"\n        self.toolkit = None\n"
  },
  {
    "path": "tests/toolkit_meta_tool_test.py",
    "content": "# -*- coding: utf-8 -*-\n# mypy: disable-error-code=\"index\"\n\"\"\"Test meta tool in toolkit module in agentscope.\"\"\"\nfrom unittest import IsolatedAsyncioTestCase\n\nfrom agentscope.message import ToolUseBlock, TextBlock\nfrom agentscope.tool import ToolResponse, Toolkit\n\n\ndef tool_function_1() -> ToolResponse:\n    \"\"\"Test tool function 1.\"\"\"\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=\"1\",\n            ),\n        ],\n    )\n\n\ndef tool_function_2() -> ToolResponse:\n    \"\"\"Test tool function 2.\"\"\"\n    return ToolResponse(\n        content=[\n            TextBlock(\n                type=\"text\",\n                text=\"2\",\n            ),\n        ],\n    )\n\n\nclass ToolkitMetaToolTest(IsolatedAsyncioTestCase):\n    \"\"\"Unittest for the toolkit module.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the test environment before each test.\"\"\"\n        self.toolkit = Toolkit()\n\n        self.function_1_schema = {\n            \"type\": \"function\",\n            \"function\": {\n                \"name\": \"tool_function_1\",\n                \"parameters\": {\n                    \"type\": \"object\",\n                    \"properties\": {},\n                },\n                \"description\": \"Test tool function 1.\",\n            },\n        }\n\n        self.function_2_schema = {\n            \"type\": \"function\",\n            \"function\": {\n                \"name\": \"tool_function_2\",\n                \"parameters\": {\n                    \"type\": \"object\",\n                    \"properties\": {},\n                },\n                \"description\": \"Test tool function 2.\",\n            },\n        }\n\n        self.meta_tool_schema = {\n            \"type\": \"function\",\n            \"function\": {\n                \"name\": \"reset_equipped_tools\",\n                \"parameters\": {\n                    \"properties\": {},\n                    \"type\": \"object\",\n                },\n                \"description\": \"\"\"This function allows you to activate or \\\ndeactivate tool groups\ndynamically based on your current task requirements.\n**Important: Each call sets the absolute final state of ALL tool\ngroups, not incremental changes**. Any group not explicitly set to True\nwill be deactivated, regardless of its previous state.\n\n**Best practice**: Actively manage your tool groups——activate only\nwhat you need for the current task, and promptly deactivate groups as\nsoon as they are no longer needed to conserve context space.\n\nThe function will return the usage instructions for the activated tool\ngroups, which you **MUST pay attention to and follow**. You can also\nreuse this function to check the notes of the tool groups.\"\"\",\n            },\n        }\n\n    async def test_meta_tool(self) -> None:\n        \"\"\"Test the meta tool.\"\"\"\n        self.toolkit.register_tool_function(\n            self.toolkit.reset_equipped_tools,\n        )\n\n        # Test if the meta tool is registered correctly\n        self.assertListEqual(\n            self.toolkit.get_json_schemas(),\n            [self.meta_tool_schema],\n        )\n\n        # Test creating a tool group and using the meta tool\n        self.toolkit.create_tool_group(\n            \"browser_use\",\n            \"The browser-use related tools.\",\n            notes=\"\"\"1. You must xxx\n2. First click xxx\n\"\"\",\n        )\n        self.toolkit.register_tool_function(\n            tool_function_1,\n            group_name=\"browser_use\",\n        )\n\n        self.meta_tool_schema[\"function\"][\"parameters\"][\"properties\"] = {\n            \"browser_use\": {\n                \"type\": \"boolean\",\n                \"description\": \"The browser-use related tools.\",\n                \"default\": False,\n            },\n        }\n\n        # Test if the arguments are updated correctly\n        self.assertListEqual(\n            self.toolkit.get_json_schemas(),\n            [\n                self.meta_tool_schema,\n            ],\n        )\n\n        res = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"123\",\n                name=\"reset_equipped_tools\",\n                input={\"browser_use\": True},\n            ),\n        )\n\n        # Test if the tool response is correct\n        async for chunk in res:\n            self.assertEqual(\n                chunk.content[0][\"text\"],\n                \"Now tool groups 'browser_use' are activated. \"\n                \"You MUST follow these notes to use these tools:\\n\"\n                \"<notes>## About Tool Group 'browser_use'\\n\"\n                \"1. You must xxx\\n\"\n                \"2. First click xxx\\n\"\n                \"</notes>\",\n            )\n\n        # Test if the tool group is activated correctly, i.e. the tool\n        # function 1 is available\n        self.assertListEqual(\n            self.toolkit.get_json_schemas(),\n            [\n                self.meta_tool_schema,\n                {\n                    \"type\": \"function\",\n                    \"function\": {\n                        \"name\": \"tool_function_1\",\n                        \"parameters\": {\n                            \"type\": \"object\",\n                            \"properties\": {},\n                        },\n                        \"description\": \"Test tool function 1.\",\n                    },\n                },\n            ],\n        )\n\n        # Create another tool group and register tool function 2\n        self.toolkit.create_tool_group(\n            \"file_use\",\n            \"The file-use related tools.\",\n        )\n        self.toolkit.register_tool_function(\n            tool_function_2,\n            group_name=\"file_use\",\n        )\n\n        # Test if the meta tool schema is updated correctly\n        self.meta_tool_schema[\"function\"][\"parameters\"][\"properties\"] = {\n            \"browser_use\": {\n                \"type\": \"boolean\",\n                \"description\": \"The browser-use related tools.\",\n                \"default\": False,\n            },\n            \"file_use\": {\n                \"type\": \"boolean\",\n                \"description\": \"The file-use related tools.\",\n                \"default\": False,\n            },\n        }\n\n        self.assertListEqual(\n            self.toolkit.get_json_schemas(),\n            [\n                self.meta_tool_schema,\n                self.function_1_schema,\n            ],\n        )\n\n        # Activate the file-use tool group only\n        res = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"124\",\n                name=\"reset_equipped_tools\",\n                input={\"file_use\": True},\n            ),\n        )\n\n        # Test if the tool response is correct\n        async for chunk in res:\n            self.assertEqual(\n                chunk.content[0][\"text\"],\n                \"Now tool groups 'file_use' are activated.\",\n            )\n\n        # Test if only tool function 2 is available now\n        self.assertListEqual(\n            self.toolkit.get_json_schemas(),\n            [\n                self.meta_tool_schema,\n                self.function_2_schema,\n            ],\n        )\n\n        # Test if all tool groups are deactivated\n        res = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"125\",\n                name=\"reset_equipped_tools\",\n                input={},\n            ),\n        )\n\n        # Test if the tool response is correct\n        async for chunk in res:\n            self.assertEqual(\n                chunk.content[0][\"text\"],\n                \"All tool groups are now deactivated currently.\",\n            )\n\n        # Test if no tool function is available now\n        self.assertListEqual(\n            self.toolkit.get_json_schemas(),\n            [\n                self.meta_tool_schema,\n            ],\n        )\n\n        # Test calling the inactive tool function\n        res = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"126\",\n                name=\"tool_function_1\",\n                input={},\n            ),\n        )\n\n        async for chunk in res:\n            self.assertEqual(\n                chunk.content[0][\"text\"],\n                \"FunctionInactiveError: The function 'tool_function_1' \"\n                \"is in the inactive group 'browser_use'. \"\n                \"Activate the tool group by calling 'reset_equipped_tools' \"\n                \"first to use this tool.\",\n            )\n\n    async def asyncTearDown(self) -> None:\n        \"\"\"Clean up after each test.\"\"\"\n        self.toolkit = None\n"
  },
  {
    "path": "tests/toolkit_middleware_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The unittests for toolkit middleware.\"\"\"\nfrom typing import Callable, AsyncGenerator, Coroutine, Any\nfrom unittest.async_case import IsolatedAsyncioTestCase\n\nfrom agentscope.message import TextBlock, ToolUseBlock\nfrom agentscope.tool import Toolkit, ToolResponse\n\n\nasync def middleware_1(\n    kwargs: dict,\n    next_handler: Callable[\n        ...,\n        Coroutine[Any, Any, AsyncGenerator[ToolResponse, None]],\n    ],\n) -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"A simple middleware that adds a key-value pair to the kwargs.\"\"\"\n    # Pre-processing\n    tool_call = kwargs[\"tool_call\"]\n    tool_call[\"input\"][\"a\"] += \"[pre1]\"\n\n    async for chunk in await next_handler(**kwargs):\n        chunk.content[0][\"text\"] += \"[post1]\"\n        yield chunk\n\n\nasync def middleware_2(\n    kwargs: dict,\n    next_handler: Callable[\n        ...,\n        Coroutine[Any, Any, AsyncGenerator[ToolResponse, None]],\n    ],\n) -> AsyncGenerator[ToolResponse, None]:\n    \"\"\"Another middleware that adds a key-value pair to the kwargs.\"\"\"\n    # Pre-processing\n    tool_call = kwargs[\"tool_call\"]\n    tool_call[\"input\"][\"a\"] += \"[pre2]\"\n\n    async for chunk in await next_handler(**kwargs):\n        chunk.content[0][\"text\"] += \"[post2]\"\n        yield chunk\n\n\nasync def tool(a: str) -> ToolResponse:\n    \"\"\"The test tool function.\n\n    Args:\n        a (`str`):\n            A string input.\n\n    Returns:\n        `ToolResponse`:\n            The tool response containing the input value.\n    \"\"\"\n    return ToolResponse(\n        content=[TextBlock(type=\"text\", text=a)],\n    )\n\n\nclass ToolkitMiddlewareTest(IsolatedAsyncioTestCase):\n    \"\"\"Test the toolkit middleware.\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the test case.\"\"\"\n        self.toolkit = Toolkit()\n        self.toolkit.register_tool_function(tool)\n\n    async def test_toolkit_middleware(self) -> None:\n        \"\"\"Test the toolkit middleware.\"\"\"\n        self.toolkit.register_middleware(middleware_1)\n        self.toolkit.register_middleware(middleware_2)\n\n        res = await self.toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                name=\"tool\",\n                input={\"a\": \"[ori]\"},\n                id=\"123\",\n            ),\n        )\n\n        async for chunk in res:\n            self.assertEqual(\n                chunk.content[0][\"text\"],\n                \"[ori][pre1][pre2][post2][post1]\",\n            )\n"
  },
  {
    "path": "tests/tracing_converter_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unittests for the message converter functionality in AgentScope tracing.\"\"\"\nfrom typing import Any, Dict\nfrom unittest import TestCase\n\nfrom agentscope.message import (\n    TextBlock,\n    ToolUseBlock,\n    ToolResultBlock,\n    ImageBlock,\n    AudioBlock,\n    VideoBlock,\n)\nfrom agentscope.tracing._converter import _convert_block_to_part\n\n\nclass ConverterTest(TestCase):\n    \"\"\"Test cases for _convert_block_to_part converter\"\"\"\n\n    def test_convert_text_block(self) -> None:\n        \"\"\"Test text block conversion\"\"\"\n        # Normal text block\n        block: TextBlock = {\n            \"type\": \"text\",\n            \"text\": \"Hello, world!\",\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"type\"], \"text\")\n        self.assertEqual(result[\"content\"], \"Hello, world!\")\n\n        # Missing text field\n        block = {\"type\": \"text\"}\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"type\"], \"text\")\n        self.assertEqual(result[\"content\"], \"\")\n\n        # Empty text\n        block = {\"type\": \"text\", \"text\": \"\"}\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"type\"], \"text\")\n        self.assertEqual(result[\"content\"], \"\")\n\n    def test_convert_thinking_block(self) -> None:\n        \"\"\"Test thinking block conversion\"\"\"\n        # Normal thinking block\n        block: Dict[str, Any] = {\n            \"type\": \"thinking\",\n            \"thinking\": \"Let me think about this...\",\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"type\"], \"reasoning\")\n        self.assertEqual(result[\"content\"], \"Let me think about this...\")\n\n        # Missing thinking field\n        block = {\"type\": \"thinking\"}\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"type\"], \"reasoning\")\n        self.assertEqual(result[\"content\"], \"\")\n\n    def test_convert_tool_use_block(self) -> None:\n        \"\"\"Test tool_use block conversion\"\"\"\n        # Normal tool_use block\n        block: ToolUseBlock = {\n            \"type\": \"tool_use\",\n            \"id\": \"call_123\",\n            \"name\": \"search\",\n            \"input\": {\"query\": \"test query\", \"limit\": 10},\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"type\"], \"tool_call\")\n        self.assertEqual(result[\"id\"], \"call_123\")\n        self.assertEqual(result[\"name\"], \"search\")\n        self.assertEqual(\n            result[\"arguments\"],\n            {\"query\": \"test query\", \"limit\": 10},\n        )\n\n        # Missing fields\n        block = {\n            \"type\": \"tool_use\",\n            \"id\": \"\",\n            \"name\": \"\",\n            \"input\": {},\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"type\"], \"tool_call\")\n        self.assertEqual(result[\"id\"], \"\")\n        self.assertEqual(result[\"name\"], \"\")\n        self.assertEqual(result[\"arguments\"], {})\n\n    def test_convert_tool_result_block(self) -> None:\n        \"\"\"Test tool_result block conversion\"\"\"\n        # String output\n        block: ToolResultBlock = {\n            \"type\": \"tool_result\",\n            \"id\": \"call_123\",\n            \"name\": \"search\",\n            \"output\": \"Search results here\",\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"type\"], \"tool_call_response\")\n        self.assertEqual(result[\"id\"], \"call_123\")\n        self.assertEqual(result[\"response\"], \"Search results here\")\n\n        # Dict output\n        block = {\n            \"type\": \"tool_result\",\n            \"id\": \"call_123\",\n            \"name\": \"search\",\n            \"output\": {\"result\": \"success\", \"data\": [1, 2, 3]},\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertIsInstance(result[\"response\"], str)\n        self.assertIn(\"result\", result[\"response\"])\n        self.assertIn(\"success\", result[\"response\"])\n\n        # List output\n        block = {\n            \"type\": \"tool_result\",\n            \"id\": \"call_123\",\n            \"name\": \"search\",\n            \"output\": [\n                {\"type\": \"text\", \"text\": \"Result 1\"},\n                {\"type\": \"text\", \"text\": \"Result 2\"},\n            ],\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertIsInstance(result[\"response\"], str)\n        self.assertIn(\"Result 1\", result[\"response\"])\n\n        # Numeric output\n        block = {\n            \"type\": \"tool_result\",\n            \"id\": \"call_123\",\n            \"name\": \"calculate\",\n            \"output\": 42,\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"response\"], \"42\")\n\n        # Empty output\n        block = {\n            \"type\": \"tool_result\",\n            \"id\": \"call_123\",\n            \"name\": \"search\",\n            \"output\": \"\",\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"response\"], \"\")\n\n        # None output\n        block = {\n            \"type\": \"tool_result\",\n            \"id\": \"call_123\",\n            \"name\": \"search\",\n            \"output\": None,\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"response\"], \"None\")\n\n        # Missing id\n        block = {\n            \"type\": \"tool_result\",\n            \"id\": \"\",\n            \"name\": \"search\",\n            \"output\": \"result\",\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"id\"], \"\")\n\n    def test_convert_image_block(self) -> None:\n        \"\"\"Test image block conversion\"\"\"\n        # URL source\n        source: Dict[str, Any] = {\n            \"type\": \"url\",\n            \"url\": \"https://example.com/image.jpg\",\n        }\n        block: ImageBlock = {\n            \"type\": \"image\",\n            \"source\": source,\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"type\"], \"uri\")\n        self.assertEqual(result[\"uri\"], \"https://example.com/image.jpg\")\n        self.assertEqual(result[\"modality\"], \"image\")\n\n        # Base64 source\n        source = {\n            \"type\": \"base64\",\n            \"media_type\": \"image/png\",\n            \"data\": \"base\",\n        }\n        block = {\n            \"type\": \"image\",\n            \"source\": source,\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"type\"], \"blob\")\n        self.assertEqual(result[\"content\"], source[\"data\"])\n        self.assertEqual(result[\"media_type\"], \"image/png\")\n        self.assertEqual(result[\"modality\"], \"image\")\n\n        # Base64 with default media_type\n        source = {\n            \"type\": \"base64\",\n            \"media_type\": \"image/jpeg\",\n            \"data\": \"base64data\",\n        }\n        block = {\n            \"type\": \"image\",\n            \"source\": source,\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"media_type\"], \"image/jpeg\")\n\n        # URL missing url field\n        block = {\n            \"type\": \"image\",\n            \"source\": {\"type\": \"url\"},\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"uri\"], \"\")\n\n        # Base64 missing fields\n        block = {\n            \"type\": \"image\",\n            \"source\": {\"type\": \"base64\"},\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"content\"], \"\")\n        self.assertEqual(result[\"media_type\"], \"image/jpeg\")\n\n        # Invalid source type\n        block = {\n            \"type\": \"image\",\n            \"source\": {\"type\": \"invalid\"},\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNone(result)\n\n        # Missing source\n        block = {\"type\": \"image\"}\n        result = _convert_block_to_part(block)\n        self.assertIsNone(result)\n\n    def test_convert_audio_block(self) -> None:\n        \"\"\"Test audio block conversion\"\"\"\n        # URL source\n        source: Dict[str, Any] = {\n            \"type\": \"url\",\n            \"url\": \"https://example.com/audio.wav\",\n        }\n        block: AudioBlock = {\n            \"type\": \"audio\",\n            \"source\": source,\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"type\"], \"uri\")\n        self.assertEqual(result[\"uri\"], \"https://example.com/audio.wav\")\n        self.assertEqual(result[\"modality\"], \"audio\")\n\n        # Base64 source\n        source = {\n            \"type\": \"base64\",\n            \"media_type\": \"audio/mpeg\",\n            \"data\": \"base64audiodata\",\n        }\n        block = {\n            \"type\": \"audio\",\n            \"source\": source,\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"type\"], \"blob\")\n        self.assertEqual(result[\"content\"], \"base64audiodata\")\n        self.assertEqual(result[\"media_type\"], \"audio/mpeg\")\n        self.assertEqual(result[\"modality\"], \"audio\")\n\n        # Base64 with default media_type\n        source = {\n            \"type\": \"base64\",\n            \"media_type\": \"audio/wav\",\n            \"data\": \"base64data\",\n        }\n        block = {\n            \"type\": \"audio\",\n            \"source\": source,\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"media_type\"], \"audio/wav\")\n\n        # URL missing url field\n        block = {\n            \"type\": \"audio\",\n            \"source\": {\"type\": \"url\"},\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"uri\"], \"\")\n\n        # Base64 missing fields\n        block = {\n            \"type\": \"audio\",\n            \"source\": {\"type\": \"base64\"},\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"content\"], \"\")\n        self.assertEqual(result[\"media_type\"], \"audio/wav\")\n\n        # Invalid source type\n        block = {\n            \"type\": \"audio\",\n            \"source\": {\"type\": \"invalid\"},\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNone(result)\n\n    def test_convert_video_block(self) -> None:\n        \"\"\"Test video block conversion\"\"\"\n        # URL source\n        source: Dict[str, Any] = {\n            \"type\": \"url\",\n            \"url\": \"https://example.com/video.mp4\",\n        }\n        block: VideoBlock = {\n            \"type\": \"video\",\n            \"source\": source,\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"type\"], \"uri\")\n        self.assertEqual(result[\"uri\"], \"https://example.com/video.mp4\")\n        self.assertEqual(result[\"modality\"], \"video\")\n\n        # Base64 source\n        source = {\n            \"type\": \"base64\",\n            \"media_type\": \"video/webm\",\n            \"data\": \"base64videodata\",\n        }\n        block = {\n            \"type\": \"video\",\n            \"source\": source,\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"type\"], \"blob\")\n        self.assertEqual(result[\"content\"], \"base64videodata\")\n        self.assertEqual(result[\"media_type\"], \"video/webm\")\n        self.assertEqual(result[\"modality\"], \"video\")\n\n        # Base64 with default media_type\n        source = {\n            \"type\": \"base64\",\n            \"media_type\": \"video/mp4\",\n            \"data\": \"base64data\",\n        }\n        block = {\n            \"type\": \"video\",\n            \"source\": source,\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"media_type\"], \"video/mp4\")\n\n        # URL missing url field\n        block = {\n            \"type\": \"video\",\n            \"source\": {\"type\": \"url\"},\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"uri\"], \"\")\n\n        # Base64 missing fields\n        block = {\n            \"type\": \"video\",\n            \"source\": {\"type\": \"base64\"},\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNotNone(result)\n        self.assertEqual(result[\"content\"], \"\")\n        self.assertEqual(result[\"media_type\"], \"video/mp4\")\n\n        # Invalid source type\n        block = {\n            \"type\": \"video\",\n            \"source\": {\"type\": \"invalid\"},\n        }\n        result = _convert_block_to_part(block)\n        self.assertIsNone(result)\n\n    def test_convert_invalid_blocks(self) -> None:\n        \"\"\"Test invalid block types\"\"\"\n        # Unknown block type\n        block = {\"type\": \"unknown_type\"}\n        result = _convert_block_to_part(block)\n        self.assertIsNone(result)\n\n        # Missing type field\n        block = {}\n        result = _convert_block_to_part(block)\n        self.assertIsNone(result)\n"
  },
  {
    "path": "tests/tracing_extractor_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unit tests for the tracing extractor module.\"\"\"\nfrom unittest import TestCase\nfrom unittest.mock import Mock\n\nfrom agentscope.message import Msg, TextBlock, ToolUseBlock\nfrom agentscope.model import ChatModelBase, ChatResponse\nfrom agentscope.agent import AgentBase\nfrom agentscope.tool import Toolkit\nfrom agentscope.formatter import FormatterBase\nfrom agentscope.embedding import EmbeddingModelBase\nfrom agentscope.tracing._extractor import (\n    _get_common_attributes,\n    _get_format_target,\n    _get_provider_name,\n    _get_tool_definitions,\n    _get_llm_request_attributes,\n    _get_llm_span_name,\n    _get_llm_output_messages,\n    _get_llm_response_attributes,\n    _get_agent_messages,\n    _get_agent_request_attributes,\n    _get_agent_span_name,\n    _get_agent_response_attributes,\n    _get_tool_request_attributes,\n    _get_tool_span_name,\n    _get_tool_response_attributes,\n    _get_formatter_request_attributes,\n    _get_formatter_span_name,\n    _get_formatter_response_attributes,\n    _get_generic_function_request_attributes,\n    _get_generic_function_span_name,\n    _get_generic_function_response_attributes,\n    _get_embedding_request_attributes,\n    _get_embedding_span_name,\n    _get_embedding_response_attributes,\n)\nfrom agentscope.tracing._attributes import (\n    SpanAttributes,\n    OperationNameValues,\n    ProviderNameValues,\n)\n\n\nclass ExtractorTest(TestCase):\n    \"\"\"Test cases for the extractor module.\"\"\"\n\n    def setUp(self) -> None:\n        \"\"\"Set up test fixtures.\"\"\"\n        self.mock_model = Mock(spec=ChatModelBase)\n        self.mock_model.model_name = \"test-model\"\n        self.mock_model.__class__.__name__ = \"TestChatModel\"\n\n        self.mock_agent = Mock(spec=AgentBase)\n        self.mock_agent.id = \"agent-1\"\n        self.mock_agent.name = \"TestAgent\"\n        self.mock_agent.__class__.__doc__ = \"Test agent description\"\n\n        self.mock_formatter = Mock(spec=FormatterBase)\n        self.mock_formatter.__class__.__name__ = \"OpenAIChatFormatter\"\n\n        self.mock_embedding = Mock(spec=EmbeddingModelBase)\n        self.mock_embedding.model_name = \"embedding-model\"\n\n    def test_get_common_attributes(self) -> None:\n        \"\"\"Test _get_common_attributes.\"\"\"\n        from agentscope import _config\n\n        original_run_id = _config.run_id\n        _config.run_id = \"test-run-id\"\n\n        try:\n            attributes = _get_common_attributes()\n            self.assertIn(SpanAttributes.GEN_AI_CONVERSATION_ID, attributes)\n            self.assertEqual(\n                attributes[SpanAttributes.GEN_AI_CONVERSATION_ID],\n                '\"test-run-id\"',\n            )\n        finally:\n            _config.run_id = original_run_id\n\n    def test_get_format_target(self) -> None:\n        \"\"\"Test _get_format_target.\"\"\"\n        # Test OpenAI formatter\n        formatter = Mock()\n        formatter.__class__.__name__ = \"OpenAIChatFormatter\"\n        self.assertEqual(\n            _get_format_target(formatter),\n            ProviderNameValues.OPENAI,\n        )\n\n        # Test DashScope formatter\n        formatter.__class__.__name__ = \"DashScopeChatFormatter\"\n        self.assertEqual(\n            _get_format_target(formatter),\n            ProviderNameValues.DASHSCOPE,\n        )\n\n        # Test unknown formatter\n        formatter.__class__.__name__ = \"UnknownFormatter\"\n        self.assertEqual(_get_format_target(formatter), \"unknown\")\n\n    def test_get_provider_name(self) -> None:\n        \"\"\"Test _get_provider_name.\"\"\"\n        # Test OpenAI model\n        model = Mock(spec=ChatModelBase)\n        model.__class__.__name__ = \"OpenAIChatModel\"\n        model.client = Mock()\n        model.client.base_url = \"https://api.openai.com/v1\"\n        self.assertEqual(_get_provider_name(model), ProviderNameValues.OPENAI)\n\n        # Test DashScope model\n        model.__class__.__name__ = \"DashScopeChatModel\"\n        self.assertEqual(\n            _get_provider_name(model),\n            ProviderNameValues.DASHSCOPE,\n        )\n\n        # Test OpenAI model with custom base_url\n        model.__class__.__name__ = \"OpenAIChatModel\"\n        model.client.base_url = \"https://api.deepseek.com/v1\"\n        self.assertEqual(\n            _get_provider_name(model),\n            ProviderNameValues.DEEPSEEK,\n        )\n\n        # Test model without base_url\n        model.client.base_url = None\n        self.assertEqual(_get_provider_name(model), ProviderNameValues.OPENAI)\n\n    def test_get_tool_definitions(self) -> None:\n        \"\"\"Test _get_tool_definitions.\"\"\"\n        # Test with valid tools\n        tools = [\n            {\n                \"type\": \"function\",\n                \"function\": {\n                    \"name\": \"test_tool\",\n                    \"description\": \"A test tool\",\n                    \"parameters\": {\"type\": \"object\", \"properties\": {}},\n                },\n            },\n        ]\n        result = _get_tool_definitions(tools, \"auto\")\n        self.assertIsNotNone(result)\n        self.assertIn(\"test_tool\", result)\n\n        # Test with None tools\n        self.assertIsNone(_get_tool_definitions(None, \"auto\"))\n\n        # Test with empty tools\n        self.assertIsNone(_get_tool_definitions([], \"auto\"))\n\n        # Test with tool_choice=\"none\"\n        self.assertIsNone(_get_tool_definitions(tools, \"none\"))\n\n        # Test with invalid tool format\n        invalid_tools = [{\"type\": \"function\"}]\n        self.assertIsNone(_get_tool_definitions(invalid_tools, \"auto\"))\n\n    def test_get_llm_request_attributes(self) -> None:\n        \"\"\"Test _get_llm_request_attributes and _get_llm_span_name.\"\"\"\n        args = ()\n        kwargs = {\n            \"temperature\": 0.7,\n            \"top_p\": 0.9,\n            \"top_k\": 40,\n            \"max_tokens\": 100,\n            \"presence_penalty\": 0.1,\n            \"frequency_penalty\": 0.2,\n            \"stop_sequences\": [\"stop\"],\n            \"seed\": 42,\n        }\n\n        attributes = _get_llm_request_attributes(self.mock_model, args, kwargs)\n\n        self.assertEqual(\n            attributes[SpanAttributes.GEN_AI_OPERATION_NAME],\n            OperationNameValues.CHAT,\n        )\n        self.assertEqual(\n            attributes[SpanAttributes.GEN_AI_REQUEST_MODEL],\n            \"test-model\",\n        )\n        self.assertEqual(\n            attributes[SpanAttributes.GEN_AI_REQUEST_TEMPERATURE],\n            0.7,\n        )\n        self.assertEqual(\n            attributes[SpanAttributes.GEN_AI_REQUEST_TOP_P],\n            0.9,\n        )\n        self.assertIn(SpanAttributes.AGENTSCOPE_FUNCTION_INPUT, attributes)\n\n        # Test span name generation\n        span_name = _get_llm_span_name(attributes)\n        self.assertEqual(span_name, \"chat test-model\")\n\n    def test_get_llm_response_attributes(self) -> None:\n        \"\"\"Test _get_llm_response_attributes and _get_llm_output_messages.\"\"\"\n        # Create a mock usage object\n        usage = Mock()\n        usage.input_tokens = 10\n        usage.output_tokens = 20\n\n        response = ChatResponse(\n            id=\"test-id\",\n            content=[TextBlock(type=\"text\", text=\"Hello\")],\n        )\n        response.usage = usage\n\n        # Test output messages extraction\n        messages = _get_llm_output_messages(response)\n        self.assertEqual(len(messages), 1)\n        self.assertEqual(messages[0][\"role\"], \"assistant\")\n        self.assertIn(\"parts\", messages[0])\n\n        # Test with non-ChatResponse\n        result = _get_llm_output_messages(\"not a response\")\n        self.assertEqual(result, \"not a response\")\n\n        # Test response attributes\n        attributes = _get_llm_response_attributes(response)\n\n        self.assertEqual(\n            attributes[SpanAttributes.GEN_AI_RESPONSE_ID],\n            \"test-id\",\n        )\n        self.assertEqual(\n            attributes[SpanAttributes.GEN_AI_USAGE_INPUT_TOKENS],\n            10,\n        )\n        self.assertEqual(\n            attributes[SpanAttributes.GEN_AI_USAGE_OUTPUT_TOKENS],\n            20,\n        )\n        self.assertIn(SpanAttributes.GEN_AI_OUTPUT_MESSAGES, attributes)\n\n    def test_get_agent_messages_single_msg(self) -> None:\n        \"\"\"Test _get_agent_messages with single Msg input.\"\"\"\n        msg = Msg(\n            \"test_user\",\n            [TextBlock(type=\"text\", text=\"Hello\")],\n            \"user\",\n        )\n        result = _get_agent_messages(msg)\n\n        self.assertEqual(len(result), 1)\n        self.assertEqual(result[0][\"role\"], \"user\")\n        self.assertEqual(result[0][\"name\"], \"test_user\")\n        self.assertEqual(result[0][\"finish_reason\"], \"stop\")\n        self.assertIn(\"parts\", result[0])\n        self.assertEqual(len(result[0][\"parts\"]), 1)\n        self.assertEqual(result[0][\"parts\"][0][\"type\"], \"text\")\n        self.assertEqual(result[0][\"parts\"][0][\"content\"], \"Hello\")\n\n    def test_get_agent_messages_list_of_msgs(self) -> None:\n        \"\"\"Test _get_agent_messages with list of Msg inputs.\"\"\"\n        msgs = [\n            Msg(\n                \"user1\",\n                [TextBlock(type=\"text\", text=\"Hello\")],\n                \"user\",\n            ),\n            Msg(\n                \"assistant1\",\n                [TextBlock(type=\"text\", text=\"Hi there!\")],\n                \"assistant\",\n            ),\n            Msg(\n                \"user1\",\n                [TextBlock(type=\"text\", text=\"How are you?\")],\n                \"user\",\n            ),\n        ]\n        result = _get_agent_messages(msgs)\n\n        # Verify correct number of messages\n        self.assertEqual(len(result), 3)\n\n        # Verify first message (user)\n        self.assertEqual(result[0][\"role\"], \"user\")\n        self.assertEqual(result[0][\"name\"], \"user1\")\n        self.assertEqual(result[0][\"finish_reason\"], \"stop\")\n        self.assertEqual(result[0][\"parts\"][0][\"content\"], \"Hello\")\n\n        # Verify second message (assistant)\n        self.assertEqual(result[1][\"role\"], \"assistant\")\n        self.assertEqual(result[1][\"name\"], \"assistant1\")\n        self.assertEqual(result[1][\"finish_reason\"], \"stop\")\n        self.assertEqual(result[1][\"parts\"][0][\"content\"], \"Hi there!\")\n\n        # Verify third message (user)\n        self.assertEqual(result[2][\"role\"], \"user\")\n        self.assertEqual(result[2][\"name\"], \"user1\")\n        self.assertEqual(result[2][\"parts\"][0][\"content\"], \"How are you?\")\n\n    def test_get_agent_messages_empty_list(self) -> None:\n        \"\"\"Test _get_agent_messages with empty list.\"\"\"\n        result = _get_agent_messages([])\n        self.assertEqual(len(result), 0)\n\n    def test_get_agent_request_attributes(self) -> None:\n        \"\"\"Test _get_agent_request_attributes and _get_agent_span_name.\"\"\"\n        # Test with single Msg\n        msg = Msg(\n            \"test_user\",\n            [TextBlock(type=\"text\", text=\"Hello\")],\n            \"user\",\n        )\n\n        # Test request attributes\n        args = (msg,)\n        kwargs = {}\n        attributes = _get_agent_request_attributes(\n            self.mock_agent,\n            args,\n            kwargs,\n        )\n\n        self.assertEqual(\n            attributes[SpanAttributes.GEN_AI_OPERATION_NAME],\n            OperationNameValues.INVOKE_AGENT,\n        )\n        self.assertEqual(\n            attributes[SpanAttributes.GEN_AI_AGENT_ID],\n            \"agent-1\",\n        )\n        self.assertEqual(\n            attributes[SpanAttributes.GEN_AI_AGENT_NAME],\n            \"TestAgent\",\n        )\n        self.assertIn(SpanAttributes.GEN_AI_INPUT_MESSAGES, attributes)\n        self.assertIn(SpanAttributes.AGENTSCOPE_FUNCTION_INPUT, attributes)\n\n        # Test span name generation\n        span_name = _get_agent_span_name(attributes)\n        self.assertEqual(span_name, \"invoke_agent TestAgent\")\n\n    def test_get_agent_request_attributes_with_list(self) -> None:\n        \"\"\"Test _get_agent_request_attributes with list of Msg inputs.\"\"\"\n        msgs = [\n            Msg(\n                \"user1\",\n                [TextBlock(type=\"text\", text=\"Hello\")],\n                \"user\",\n            ),\n            Msg(\n                \"assistant1\",\n                [TextBlock(type=\"text\", text=\"Hi!\")],\n                \"assistant\",\n            ),\n        ]\n\n        # Test with list in args\n        args = (msgs,)\n        kwargs = {}\n        attributes = _get_agent_request_attributes(\n            self.mock_agent,\n            args,\n            kwargs,\n        )\n\n        self.assertEqual(\n            attributes[SpanAttributes.GEN_AI_OPERATION_NAME],\n            OperationNameValues.INVOKE_AGENT,\n        )\n        self.assertIn(SpanAttributes.GEN_AI_INPUT_MESSAGES, attributes)\n\n        # Test with list in kwargs\n        args = ()\n        kwargs = {\"msg\": msgs}\n        attributes = _get_agent_request_attributes(\n            self.mock_agent,\n            args,\n            kwargs,\n        )\n        self.assertIn(SpanAttributes.GEN_AI_INPUT_MESSAGES, attributes)\n\n    def test_get_agent_response_attributes(self) -> None:\n        \"\"\"Test _get_agent_response_attributes with single Msg.\"\"\"\n        response = Msg(\n            \"assistant\",\n            [TextBlock(type=\"text\", text=\"Hi\")],\n            \"assistant\",\n        )\n        attributes = _get_agent_response_attributes(response)\n\n        self.assertIn(SpanAttributes.GEN_AI_OUTPUT_MESSAGES, attributes)\n        self.assertIn(SpanAttributes.AGENTSCOPE_FUNCTION_OUTPUT, attributes)\n\n    def test_get_agent_response_attributes_with_list(self) -> None:\n        \"\"\"Test _get_agent_response_attributes with list of Msg.\"\"\"\n        responses = [\n            Msg(\n                \"assistant\",\n                [TextBlock(type=\"text\", text=\"First response\")],\n                \"assistant\",\n            ),\n            Msg(\n                \"assistant\",\n                [TextBlock(type=\"text\", text=\"Second response\")],\n                \"assistant\",\n            ),\n        ]\n        attributes = _get_agent_response_attributes(responses)\n\n        self.assertIn(SpanAttributes.GEN_AI_OUTPUT_MESSAGES, attributes)\n        self.assertIn(SpanAttributes.AGENTSCOPE_FUNCTION_OUTPUT, attributes)\n\n    def test_get_tool_request_attributes(self) -> None:\n        \"\"\"Test _get_tool_request_attributes and _get_tool_span_name.\"\"\"\n        # Create a mock toolkit with tool definition\n        toolkit = Mock(spec=Toolkit)\n        tool_func = Mock()\n        tool_func.json_schema = {\n            \"function\": {\n                \"description\": \"Test tool description\",\n            },\n        }\n        toolkit.tools = {\"test_tool\": tool_func}\n\n        tool_call = ToolUseBlock(\n            type=\"tool_use\",\n            id=\"call-1\",\n            name=\"test_tool\",\n            input={\"arg1\": \"value1\"},\n        )\n\n        attributes = _get_tool_request_attributes(toolkit, tool_call)\n\n        self.assertEqual(\n            attributes[SpanAttributes.GEN_AI_OPERATION_NAME],\n            OperationNameValues.EXECUTE_TOOL,\n        )\n        self.assertEqual(\n            attributes[SpanAttributes.GEN_AI_TOOL_CALL_ID],\n            \"call-1\",\n        )\n        self.assertEqual(\n            attributes[SpanAttributes.GEN_AI_TOOL_NAME],\n            \"test_tool\",\n        )\n        self.assertIn(SpanAttributes.GEN_AI_TOOL_CALL_ARGUMENTS, attributes)\n        self.assertIn(SpanAttributes.GEN_AI_TOOL_DESCRIPTION, attributes)\n\n        # Test span name generation\n        span_name = _get_tool_span_name(attributes)\n        self.assertEqual(span_name, \"execute_tool test_tool\")\n\n    def test_get_tool_response_attributes(self) -> None:\n        \"\"\"Test _get_tool_response_attributes.\"\"\"\n        response = {\"result\": \"success\"}\n        attributes = _get_tool_response_attributes(response)\n\n        self.assertIn(SpanAttributes.GEN_AI_TOOL_CALL_RESULT, attributes)\n        self.assertIn(SpanAttributes.AGENTSCOPE_FUNCTION_OUTPUT, attributes)\n\n    def test_get_formatter_request_attributes(self) -> None:\n        \"\"\"Test formatter request_attributes and span_name.\"\"\"\n        args = ()\n        kwargs = {}\n\n        attributes = _get_formatter_request_attributes(\n            self.mock_formatter,\n            args,\n            kwargs,\n        )\n\n        self.assertEqual(\n            attributes[SpanAttributes.GEN_AI_OPERATION_NAME],\n            OperationNameValues.FORMATTER,\n        )\n        self.assertIn(SpanAttributes.AGENTSCOPE_FORMAT_TARGET, attributes)\n        self.assertIn(SpanAttributes.AGENTSCOPE_FUNCTION_INPUT, attributes)\n\n        # Test span name generation\n        span_name = _get_formatter_span_name(attributes)\n        self.assertEqual(span_name, \"format openai\")\n\n    def test_get_formatter_response_attributes(self) -> None:\n        \"\"\"Test _get_formatter_response_attributes.\"\"\"\n        response = [{\"role\": \"user\", \"content\": \"Hello\"}]\n        attributes = _get_formatter_response_attributes(response)\n\n        self.assertIn(SpanAttributes.AGENTSCOPE_FUNCTION_OUTPUT, attributes)\n        self.assertEqual(\n            attributes[SpanAttributes.AGENTSCOPE_FORMAT_COUNT],\n            1,\n        )\n\n    def test_get_generic_function_request_attributes(self) -> None:\n        \"\"\"Test generic function request_attributes, span_name and response.\"\"\"\n        args = (1, 2, 3)\n        kwargs = {\"key\": \"value\"}\n\n        attributes = _get_generic_function_request_attributes(\n            \"test_function\",\n            args,\n            kwargs,\n        )\n\n        self.assertEqual(\n            attributes[SpanAttributes.GEN_AI_OPERATION_NAME],\n            OperationNameValues.INVOKE_GENERIC_FUNCTION,\n        )\n        self.assertEqual(\n            attributes[SpanAttributes.AGENTSCOPE_FUNCTION_NAME],\n            \"test_function\",\n        )\n        self.assertIn(SpanAttributes.AGENTSCOPE_FUNCTION_INPUT, attributes)\n\n        # Test span name generation\n        span_name = _get_generic_function_span_name(attributes)\n        self.assertEqual(span_name, \"invoke_generic_function test_function\")\n\n        # Test response attributes\n        response = {\"result\": \"success\"}\n        response_attributes = _get_generic_function_response_attributes(\n            response,\n        )\n        self.assertIn(\n            SpanAttributes.AGENTSCOPE_FUNCTION_OUTPUT,\n            response_attributes,\n        )\n\n    def test_get_embedding_request_attributes(self) -> None:\n        \"\"\"Test _get_embedding_request_attributes, span_name and response.\"\"\"\n        args = ()\n        kwargs = {\"dimensions\": 768}\n\n        attributes = _get_embedding_request_attributes(\n            self.mock_embedding,\n            args,\n            kwargs,\n        )\n\n        self.assertEqual(\n            attributes[SpanAttributes.GEN_AI_OPERATION_NAME],\n            OperationNameValues.EMBEDDINGS,\n        )\n        self.assertEqual(\n            attributes[SpanAttributes.GEN_AI_REQUEST_MODEL],\n            \"embedding-model\",\n        )\n        self.assertEqual(\n            attributes[SpanAttributes.GEN_AI_EMBEDDINGS_DIMENSION_COUNT],\n            768,\n        )\n\n        # Test span name generation\n        span_name = _get_embedding_span_name(attributes)\n        self.assertEqual(span_name, \"embeddings embedding-model\")\n\n        # Test response attributes\n        response = [[0.1, 0.2, 0.3]]\n        response_attributes = _get_embedding_response_attributes(response)\n        self.assertIn(\n            SpanAttributes.AGENTSCOPE_FUNCTION_OUTPUT,\n            response_attributes,\n        )\n"
  },
  {
    "path": "tests/tracing_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unittests for the tracing functionality in AgentScope.\"\"\"\nfrom typing import (\n    AsyncGenerator,\n    Generator,\n    Any,\n)\nfrom unittest import IsolatedAsyncioTestCase\n\nfrom agentscope import _config\nfrom agentscope.agent import AgentBase\nfrom agentscope.embedding import EmbeddingModelBase\nfrom agentscope.formatter import FormatterBase\nfrom agentscope.message import (\n    TextBlock,\n    Msg,\n    ToolUseBlock,\n)\nfrom agentscope.model import ChatModelBase, ChatResponse\nfrom agentscope.tool import Toolkit, ToolResponse\nfrom agentscope.tracing import (\n    trace,\n    trace_llm,\n    trace_reply,\n    trace_format,\n    trace_embedding,\n)\n\n\nclass TracingTest(IsolatedAsyncioTestCase):\n    \"\"\"Test cases for tracing functionality\"\"\"\n\n    async def asyncSetUp(self) -> None:\n        \"\"\"Set up the environment\"\"\"\n        _config.trace_enabled = True\n\n    async def test_trace(self) -> None:\n        \"\"\"Test the basic tracing functionality\"\"\"\n\n        @trace(name=\"test_func\")\n        async def test_func(x: int) -> int:\n            \"\"\"Test async function\"\"\" \"\"\n            return x * 2\n\n        result = await test_func(5)\n        self.assertEqual(result, 10)\n\n        @trace(name=\"test_gen\")\n        async def test_gen() -> AsyncGenerator[str, None]:\n            \"\"\"Test async generator\"\"\"\n            for i in range(3):\n                yield f\"chunk_{i}\"\n\n        results = [_ async for _ in test_gen()]\n        self.assertListEqual(results, [\"chunk_0\", \"chunk_1\", \"chunk_2\"])\n\n        @trace(name=\"test_func_return_with_sync_gen\")\n        async def test_func_return_with_sync_gen() -> Generator[\n            str,\n            None,\n            None,\n        ]:\n            \"\"\"Test async func returning sync generator\"\"\"\n\n            def sync_gen() -> Generator[str, None, None]:\n                \"\"\"sync generator\"\"\"\n                for i in range(3):\n                    yield f\"sync_chunk_{i}\"\n\n            return sync_gen()\n\n        results = list(await test_func_return_with_sync_gen())\n        self.assertListEqual(\n            results,\n            [\"sync_chunk_0\", \"sync_chunk_1\", \"sync_chunk_2\"],\n        )\n\n        @trace(name=\"sync_func\")\n        def sync_func(x: int) -> int:\n            \"\"\"Test synchronous function\"\"\"\n            return x + 3\n\n        result = sync_func(4)\n        self.assertEqual(result, 7)\n\n        @trace(name=\"sync_gen\")\n        def sync_gen() -> Generator[str, None, None]:\n            \"\"\"Test synchronous generator\"\"\"\n            for i in range(3):\n                yield f\"sync_chunk_{i}\"\n\n        results = list(sync_gen())\n        self.assertListEqual(\n            results,\n            [\"sync_chunk_0\", \"sync_chunk_1\", \"sync_chunk_2\"],\n        )\n\n        @trace(name=\"sync_func_return_with_async_gen\")\n        def sync_func_return_with_async_gen() -> AsyncGenerator[str, None]:\n            \"\"\"Test sync func returning async generator\"\"\"\n\n            async def async_gen() -> AsyncGenerator[str, None]:\n                \"\"\"async generator\"\"\"\n                for i in range(3):\n                    yield f\"chunk_{i}\"\n\n            return async_gen()\n\n        results = [_ async for _ in sync_func_return_with_async_gen()]\n        self.assertListEqual(results, [\"chunk_0\", \"chunk_1\", \"chunk_2\"])\n\n        # Error handling\n        @trace(name=\"error_sync_func\")\n        def error_sync_func() -> int:\n            \"\"\"Test error handling in sync function\"\"\"\n            raise ValueError(\"Negative value not allowed\")\n\n        with self.assertRaises(ValueError):\n            error_sync_func()\n\n        @trace(name=\"error_async_func\")\n        async def error_async_func() -> int:\n            \"\"\"Test error handling in async function\"\"\"\n            raise ValueError(\"Negative value not allowed\")\n\n        with self.assertRaises(ValueError):\n            await error_async_func()\n\n    async def test_trace_llm(self) -> None:\n        \"\"\"Test tracing LLM\"\"\"\n\n        class LLM(ChatModelBase):\n            \"\"\"Test LLM class\"\"\"\n\n            def __init__(self, stream: bool, raise_error: bool) -> None:\n                \"\"\"Initialize LLM\"\"\"\n                super().__init__(\"test\", stream)\n                self.raise_error = raise_error\n\n            @trace_llm\n            async def __call__(\n                self,\n                messages: list[dict],\n                **kwargs: Any,\n            ) -> AsyncGenerator[ChatResponse, None] | ChatResponse:\n                \"\"\"Simulate LLM call\"\"\"\n\n                if self.raise_error:\n                    raise ValueError(\"Simulated error in LLM call\")\n\n                if self.stream:\n\n                    async def generator() -> AsyncGenerator[\n                        ChatResponse,\n                        None,\n                    ]:\n                        for i in range(3):\n                            yield ChatResponse(\n                                id=f\"msg_{i}\",\n                                content=[\n                                    TextBlock(\n                                        type=\"text\",\n                                        text=\"x\" * (i + 1),\n                                    ),\n                                ],\n                            )\n\n                    return generator()\n                return ChatResponse(\n                    id=\"msg_0\",\n                    content=[\n                        TextBlock(\n                            type=\"text\",\n                            text=\"Hello, world!\",\n                        ),\n                    ],\n                )\n\n        stream_llm = LLM(True, False)\n        res = [_.content async for _ in await stream_llm([])]\n        self.assertListEqual(\n            res,\n            [\n                [TextBlock(type=\"text\", text=\"x\")],\n                [TextBlock(type=\"text\", text=\"xx\")],\n                [TextBlock(type=\"text\", text=\"xxx\")],\n            ],\n        )\n\n        non_stream_llm = LLM(False, False)\n        res = await non_stream_llm([])\n        self.assertListEqual(\n            res.content,\n            [\n                TextBlock(type=\"text\", text=\"Hello, world!\"),\n            ],\n        )\n\n        error_llm = LLM(False, True)\n        with self.assertRaises(ValueError):\n            await error_llm([])\n\n    async def test_trace_reply(self) -> None:\n        \"\"\"Test tracing reply\"\"\"\n\n        class Agent(AgentBase):\n            \"\"\"Test Agent class\"\"\"\n\n            @trace_reply\n            async def reply(self, raise_error: bool = False) -> Msg:\n                \"\"\"Simulate agent reply\"\"\"\n                if raise_error:\n                    raise ValueError(\"Simulated error in reply\")\n                return Msg(\n                    \"assistant\",\n                    [TextBlock(type=\"text\", text=\"Hello, world!\")],\n                    \"assistant\",\n                )\n\n            async def observe(self, msg: Msg) -> None:\n                raise NotImplementedError()\n\n            async def handle_interrupt(\n                self,\n                *args: Any,\n                **kwargs: Any,\n            ) -> Msg:\n                \"\"\"Handle interrupt\"\"\"\n                raise NotImplementedError()\n\n        agent = Agent()\n        res = await agent()\n        self.assertListEqual(\n            res.content,\n            [TextBlock(type=\"text\", text=\"Hello, world!\")],\n        )\n\n        with self.assertRaises(ValueError):\n            await agent.reply(raise_error=True)\n\n    async def test_trace_format(self) -> None:\n        \"\"\"Test tracing formatter\"\"\"\n\n        class Formatter(FormatterBase):\n            \"\"\"Test Formatter class\"\"\"\n\n            @trace_format\n            async def format(self, raise_error: bool = False) -> list[dict]:\n                \"\"\"Simulate formatting\"\"\"\n                if raise_error:\n                    raise ValueError(\"Simulated error in formatting\")\n                return [{\"role\": \"user\", \"content\": \"Hello, world!\"}]\n\n        formatter = Formatter()\n        res = await formatter.format()\n        self.assertListEqual(\n            res,\n            [{\"role\": \"user\", \"content\": \"Hello, world!\"}],\n        )\n\n        with self.assertRaises(ValueError):\n            await formatter.format(raise_error=True)\n\n    async def test_trace_toolkit(self) -> None:\n        \"\"\"Test tracing toolkit\"\"\"\n        toolkit = Toolkit()\n\n        def func(raise_error: bool) -> ToolResponse:\n            \"\"\"Test tool function\"\"\"\n            if raise_error:\n                raise ValueError(\"Simulated error in tool function\")\n            return ToolResponse(\n                content=[\n                    TextBlock(type=\"text\", text=\"Tool executed successfully\"),\n                ],\n            )\n\n        toolkit.register_tool_function(func)\n        res = await toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"xxx\",\n                name=\"func\",\n                input={\"raise_error\": False},\n            ),\n        )\n        async for chunk in res:\n            self.assertListEqual(\n                chunk.content,\n                [TextBlock(type=\"text\", text=\"Tool executed successfully\")],\n            )\n        res = await toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"xxx\",\n                name=\"func\",\n                input={\"raise_error\": True},\n            ),\n        )\n        async for chunk in res:\n            self.assertListEqual(\n                chunk.content,\n                [\n                    TextBlock(\n                        type=\"text\",\n                        text=\"Error: Simulated error in tool function\",\n                    ),\n                ],\n            )\n\n        async def gen_func(\n            raise_error: bool,\n        ) -> AsyncGenerator[ToolResponse, None]:\n            \"\"\"Test async generator tool function\"\"\"\n            yield ToolResponse(\n                content=[TextBlock(type=\"text\", text=\"Chunk 0\")],\n            )\n            if raise_error:\n                raise ValueError(\n                    \"Simulated error in async generator tool function\",\n                )\n            yield ToolResponse(\n                content=[TextBlock(type=\"text\", text=\"Chunk 1\")],\n            )\n\n        toolkit.register_tool_function(gen_func)\n        res = await toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"xxx\",\n                name=\"gen_func\",\n                input={\"raise_error\": False},\n            ),\n        )\n        index = 0\n        async for chunk in res:\n            self.assertListEqual(\n                chunk.content,\n                [TextBlock(type=\"text\", text=f\"Chunk {index}\")],\n            )\n            index += 1\n\n        res = await toolkit.call_tool_function(\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"xxx\",\n                name=\"gen_func\",\n                input={\"raise_error\": True},\n            ),\n        )\n        with self.assertRaises(ValueError):\n            async for _ in res:\n                pass\n\n    async def test_trace_embedding(self) -> None:\n        \"\"\"Test tracing embedding\"\"\"\n\n        class EmbeddingModel(EmbeddingModelBase):\n            \"\"\"Test embedding model class\"\"\"\n\n            def __init__(self) -> None:\n                \"\"\"Initialize embedding model\"\"\"\n                super().__init__(\"test_embedding\", 3)\n\n            @trace_embedding\n            async def __call__(self, raise_error: bool) -> list[list[float]]:\n                \"\"\"Simulate embedding call\"\"\"\n                if raise_error:\n                    raise ValueError(\"Simulated error in embedding call\")\n                return [[0, 1, 2]]\n\n        model = EmbeddingModel()\n        res = await model(False)\n        self.assertListEqual(res, [[0, 1, 2]])\n\n        with self.assertRaises(ValueError):\n            await model(True)\n\n    async def asyncTearDown(self) -> None:\n        \"\"\"Tear down the environment\"\"\"\n        _config.trace_enabled = True\n"
  },
  {
    "path": "tests/tracing_utils_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unit tests for the tracing utils module.\"\"\"\nfrom unittest import TestCase\nimport datetime\nimport enum\nimport json\nfrom dataclasses import dataclass\n\nfrom pydantic import BaseModel\n\nfrom agentscope.message import Msg, TextBlock\nfrom agentscope.tracing._utils import _to_serializable, _serialize_to_str\n\n\nclass ExampleEnum(enum.Enum):\n    \"\"\"Example enum for serialization.\"\"\"\n\n    VALUE1 = \"value1\"\n    VALUE2 = 42\n\n\n@dataclass\nclass ExampleDataClass:\n    \"\"\"Example dataclass for serialization.\"\"\"\n\n    name: str\n    age: int\n\n\nclass ExamplePydanticModel(BaseModel):\n    \"\"\"Example Pydantic model for serialization.\"\"\"\n\n    name: str\n    age: int\n\n\nclass UtilsTest(TestCase):\n    \"\"\"Test cases for the utils module.\"\"\"\n\n    def test_to_serializable_primitive_types(self) -> None:\n        \"\"\"Test _to_serializable with primitive types.\"\"\"\n        # Test string\n        self.assertEqual(_to_serializable(\"hello\"), \"hello\")\n\n        # Test integer\n        self.assertEqual(_to_serializable(42), 42)\n\n        # Test boolean\n        self.assertEqual(_to_serializable(True), True)\n        self.assertEqual(_to_serializable(False), False)\n\n        # Test float\n        self.assertEqual(_to_serializable(3.14), 3.14)\n\n        # Test None\n        self.assertIsNone(_to_serializable(None))\n\n    def test_to_serializable_collections(self) -> None:\n        \"\"\"Test _to_serializable with collections.\"\"\"\n        # Test list\n        result = _to_serializable([1, 2, 3])\n        self.assertEqual(result, [1, 2, 3])\n\n        # Test tuple\n        result = _to_serializable((1, 2, 3))\n        self.assertEqual(result, [1, 2, 3])\n\n        # Test set\n        result = _to_serializable({1, 2, 3})\n        self.assertIsInstance(result, list)\n        self.assertEqual(set(result), {1, 2, 3})\n\n        # Test frozenset\n        result = _to_serializable(frozenset([1, 2, 3]))\n        self.assertIsInstance(result, list)\n        self.assertEqual(set(result), {1, 2, 3})\n\n    def test_to_serializable_dict(self) -> None:\n        \"\"\"Test _to_serializable with dictionaries.\"\"\"\n        # Test simple dict\n        result = _to_serializable({\"key\": \"value\", \"num\": 42})\n        self.assertEqual(result, {\"key\": \"value\", \"num\": 42})\n\n        # Test nested dict\n        result = _to_serializable({\"nested\": {\"key\": \"value\"}})\n        self.assertEqual(result, {\"nested\": {\"key\": \"value\"}})\n\n        # Test dict with non-string keys\n        result = _to_serializable({1: \"one\", 2: \"two\"})\n        self.assertEqual(result, {\"1\": \"one\", \"2\": \"two\"})\n\n    def test_to_serializable_msg(self) -> None:\n        \"\"\"Test _to_serializable with Msg objects.\"\"\"\n        msg = Msg(\"user\", [TextBlock(type=\"text\", text=\"Hello\")], \"user\")\n        result = _to_serializable(msg)\n        self.assertIsInstance(result, str)\n        self.assertIn(\"Msg\", result)\n\n    def test_to_serializable_pydantic_model(self) -> None:\n        \"\"\"Test _to_serializable with Pydantic models.\"\"\"\n        model = ExamplePydanticModel(name=\"test\", age=42)\n        result = _to_serializable(model)\n        self.assertIsInstance(result, str)\n        self.assertIn(\"ExamplePydanticModel\", result)\n\n        # Test Pydantic class\n        result = _to_serializable(ExamplePydanticModel)\n        self.assertIsInstance(result, str)\n\n    def test_to_serializable_dataclass(self) -> None:\n        \"\"\"Test _to_serializable with dataclasses.\"\"\"\n        obj = ExampleDataClass(name=\"test\", age=42)\n        result = _to_serializable(obj)\n        self.assertIsInstance(result, str)\n        self.assertIn(\"ExampleDataClass\", result)\n\n    def test_to_serializable_datetime(self) -> None:\n        \"\"\"Test _to_serializable with datetime objects.\"\"\"\n        # Test date\n        date_obj = datetime.date(2024, 1, 1)\n        result = _to_serializable(date_obj)\n        self.assertEqual(result, \"2024-01-01\")\n\n        # Test datetime\n        dt_obj = datetime.datetime(2024, 1, 1, 12, 30, 45)\n        result = _to_serializable(dt_obj)\n        self.assertEqual(result, \"2024-01-01T12:30:45\")\n\n        # Test time\n        time_obj = datetime.time(12, 30, 45)\n        result = _to_serializable(time_obj)\n        self.assertEqual(result, \"12:30:45\")\n\n    def test_to_serializable_timedelta(self) -> None:\n        \"\"\"Test _to_serializable with timedelta objects.\"\"\"\n        delta = datetime.timedelta(days=1, hours=2, minutes=30)\n        result = _to_serializable(delta)\n        self.assertIsInstance(result, (int, float))\n        self.assertGreater(result, 0)\n\n    def test_to_serializable_enum(self) -> None:\n        \"\"\"Test _to_serializable with enum objects.\"\"\"\n        result = _to_serializable(ExampleEnum.VALUE1)\n        self.assertEqual(result, \"value1\")\n\n        result = _to_serializable(ExampleEnum.VALUE2)\n        self.assertEqual(result, 42)\n\n    def test_to_serializable_unknown_type(self) -> None:\n        \"\"\"Test _to_serializable with unknown types.\"\"\"\n\n        class CustomClass:\n            \"\"\"Custom class for testing.\"\"\"\n\n            def __init__(self) -> None:\n                self.value = \"test\"\n\n        obj = CustomClass()\n        result = _to_serializable(obj)\n        self.assertIsInstance(result, str)\n\n    def test_to_serializable_nested_structures(self) -> None:\n        \"\"\"Test _to_serializable with nested structures.\"\"\"\n        data = {\n            \"list\": [1, 2, {\"nested\": \"value\"}],\n            \"tuple\": (1, 2, 3),\n            \"set\": {1, 2, 3},\n        }\n        result = _to_serializable(data)\n        self.assertIsInstance(result, dict)\n        self.assertIsInstance(result[\"list\"], list)\n        self.assertIsInstance(result[\"tuple\"], list)\n        self.assertIsInstance(result[\"set\"], list)\n\n    def test_serialize_to_str_simple(self) -> None:\n        \"\"\"Test _serialize_to_str with simple types.\"\"\"\n        # Test string\n        result = _serialize_to_str(\"hello\")\n        self.assertEqual(result, '\"hello\"')\n\n        # Test integer\n        result = _serialize_to_str(42)\n        self.assertEqual(result, \"42\")\n\n        # Test boolean\n        result = _serialize_to_str(True)\n        self.assertEqual(result, \"true\")\n\n        # Test None\n        result = _serialize_to_str(None)\n        self.assertEqual(result, \"null\")\n\n    def test_serialize_to_str_list(self) -> None:\n        \"\"\"Test _serialize_to_str with lists.\"\"\"\n        numbers = [1, 2, 3]\n        result = _serialize_to_str(numbers)\n        self.assertEqual(result, json.dumps(numbers))\n\n        strings = [\"a\", \"b\", \"c\"]\n        result = _serialize_to_str(strings)\n        self.assertEqual(result, json.dumps(strings))\n\n    def test_serialize_to_str_dict(self) -> None:\n        \"\"\"Test _serialize_to_str with dictionaries.\"\"\"\n        result = _serialize_to_str({\"key\": \"value\", \"num\": 42})\n        self.assertIn(\"key\", result)\n        self.assertIn(\"value\", result)\n        self.assertIn(\"num\", result)\n        self.assertIn(\"42\", result)\n\n    def test_serialize_to_str_non_serializable(self) -> None:\n        \"\"\"Test _serialize_to_str with non-serializable objects.\"\"\"\n        msg = Msg(\"user\", [TextBlock(type=\"text\", text=\"Hello\")], \"user\")\n        result = _serialize_to_str(msg)\n        self.assertIsInstance(result, str)\n        self.assertIn(\"Msg\", result)\n\n    def test_serialize_to_str_unicode(self) -> None:\n        \"\"\"Test _serialize_to_str with unicode characters.\"\"\"\n        result = _serialize_to_str(\"hi\")\n        self.assertIn(\"hi\", result)\n\n    def test_serialize_to_str_complex_nested(self) -> None:\n        \"\"\"Test _serialize_to_str with complex nested structures.\"\"\"\n        data = {\n            \"list\": [1, 2, {\"nested\": \"value\"}],\n            \"datetime\": datetime.datetime(2024, 1, 1),\n            \"enum\": ExampleEnum.VALUE1,\n        }\n        result = _serialize_to_str(data)\n        self.assertIsInstance(result, str)\n        self.assertIn(\"list\", result)\n"
  },
  {
    "path": "tests/tts_dashscope_cosyvoice_test.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=protected-access\n\"\"\"The unittests for DashScope CosyVoice TTS models.\"\"\"\nimport base64\nfrom typing import AsyncGenerator\nfrom unittest import IsolatedAsyncioTestCase\nfrom unittest.mock import Mock, patch, AsyncMock, MagicMock\n\nfrom agentscope.message import Msg, AudioBlock, Base64Source\nfrom agentscope.tts import (\n    DashScopeCosyVoiceRealtimeTTSModel,\n    DashScopeCosyVoiceTTSModel,\n    TTSResponse,\n)\n\n\nclass DashScopeCosyVoiceTTSModelTest(IsolatedAsyncioTestCase):\n    \"\"\"The unittests for DashScope CosyVoice TTS model (non-realtime).\"\"\"\n\n    def setUp(self) -> None:\n        \"\"\"Set up the test case.\"\"\"\n        self.api_key = \"test_api_key\"\n        self.mock_audio_bytes = b\"fake_audio_data_bytes\"\n        self.mock_audio_base64 = base64.b64encode(\n            self.mock_audio_bytes,\n        ).decode(\"utf-8\")\n\n    def _create_mock_dashscope_modules(self) -> dict:\n        \"\"\"Create mock dashscope modules for patching.\"\"\"\n        mock_audio_format = MagicMock()\n        mock_audio_format.PCM_24000HZ_MONO_16BIT = \"pcm_24000hz_mono_16bit\"\n\n        mock_speech_synthesizer = MagicMock()\n\n        mock_tts_v2 = MagicMock()\n        mock_tts_v2.SpeechSynthesizer = mock_speech_synthesizer\n        mock_tts_v2.AudioFormat = mock_audio_format\n        mock_tts_v2.ResultCallback = Mock\n\n        mock_audio = MagicMock()\n        mock_audio.tts_v2 = mock_tts_v2\n\n        mock_dashscope = MagicMock()\n        mock_dashscope.api_key = None\n        mock_dashscope.audio = mock_audio\n\n        return {\n            \"dashscope\": mock_dashscope,\n            \"dashscope.audio\": mock_audio,\n            \"dashscope.audio.tts_v2\": mock_tts_v2,\n        }\n\n    def test_init(self) -> None:\n        \"\"\"Test initialization of DashScopeCosyVoiceTTSModel.\"\"\"\n        mock_modules = self._create_mock_dashscope_modules()\n\n        with patch.dict(\"sys.modules\", mock_modules):\n            model = DashScopeCosyVoiceTTSModel(\n                api_key=self.api_key,\n                model_name=\"cosyvoice-v3-plus\",\n                voice=\"longanyang\",\n                stream=False,\n            )\n            self.assertEqual(model.model_name, \"cosyvoice-v3-plus\")\n            self.assertEqual(model.voice, \"longanyang\")\n            self.assertFalse(model.stream)\n            self.assertFalse(model.supports_streaming_input)\n\n    async def test_synthesize_non_streaming(self) -> None:\n        \"\"\"Test synthesize method in non-streaming mode.\"\"\"\n        mock_modules = self._create_mock_dashscope_modules()\n\n        with patch.dict(\"sys.modules\", mock_modules):\n            model = DashScopeCosyVoiceTTSModel(\n                api_key=self.api_key,\n                stream=False,\n            )\n\n            # Mock _create_synthesizer to return a mock synthesizer\n            mock_synthesizer = MagicMock()\n            mock_synthesizer.call.return_value = self.mock_audio_bytes\n            model._create_synthesizer = Mock(\n                return_value=(mock_synthesizer, None),\n            )\n\n            msg = Msg(name=\"user\", content=\"Hello! Test message.\", role=\"user\")\n            response = await model.synthesize(msg)\n\n            self.assertIsInstance(response, TTSResponse)\n            self.assertEqual(response.content[\"type\"], \"audio\")\n            self.assertEqual(\n                response.content[\"source\"][\"data\"],\n                self.mock_audio_base64,\n            )\n            mock_synthesizer.call.assert_called_once_with(\n                text=\"Hello! Test message.\",\n            )\n\n    async def test_synthesize_streaming(self) -> None:\n        \"\"\"Test synthesize method in streaming mode.\"\"\"\n        mock_modules = self._create_mock_dashscope_modules()\n\n        with patch.dict(\"sys.modules\", mock_modules):\n            model = DashScopeCosyVoiceTTSModel(\n                api_key=self.api_key,\n                stream=True,\n            )\n\n            # Create mock callback with proper async generator\n            mock_callback = MagicMock()\n\n            async def mock_generator() -> AsyncGenerator[TTSResponse, None]:\n                yield TTSResponse(\n                    content=AudioBlock(\n                        type=\"audio\",\n                        source=Base64Source(\n                            type=\"base64\",\n                            data=self.mock_audio_base64,\n                            media_type=\"audio/pcm;rate=24000\",\n                        ),\n                    ),\n                    is_last=False,\n                )\n                yield TTSResponse(\n                    content=AudioBlock(\n                        type=\"audio\",\n                        source=Base64Source(\n                            type=\"base64\",\n                            data=self.mock_audio_base64,\n                            media_type=\"audio/pcm;rate=24000\",\n                        ),\n                    ),\n                    is_last=True,\n                )\n\n            # Directly return the generator object\n            mock_callback.get_audio_chunk = Mock(return_value=mock_generator())\n\n            mock_synthesizer = MagicMock()\n            model._create_synthesizer = Mock(\n                return_value=(mock_synthesizer, mock_callback),\n            )\n\n            msg = Msg(name=\"user\", content=\"Test streaming.\", role=\"user\")\n            response = await model.synthesize(msg)\n\n            # Verify response is an async generator\n            self.assertIsInstance(response, AsyncGenerator)\n            chunks = [chunk async for chunk in response]\n\n            # Verify we got some chunks\n            self.assertGreater(len(chunks), 0)\n            # Verify each chunk is a TTSResponse\n            for chunk in chunks:\n                self.assertIsInstance(chunk, TTSResponse)\n            mock_synthesizer.call.assert_called_once_with(\n                text=\"Test streaming.\",\n            )\n\n\nclass DashScopeCosyVoiceRealtimeTTSModelTest(IsolatedAsyncioTestCase):\n    \"\"\"The unittests for DashScope CosyVoice Realtime TTS model.\"\"\"\n\n    def setUp(self) -> None:\n        \"\"\"Set up the test case.\"\"\"\n        self.api_key = \"test_api_key\"\n        self.mock_audio_bytes = b\"fake_audio_data_bytes\"\n        self.mock_audio_base64 = base64.b64encode(\n            self.mock_audio_bytes,\n        ).decode(\"utf-8\")\n\n    def _create_mock_dashscope_modules(self) -> dict:\n        \"\"\"Create mock dashscope modules for patching.\"\"\"\n        mock_audio_format = MagicMock()\n        mock_audio_format.PCM_24000HZ_MONO_16BIT = \"pcm_24000hz_mono_16bit\"\n\n        mock_speech_synthesizer = MagicMock()\n\n        mock_tts_v2 = MagicMock()\n        mock_tts_v2.SpeechSynthesizer = mock_speech_synthesizer\n        mock_tts_v2.AudioFormat = mock_audio_format\n        mock_tts_v2.ResultCallback = Mock\n\n        mock_audio = MagicMock()\n        mock_audio.tts_v2 = mock_tts_v2\n\n        mock_dashscope = MagicMock()\n        mock_dashscope.api_key = None\n        mock_dashscope.audio = mock_audio\n\n        return {\n            \"dashscope\": mock_dashscope,\n            \"dashscope.audio\": mock_audio,\n            \"dashscope.audio.tts_v2\": mock_tts_v2,\n        }\n\n    def _create_mock_callback(self) -> MagicMock:\n        \"\"\"Create a mock callback for testing.\"\"\"\n        mock_callback = MagicMock()\n        mock_callback.chunk_event = MagicMock()\n        mock_callback.finish_event = MagicMock()\n        mock_callback._audio_bytes = b\"\"\n        mock_callback._audio_base64 = \"\"\n        mock_callback._last_encoded_pos = 0\n        return mock_callback\n\n    def test_init(self) -> None:\n        \"\"\"Test initialization of DashScopeCosyVoiceRealtimeTTSModel.\"\"\"\n        mock_modules = self._create_mock_dashscope_modules()\n\n        with patch.dict(\"sys.modules\", mock_modules):\n            # Mock _get_cosyvoice_callback_class\n            with patch(\n                \"agentscope.tts._dashscope_cosyvoice_realtime_tts_model\"\n                \"._get_cosyvoice_callback_class\",\n            ) as mock_get_callback:\n                mock_callback_class = MagicMock()\n                mock_callback_class.return_value = self._create_mock_callback()\n                mock_get_callback.return_value = mock_callback_class\n\n                model = DashScopeCosyVoiceRealtimeTTSModel(\n                    api_key=self.api_key,\n                    model_name=\"cosyvoice-v3-plus\",\n                    voice=\"longanyang\",\n                    stream=True,\n                )\n                self.assertEqual(model.model_name, \"cosyvoice-v3-plus\")\n                self.assertEqual(model.voice, \"longanyang\")\n                self.assertTrue(model.stream)\n                self.assertTrue(model.supports_streaming_input)\n\n    async def test_push_incremental_text(self) -> None:\n        \"\"\"Test push method with incremental text chunks.\"\"\"\n        mock_modules = self._create_mock_dashscope_modules()\n        mock_synthesizer_instance = MagicMock()\n        mock_modules[\n            \"dashscope.audio.tts_v2\"\n        ].SpeechSynthesizer.return_value = mock_synthesizer_instance\n\n        with patch.dict(\"sys.modules\", mock_modules):\n            with patch(\n                \"agentscope.tts._dashscope_cosyvoice_realtime_tts_model\"\n                \"._get_cosyvoice_callback_class\",\n            ) as mock_get_callback:\n                mock_callback_class = MagicMock()\n                mock_callback = self._create_mock_callback()\n                mock_callback.get_audio_data = AsyncMock(\n                    return_value=TTSResponse(\n                        content=AudioBlock(\n                            type=\"audio\",\n                            source=Base64Source(\n                                type=\"base64\",\n                                data=self.mock_audio_base64,\n                                media_type=\"audio/pcm;rate=24000\",\n                            ),\n                        ),\n                    ),\n                )\n                mock_callback_class.return_value = mock_callback\n                mock_get_callback.return_value = mock_callback_class\n\n                model = DashScopeCosyVoiceRealtimeTTSModel(\n                    api_key=self.api_key,\n                )\n                await model.connect()\n\n                msg_id = \"test_msg_001\"\n                text_chunks = [\"Hello there!\\n\\n\", \"This is a test message.\"]\n\n                accumulated_text = \"\"\n                for chunk in text_chunks:\n                    accumulated_text += chunk\n                    msg = Msg(\n                        name=\"user\",\n                        content=accumulated_text,\n                        role=\"user\",\n                    )\n                    msg.id = msg_id\n\n                    response = await model.push(msg)\n                    self.assertIsInstance(response, TTSResponse)\n\n                # Verify streaming_call was called\n                self.assertGreater(\n                    mock_synthesizer_instance.streaming_call.call_count,\n                    0,\n                )\n\n    async def test_synthesize_non_streaming(self) -> None:\n        \"\"\"Test synthesize method in non-streaming mode.\"\"\"\n        mock_modules = self._create_mock_dashscope_modules()\n        mock_synthesizer_instance = MagicMock()\n        mock_modules[\n            \"dashscope.audio.tts_v2\"\n        ].SpeechSynthesizer.return_value = mock_synthesizer_instance\n\n        with patch.dict(\"sys.modules\", mock_modules):\n            with patch(\n                \"agentscope.tts._dashscope_cosyvoice_realtime_tts_model\"\n                \"._get_cosyvoice_callback_class\",\n            ) as mock_get_callback:\n                mock_callback_class = MagicMock()\n                mock_callback = self._create_mock_callback()\n                mock_callback.get_audio_data = AsyncMock(\n                    return_value=TTSResponse(\n                        content=AudioBlock(\n                            type=\"audio\",\n                            source=Base64Source(\n                                type=\"base64\",\n                                data=self.mock_audio_base64,\n                                media_type=\"audio/pcm;rate=24000\",\n                            ),\n                        ),\n                    ),\n                )\n                mock_callback_class.return_value = mock_callback\n                mock_get_callback.return_value = mock_callback_class\n\n                model = DashScopeCosyVoiceRealtimeTTSModel(\n                    api_key=self.api_key,\n                    stream=False,\n                )\n                await model.connect()\n\n                msg = Msg(\n                    name=\"user\",\n                    content=\"Hello! Test message.\",\n                    role=\"user\",\n                )\n                response = await model.synthesize(msg)\n\n                self.assertIsInstance(response, TTSResponse)\n                self.assertEqual(response.content[\"type\"], \"audio\")\n                mock_synthesizer_instance.streaming_complete.assert_called_once()  # noqa\n\n    async def test_synthesize_streaming(self) -> None:\n        \"\"\"Test synthesize method in streaming mode.\"\"\"\n        mock_modules = self._create_mock_dashscope_modules()\n        mock_synthesizer_instance = MagicMock()\n        mock_modules[\n            \"dashscope.audio.tts_v2\"\n        ].SpeechSynthesizer.return_value = mock_synthesizer_instance\n\n        with patch.dict(\"sys.modules\", mock_modules):\n            with patch(\n                \"agentscope.tts._dashscope_cosyvoice_realtime_tts_model\"\n                \"._get_cosyvoice_callback_class\",\n            ) as mock_get_callback:\n                mock_callback_class = MagicMock()\n                mock_callback = self._create_mock_callback()\n\n                async def mock_generator() -> AsyncGenerator[\n                    TTSResponse,\n                    None,\n                ]:\n                    yield TTSResponse(\n                        content=AudioBlock(\n                            type=\"audio\",\n                            source=Base64Source(\n                                type=\"base64\",\n                                data=self.mock_audio_base64,\n                                media_type=\"audio/pcm;rate=24000\",\n                            ),\n                        ),\n                        is_last=False,\n                    )\n                    yield TTSResponse(\n                        content=AudioBlock(\n                            type=\"audio\",\n                            source=Base64Source(\n                                type=\"base64\",\n                                data=self.mock_audio_base64,\n                                media_type=\"audio/pcm;rate=24000\",\n                            ),\n                        ),\n                        is_last=True,\n                    )\n\n                mock_callback.get_audio_chunk = Mock(\n                    return_value=mock_generator(),\n                )\n                mock_callback_class.return_value = mock_callback\n                mock_get_callback.return_value = mock_callback_class\n\n                model = DashScopeCosyVoiceRealtimeTTSModel(\n                    api_key=self.api_key,\n                    stream=True,\n                )\n                await model.connect()\n\n                msg = Msg(name=\"user\", content=\"Test streaming.\", role=\"user\")\n                response = await model.synthesize(msg)\n\n                # Verify response is an async generator\n                self.assertIsInstance(response, AsyncGenerator)\n                chunks = [chunk async for chunk in response]\n\n                # Verify we got some chunks\n                self.assertGreater(len(chunks), 0)\n                # Verify each chunk is a TTSResponse\n                for chunk in chunks:\n                    self.assertIsInstance(chunk, TTSResponse)\n"
  },
  {
    "path": "tests/tts_dashscope_test.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=protected-access\n\"\"\"The unittests for DashScope TTS models.\"\"\"\nimport base64\nfrom typing import AsyncGenerator\nfrom unittest import IsolatedAsyncioTestCase\nfrom unittest.mock import Mock, patch, AsyncMock, MagicMock\n\nfrom agentscope.message import Msg, AudioBlock, Base64Source\nfrom agentscope.tts import (\n    DashScopeRealtimeTTSModel,\n    DashScopeTTSModel,\n    TTSResponse,\n)\n\n\nclass DashScopeRealtimeTTSModelTest(IsolatedAsyncioTestCase):\n    \"\"\"The unittests for DashScope Realtime TTS model.\"\"\"\n\n    def setUp(self) -> None:\n        \"\"\"Set up the test case.\"\"\"\n        self.api_key = \"test_api_key\"\n        self.mock_audio_data = base64.b64encode(b\"fake_audio_data\").decode(\n            \"utf-8\",\n        )\n\n    def _create_mock_tts_client(self) -> Mock:\n        \"\"\"Create a mock QwenTtsRealtime client.\"\"\"\n        mock_client = Mock()\n        mock_client.connect = Mock()\n        mock_client.close = Mock()\n        mock_client.finish = Mock()\n        mock_client.update_session = Mock()\n        mock_client.append_text = Mock()\n        return mock_client\n\n    def _create_mock_dashscope_modules(self) -> dict:\n        \"\"\"Create mock dashscope modules for patching.\"\"\"\n        mock_qwen_tts_realtime = MagicMock()\n        mock_qwen_tts_realtime.QwenTtsRealtime = Mock\n        mock_qwen_tts_realtime.QwenTtsRealtimeCallback = Mock\n\n        mock_audio = MagicMock()\n        mock_audio.qwen_tts_realtime = mock_qwen_tts_realtime\n\n        mock_dashscope = MagicMock()\n        mock_dashscope.api_key = None\n        mock_dashscope.audio = mock_audio\n\n        return {\n            \"dashscope\": mock_dashscope,\n            \"dashscope.audio\": mock_audio,\n            \"dashscope.audio.qwen_tts_realtime\": mock_qwen_tts_realtime,\n        }\n\n    def test_init(self) -> None:\n        \"\"\"Test initialization of DashScopeRealtimeTTSModel.\"\"\"\n        mock_modules = self._create_mock_dashscope_modules()\n        mock_tts_client = self._create_mock_tts_client()\n        mock_tts_class = Mock(return_value=mock_tts_client)\n        mock_modules[\n            \"dashscope.audio.qwen_tts_realtime\"\n        ].QwenTtsRealtime = mock_tts_class\n\n        with patch.dict(\"sys.modules\", mock_modules):\n            model = DashScopeRealtimeTTSModel(\n                api_key=self.api_key,\n                stream=False,\n            )\n            self.assertEqual(model.model_name, \"qwen3-tts-flash-realtime\")\n            self.assertFalse(model.stream)\n            self.assertFalse(model._connected)\n\n    async def test_push_incremental_text(self) -> None:\n        \"\"\"Test push method with incremental text chunks.\"\"\"\n        mock_modules = self._create_mock_dashscope_modules()\n        mock_client = self._create_mock_tts_client()\n        mock_tts_class = Mock(return_value=mock_client)\n        mock_modules[\n            \"dashscope.audio.qwen_tts_realtime\"\n        ].QwenTtsRealtime = mock_tts_class\n\n        with patch.dict(\"sys.modules\", mock_modules):\n            async with DashScopeRealtimeTTSModel(\n                api_key=self.api_key,\n                stream=False,\n            ) as model:\n                # Mock callback to return audio data\n                model._dashscope_callback.get_audio_data = AsyncMock(\n                    return_value=TTSResponse(\n                        content=AudioBlock(\n                            type=\"audio\",\n                            source=Base64Source(\n                                type=\"base64\",\n                                data=self.mock_audio_data,\n                                media_type=\"audio/pcm;rate=24000\",\n                            ),\n                        ),\n                    ),\n                )\n\n                msg_id = \"test_msg_001\"\n                text_chunks = [\"Hello there!\\n\\n\", \"This is a test message.\"]\n\n                accumulated_text = \"\"\n                for chunk in text_chunks:\n                    accumulated_text += chunk\n                    msg = Msg(\n                        name=\"user\",\n                        content=accumulated_text,\n                        role=\"user\",\n                    )\n                    msg.id = msg_id\n\n                    response = await model.push(msg)\n                    self.assertIsInstance(response, TTSResponse)\n\n                # Verify append_text was called\n                self.assertGreater(mock_client.append_text.call_count, 0)\n\n    async def test_synthesize_non_streaming(self) -> None:\n        \"\"\"Test synthesize method in non-streaming mode.\"\"\"\n        mock_modules = self._create_mock_dashscope_modules()\n        mock_client = self._create_mock_tts_client()\n        mock_tts_class = Mock(return_value=mock_client)\n        mock_modules[\n            \"dashscope.audio.qwen_tts_realtime\"\n        ].QwenTtsRealtime = mock_tts_class\n\n        with patch.dict(\"sys.modules\", mock_modules):\n            async with DashScopeRealtimeTTSModel(\n                api_key=self.api_key,\n                stream=False,\n            ) as model:\n                model._dashscope_callback.get_audio_data = AsyncMock(\n                    return_value=TTSResponse(\n                        content=AudioBlock(\n                            type=\"audio\",\n                            source=Base64Source(\n                                type=\"base64\",\n                                data=self.mock_audio_data,\n                                media_type=\"audio/pcm;rate=24000\",\n                            ),\n                        ),\n                    ),\n                )\n\n                msg = Msg(\n                    name=\"user\",\n                    content=\"Hello! Test message.\",\n                    role=\"user\",\n                )\n                response = await model.synthesize(msg)\n\n                self.assertIsInstance(response, TTSResponse)\n                self.assertEqual(response.content[\"type\"], \"audio\")\n\n    async def test_synthesize_streaming(self) -> None:\n        \"\"\"Test synthesize method in streaming mode.\"\"\"\n        mock_modules = self._create_mock_dashscope_modules()\n        mock_client = self._create_mock_tts_client()\n        mock_tts_class = Mock(return_value=mock_client)\n        mock_modules[\n            \"dashscope.audio.qwen_tts_realtime\"\n        ].QwenTtsRealtime = mock_tts_class\n\n        with patch.dict(\"sys.modules\", mock_modules):\n            async with DashScopeRealtimeTTSModel(\n                api_key=self.api_key,\n                stream=True,\n            ) as model:\n\n                async def mock_generator() -> AsyncGenerator[\n                    TTSResponse,\n                    None,\n                ]:\n                    yield TTSResponse(\n                        content=AudioBlock(\n                            type=\"audio\",\n                            source=Base64Source(\n                                type=\"base64\",\n                                data=self.mock_audio_data,\n                                media_type=\"audio/pcm;rate=24000\",\n                            ),\n                        ),\n                    )\n                    yield TTSResponse(content=None)\n\n                model._dashscope_callback.get_audio_chunk = mock_generator\n\n                msg = Msg(name=\"user\", content=\"Test streaming.\", role=\"user\")\n                response = await model.synthesize(msg)\n\n                self.assertIsInstance(response, AsyncGenerator)\n                chunk_count = 0\n                async for chunk in response:\n                    self.assertIsInstance(chunk, TTSResponse)\n                    chunk_count += 1\n                self.assertGreater(chunk_count, 0)\n\n\nclass DashScopeTTSModelTest(IsolatedAsyncioTestCase):\n    \"\"\"The unittests for DashScope TTS model (non-realtime).\"\"\"\n\n    def setUp(self) -> None:\n        \"\"\"Set up the test case.\"\"\"\n        self.api_key = \"test_api_key\"\n        self.mock_audio_data = \"ZmFrZV9hdWRpb19kYXRh\"  # base64 encoded\n\n    def _create_mock_response_chunk(self, audio_data: str) -> Mock:\n        \"\"\"Create a mock response chunk.\"\"\"\n        mock_chunk = Mock()\n        mock_chunk.output = Mock()\n        mock_chunk.output.audio = Mock()\n        mock_chunk.output.audio.data = audio_data\n        return mock_chunk\n\n    def test_init(self) -> None:\n        \"\"\"Test initialization of DashScopeTTSModel.\"\"\"\n        model = DashScopeTTSModel(\n            api_key=self.api_key,\n            model_name=\"qwen3-tts-flash\",\n            voice=\"Cherry\",\n            stream=False,\n        )\n        self.assertEqual(model.model_name, \"qwen3-tts-flash\")\n        self.assertEqual(model.voice, \"Cherry\")\n        self.assertFalse(model.stream)\n        self.assertFalse(model.supports_streaming_input)\n\n    async def test_synthesize_non_streaming(self) -> None:\n        \"\"\"Test synthesize method in non-streaming mode.\"\"\"\n        model = DashScopeTTSModel(\n            api_key=self.api_key,\n            stream=False,\n        )\n\n        mock_chunks = [\n            self._create_mock_response_chunk(\"audio1\"),\n            self._create_mock_response_chunk(\"audio2\"),\n        ]\n\n        with patch(\"dashscope.MultiModalConversation.call\") as mock_call:\n            mock_call.return_value = iter(mock_chunks)\n\n            msg = Msg(name=\"user\", content=\"Hello! Test message.\", role=\"user\")\n            response = await model.synthesize(msg)\n\n            expected_content = AudioBlock(\n                type=\"audio\",\n                source=Base64Source(\n                    type=\"base64\",\n                    data=\"audio1audio2\",\n                    media_type=\"audio/pcm;rate=24000\",\n                ),\n            )\n            self.assertEqual(response.content, expected_content)\n\n    async def test_synthesize_streaming(self) -> None:\n        \"\"\"Test synthesize method in streaming mode.\"\"\"\n        model = DashScopeTTSModel(\n            api_key=self.api_key,\n            stream=True,\n        )\n\n        mock_chunks = [\n            self._create_mock_response_chunk(\"audio1\"),\n            self._create_mock_response_chunk(\"audio2\"),\n        ]\n\n        with patch(\"dashscope.MultiModalConversation.call\") as mock_call:\n            mock_call.return_value = iter(mock_chunks)\n\n            msg = Msg(name=\"user\", content=\"Test streaming.\", role=\"user\")\n            response = await model.synthesize(msg)\n\n            self.assertIsInstance(response, AsyncGenerator)\n            chunks = [chunk async for chunk in response]\n\n            # Should have 3 chunks: 2 from audio data + 1 final\n            self.assertEqual(len(chunks), 3)\n\n            # Chunk 1: accumulated \"audio1\"\n            self.assertEqual(\n                chunks[0].content,\n                AudioBlock(\n                    type=\"audio\",\n                    source=Base64Source(\n                        type=\"base64\",\n                        data=\"audio1\",\n                        media_type=\"audio/pcm;rate=24000\",\n                    ),\n                ),\n            )\n\n            # Chunk 2: accumulated \"audio1audio2\"\n            self.assertEqual(\n                chunks[1].content,\n                AudioBlock(\n                    type=\"audio\",\n                    source=Base64Source(\n                        type=\"base64\",\n                        data=\"audio1audio2\",\n                        media_type=\"audio/pcm;rate=24000\",\n                    ),\n                ),\n            )\n\n            # Final chunk: complete audio data\n            self.assertEqual(\n                chunks[2].content,\n                AudioBlock(\n                    type=\"audio\",\n                    source=Base64Source(\n                        type=\"base64\",\n                        data=\"audio1audio2\",\n                        media_type=\"audio/pcm;rate=24000\",\n                    ),\n                ),\n            )\n            self.assertTrue(chunks[2].is_last)\n"
  },
  {
    "path": "tests/tts_gemini_test.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=protected-access\n\"\"\"The unittests for Gemini TTS model.\"\"\"\nimport base64\nimport sys\nfrom typing import AsyncGenerator\nfrom unittest import IsolatedAsyncioTestCase\nfrom unittest.mock import Mock, patch, MagicMock\n\nfrom agentscope.message import Msg, AudioBlock, Base64Source\nfrom agentscope.tts import GeminiTTSModel\n\n\n# Create mock google.genai modules (required for import-time patching)\nmock_types = MagicMock()\nmock_types.GenerateContentConfig = Mock(return_value=Mock())\nmock_types.SpeechConfig = Mock(return_value=Mock())\nmock_types.VoiceConfig = Mock(return_value=Mock())\nmock_types.PrebuiltVoiceConfig = Mock(return_value=Mock())\n\nmock_genai = MagicMock()\nmock_genai.Client = Mock(return_value=MagicMock())\nmock_genai.types = mock_types\n\nmock_google = MagicMock()\nmock_google.genai = mock_genai\n\n\n@patch.dict(\n    sys.modules,\n    {\n        \"google\": mock_google,\n        \"google.genai\": mock_genai,\n        \"google.genai.types\": mock_types,\n    },\n)\nclass GeminiTTSModelTest(IsolatedAsyncioTestCase):\n    \"\"\"The unittests for Gemini TTS model.\"\"\"\n\n    def setUp(self) -> None:\n        \"\"\"Set up the test case.\"\"\"\n        self.api_key = \"test_api_key\"\n        self.mock_audio_bytes = b\"fake_audio_data\"\n        self.mock_audio_base64 = base64.b64encode(\n            self.mock_audio_bytes,\n        ).decode(\n            \"utf-8\",\n        )\n        self.mock_mime_type = \"audio/pcm;rate=24000\"\n\n    def _create_mock_response(\n        self,\n        audio_data: bytes,\n        mime_type: str,\n    ) -> MagicMock:\n        \"\"\"Create a mock Gemini response.\"\"\"\n        mock = MagicMock()\n        mock.candidates[0].content.parts[0].inline_data.data = audio_data\n        mock.candidates[0].content.parts[0].inline_data.mime_type = mime_type\n        return mock\n\n    def test_init(self) -> None:\n        \"\"\"Test initialization of GeminiTTSModel.\"\"\"\n        model = GeminiTTSModel(\n            api_key=self.api_key,\n            model_name=\"gemini-2.5-flash-preview-tts\",\n            voice=\"Kore\",\n            stream=False,\n        )\n        self.assertEqual(model.model_name, \"gemini-2.5-flash-preview-tts\")\n        self.assertEqual(model.voice, \"Kore\")\n        self.assertFalse(model.stream)\n        self.assertFalse(model.supports_streaming_input)\n\n    async def test_synthesize_non_streaming(self) -> None:\n        \"\"\"Test synthesize method in non-streaming mode.\"\"\"\n        model = GeminiTTSModel(\n            api_key=self.api_key,\n            stream=False,\n        )\n\n        # Mock the generate_content response\n        mock_response = self._create_mock_response(\n            self.mock_audio_bytes,\n            self.mock_mime_type,\n        )\n        model._client.models.generate_content = Mock(\n            return_value=mock_response,\n        )\n\n        msg = Msg(name=\"user\", content=\"Hello! Test message.\", role=\"user\")\n        response = await model.synthesize(msg)\n\n        expected_content = AudioBlock(\n            type=\"audio\",\n            source=Base64Source(\n                type=\"base64\",\n                data=self.mock_audio_base64,\n                media_type=self.mock_mime_type,\n            ),\n        )\n        self.assertEqual(response.content, expected_content)\n\n    async def test_synthesize_streaming(self) -> None:\n        \"\"\"Test synthesize method in streaming mode.\"\"\"\n        model = GeminiTTSModel(\n            api_key=self.api_key,\n            stream=True,\n        )\n\n        # Create mock streaming response chunks\n        chunk1_data = b\"audio_chunk_1\"\n        chunk2_data = b\"audio_chunk_2\"\n        mock_chunk1 = self._create_mock_response(\n            chunk1_data,\n            self.mock_mime_type,\n        )\n        mock_chunk2 = self._create_mock_response(\n            chunk2_data,\n            self.mock_mime_type,\n        )\n\n        # Mock streaming response\n        model._client.models.generate_content_stream = Mock(\n            return_value=iter([mock_chunk1, mock_chunk2]),\n        )\n\n        msg = Msg(name=\"user\", content=\"Test streaming.\", role=\"user\")\n        response = await model.synthesize(msg)\n\n        self.assertIsInstance(response, AsyncGenerator)\n        chunks = [chunk async for chunk in response]\n\n        # Should have 3 chunks: 2 from audio data + 1 final empty\n        self.assertEqual(len(chunks), 3)\n\n        chunk1_base64 = base64.b64encode(chunk1_data).decode(\"utf-8\")\n        chunk2_base64 = base64.b64encode(chunk2_data).decode(\"utf-8\")\n\n        # Chunk 1: accumulated chunk1\n        self.assertEqual(\n            chunks[0].content,\n            AudioBlock(\n                type=\"audio\",\n                source=Base64Source(\n                    type=\"base64\",\n                    data=chunk1_base64,\n                    media_type=self.mock_mime_type,\n                ),\n            ),\n        )\n\n        # Chunk 2: accumulated chunk1 + chunk2\n        self.assertEqual(\n            chunks[1].content,\n            AudioBlock(\n                type=\"audio\",\n                source=Base64Source(\n                    type=\"base64\",\n                    data=chunk1_base64 + chunk2_base64,\n                    media_type=self.mock_mime_type,\n                ),\n            ),\n        )\n\n        # Final chunk: empty\n        self.assertIsNone(chunks[2].content)\n"
  },
  {
    "path": "tests/tts_openai_test.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=protected-access\n\"\"\"The unittests for OpenAI TTS model.\"\"\"\nimport base64\nimport sys\nfrom typing import AsyncGenerator\nfrom unittest import IsolatedAsyncioTestCase\nfrom unittest.mock import Mock, patch, AsyncMock, MagicMock\n\nfrom agentscope.message import Msg, AudioBlock, Base64Source\nfrom agentscope.tts import OpenAITTSModel\n\n\n# Create mock openai module (required for import-time patching)\nmock_openai = MagicMock()\nmock_openai.AsyncOpenAI = Mock(return_value=MagicMock())\n\n\n@patch.dict(sys.modules, {\"openai\": mock_openai})\nclass OpenAITTSModelTest(IsolatedAsyncioTestCase):\n    \"\"\"The unittests for OpenAI TTS model.\"\"\"\n\n    def setUp(self) -> None:\n        \"\"\"Set up the test case.\"\"\"\n        self.api_key = \"test_api_key\"\n        self.mock_audio_bytes = b\"fake_audio_data\"\n        self.mock_audio_base64 = base64.b64encode(\n            self.mock_audio_bytes,\n        ).decode(\n            \"utf-8\",\n        )\n\n    def test_init(self) -> None:\n        \"\"\"Test initialization of OpenAITTSModel.\"\"\"\n        model = OpenAITTSModel(\n            api_key=self.api_key,\n            model_name=\"gpt-4o-mini-tts\",\n            voice=\"alloy\",\n            stream=False,\n        )\n        self.assertEqual(model.model_name, \"gpt-4o-mini-tts\")\n        self.assertEqual(model.voice, \"alloy\")\n        self.assertFalse(model.stream)\n        self.assertFalse(model.supports_streaming_input)\n\n    async def test_synthesize_non_streaming(self) -> None:\n        \"\"\"Test synthesize method in non-streaming mode.\"\"\"\n        model = OpenAITTSModel(\n            api_key=self.api_key,\n            stream=False,\n        )\n\n        # Mock the speech.create response\n        mock_response = Mock()\n        mock_response.content = self.mock_audio_bytes\n        model._client.audio.speech.create = AsyncMock(\n            return_value=mock_response,\n        )\n\n        msg = Msg(name=\"user\", content=\"Hello! Test message.\", role=\"user\")\n        response = await model.synthesize(msg)\n\n        expected_content = AudioBlock(\n            type=\"audio\",\n            source=Base64Source(\n                type=\"base64\",\n                data=self.mock_audio_base64,\n                media_type=\"audio/pcm\",\n            ),\n        )\n        self.assertEqual(response.content, expected_content)\n        model._client.audio.speech.create.assert_called_once()\n\n    async def test_synthesize_streaming(self) -> None:\n        \"\"\"Test synthesize method in streaming mode.\"\"\"\n        model = OpenAITTSModel(\n            api_key=self.api_key,\n            stream=True,\n        )\n\n        chunk1 = b\"audio_chunk_1\"\n        chunk2 = b\"audio_chunk_2\"\n\n        # Create mock streaming response inline\n        mock_stream = MagicMock()\n        mock_stream.__aenter__ = AsyncMock(return_value=mock_stream)\n        mock_stream.__aexit__ = AsyncMock(return_value=None)\n\n        async def mock_iter_bytes() -> AsyncGenerator[bytes, None]:\n            yield chunk1\n            yield chunk2\n\n        mock_stream.iter_bytes = mock_iter_bytes\n\n        model._client.audio.speech.with_streaming_response.create = Mock(\n            return_value=mock_stream,\n        )\n\n        msg = Msg(name=\"user\", content=\"Test streaming.\", role=\"user\")\n        response = await model.synthesize(msg)\n\n        self.assertIsInstance(response, AsyncGenerator)\n        chunks = [chunk async for chunk in response]\n\n        # Should have 3 chunks: 2 from audio data + 1 final\n        self.assertEqual(len(chunks), 3)\n\n        # Chunk 1\n        self.assertEqual(\n            chunks[0].content,\n            AudioBlock(\n                type=\"audio\",\n                source=Base64Source(\n                    type=\"base64\",\n                    data=base64.b64encode(chunk1).decode(\"utf-8\"),\n                    media_type=\"audio/pcm\",\n                ),\n            ),\n        )\n\n        # Chunk 2\n        self.assertEqual(\n            chunks[1].content,\n            AudioBlock(\n                type=\"audio\",\n                source=Base64Source(\n                    type=\"base64\",\n                    data=base64.b64encode(chunk2).decode(\"utf-8\"),\n                    media_type=\"audio/pcm\",\n                ),\n            ),\n        )\n\n        # Final chunk\n        self.assertTrue(chunks[2].is_last)\n"
  },
  {
    "path": "tests/tuner_test.py",
    "content": "# -*- coding: utf-8 -*-\n# pylint: disable=unused-argument\n# pylint: disable=too-many-statements\n\"\"\"Unit tests for tuner related modules.\"\"\"\nfrom unittest.async_case import IsolatedAsyncioTestCase\nfrom typing import Dict, Any\n\nfrom logging import Logger\n\nfrom agentscope.tuner import TunerModelConfig, WorkflowOutput, JudgeOutput\nfrom agentscope.tuner._config import (\n    check_judge_function,\n    check_workflow_function,\n)\n\n\nasync def correct_workflow_func(\n    task: Dict,\n    model: TunerModelConfig,\n    auxiliary_models: Dict[str, TunerModelConfig],\n    logger: Logger,\n) -> WorkflowOutput:\n    \"\"\"Correct interface matching the workflow type.\"\"\"\n    return WorkflowOutput(\n        response=\"Test response\",\n    )\n\n\nasync def correct_workflow_func_no_aux(\n    task: Dict,\n    model: TunerModelConfig,\n) -> WorkflowOutput:\n    \"\"\"Correct interface matching the workflow type without\n    auxiliary models.\"\"\"\n    return WorkflowOutput(\n        response=\"Test response\",\n    )\n\n\nasync def incorrect_workflow_func_1(task: Dict) -> WorkflowOutput:\n    \"\"\"Incorrect interface not matching the workflow type.\"\"\"\n    return WorkflowOutput(\n        response=\"Test response\",\n    )\n\n\nasync def incorrect_workflow_func_2(\n    task: Dict,\n    model: TunerModelConfig,\n    aux_model: int,\n) -> WorkflowOutput:\n    \"\"\"Incorrect interface not matching the workflow type.\"\"\"\n    return WorkflowOutput(\n        response=\"Test response\",\n    )\n\n\nasync def correct_judge_func(\n    task: Dict,\n    response: Any,\n    auxiliary_models: Dict[str, TunerModelConfig],\n    logger: Logger,\n) -> JudgeOutput:\n    \"\"\"Correct interface matching the judge type.\"\"\"\n    return JudgeOutput(\n        reward=1.0,\n    )\n\n\nasync def incorrect_judge_func_1(\n    wrong_name: Dict,\n    response: Any,\n) -> JudgeOutput:\n    \"\"\"Incorrect interface not matching the judge type.\"\"\"\n    return JudgeOutput(\n        reward=1.0,\n    )\n\n\nasync def incorrect_judge_func_2(\n    response: Any,\n) -> JudgeOutput:\n    \"\"\"Incorrect interface not matching the judge type.\"\"\"\n    return JudgeOutput(\n        reward=1.0,\n    )\n\n\nclass TestTunerFunctionType(IsolatedAsyncioTestCase):\n    \"\"\"Test cases for tuner function type validation.\"\"\"\n\n    def test_validate_workflow_type(self) -> None:\n        \"\"\"Test workflow type validation.\"\"\"\n        # Correct cases\n        check_workflow_function(correct_workflow_func)\n        check_workflow_function(correct_workflow_func_no_aux)\n\n        # Incorrect cases\n        with self.assertRaises(ValueError):\n            check_workflow_function(incorrect_workflow_func_1)\n        with self.assertRaises(ValueError):\n            check_workflow_function(incorrect_workflow_func_2)\n\n        # Correct cases\n        check_judge_function(correct_judge_func)\n\n        # Incorrect cases\n        with self.assertRaises(ValueError):\n            check_judge_function(incorrect_judge_func_1)\n        with self.assertRaises(ValueError):\n            check_judge_function(incorrect_judge_func_2)\n\n\nclass TestDataset(IsolatedAsyncioTestCase):\n    \"\"\"Test cases for DatasetConfig.\"\"\"\n\n    async def test_preview(self) -> None:\n        \"\"\"Test preview method.\"\"\"\n        try:\n            import datasets\n        except ImportError:\n            datasets = None\n            self.skipTest(\"datasets library is not installed.\")\n        from agentscope.tuner import DatasetConfig\n        from pathlib import Path\n        import tempfile\n\n        assert datasets is not None\n\n        with tempfile.TemporaryDirectory() as tmpdirname:\n            # generate a small dataset directory\n            dataset_dir = Path(tmpdirname) / \"my_dataset\"\n            dataset_dir.mkdir(parents=True, exist_ok=True)\n            sample_file = dataset_dir / \"train.jsonl\"\n            sample_content = [\n                '{\"question\": \"What is 2 + 2?\", \"answer\": \"4\"}',\n                '{\"question\": \"What is 4 + 4?\", \"answer\": \"8\"}',\n                '{\"question\": \"What is 8 + 8?\", \"answer\": \"16\"}',\n            ]\n            with open(sample_file, \"w\", encoding=\"utf-8\") as f:\n                for line in sample_content:\n                    f.write(line + \"\\n\")\n\n            dataset = DatasetConfig(path=str(dataset_dir), split=\"train\")\n            samples = dataset.preview(n=2)\n            self.assertEqual(len(samples), 2)\n            samples = dataset.preview(n=5)\n            self.assertEqual(len(samples), 3)\n            with self.assertRaises(OSError):\n                invalid_ds = DatasetConfig(path=\"/invalid/path\", split=\"train\")\n                invalid_ds.preview()\n            with self.assertRaises(ValueError):\n                invalid_ds = DatasetConfig(\n                    path=str(dataset_dir),\n                    split=\"invalid_split\",\n                )\n                invalid_ds.preview()\n"
  },
  {
    "path": "tests/user_input_test.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The unittests for user input handling.\"\"\"\nfrom typing import Literal\nfrom unittest.async_case import IsolatedAsyncioTestCase\nfrom unittest.mock import patch, MagicMock\n\nfrom pydantic import BaseModel, Field\n\nfrom agentscope.agent import UserAgent\n\n\nclass UserInputTest(IsolatedAsyncioTestCase):\n    \"\"\"The user input test class.\"\"\"\n\n    @patch(\"builtins.input\", side_effect=[\"Hi!\", \"sth\", \"apple\"])\n    async def test_user_terminal_input(self, mock_input: MagicMock) -> None:\n        \"\"\"Test the user input from terminal.\"\"\"\n\n        user_agent = UserAgent(\"Alice\")\n\n        class Choice(BaseModel):\n            \"\"\"The choice model.\"\"\"\n\n            thinking: str = Field(min_length=1, max_length=10)\n            \"\"\"The thinking\"\"\"\n            decision: Literal[\"apple\", \"banana\", \"cherry\"]\n\n        msg_res = await user_agent(structured_model=Choice)\n\n        self.assertEqual(\n            msg_res.content,\n            \"Hi!\",\n        )\n\n        self.assertEqual(\n            msg_res.metadata,\n            {\n                \"thinking\": \"sth\",\n                \"decision\": \"apple\",\n            },\n        )\n\n        self.assertEqual(mock_input.call_count, 3)\n"
  }
]