From 61886170370cad473886428fe726d92980301a04 Mon Sep 17 00:00:00 2001 From: wehub-resource-sync Date: Mon, 13 Jul 2026 12:22:20 +0800 Subject: [PATCH] chore: import upstream snapshot with attribution --- .gitattributes | 30 + .github/ISSUE_TEMPLATE/config.yml | 5 + .../ISSUE_TEMPLATE/request_new_features.yaml | 21 + .github/ISSUE_TEMPLATE/show_me_the_bug.yaml | 44 + .github/PULL_REQUEST_TEMPLATE.md | 17 + .github/dependabot.yml | 58 + .github/workflows/build-package.yaml | 33 + .../workflows/environment-corrupt-check.yaml | 33 + .github/workflows/pr-autodiff.yaml | 138 + .github/workflows/pre-commit.yaml | 26 + .github/workflows/stale.yaml | 23 + .github/workflows/top-issues.yaml | 29 + .gitignore | 202 + .pre-commit-config.yaml | 37 + .vscode/extensions.json | 8 + .vscode/settings.json | 20 + CODE_OF_CONDUCT.md | 162 + Dockerfile | 13 + LICENSE | 21 + README.md | 195 + README.wehub.md | 7 + README_ja.md | 193 + README_ko.md | 192 + README_zh.md | 198 + app/__init__.py | 10 + app/agent/__init__.py | 16 + app/agent/base.py | 196 + app/agent/browser.py | 129 + app/agent/data_analysis.py | 37 + app/agent/manus.py | 165 + app/agent/mcp.py | 185 + app/agent/react.py | 38 + app/agent/sandbox_agent.py | 223 + app/agent/swe.py | 24 + app/agent/toolcall.py | 250 + app/bedrock.py | 334 + app/config.py | 372 + app/daytona/README.md | 57 + app/daytona/sandbox.py | 165 + app/daytona/tool_base.py | 138 + app/exceptions.py | 13 + app/flow/__init__.py | 0 app/flow/base.py | 57 + app/flow/flow_factory.py | 30 + app/flow/planning.py | 442 + app/llm.py | 766 ++ app/logger.py | 42 + app/mcp/__init__.py | 0 app/mcp/server.py | 180 + app/prompt/__init__.py | 0 app/prompt/browser.py | 94 + app/prompt/manus.py | 10 + app/prompt/mcp.py | 43 + app/prompt/planning.py | 27 + app/prompt/swe.py | 22 + app/prompt/toolcall.py | 5 + app/prompt/visualization.py | 10 + app/sandbox/__init__.py | 30 + app/sandbox/client.py | 201 + app/sandbox/core/exceptions.py | 17 + app/sandbox/core/manager.py | 313 + app/sandbox/core/sandbox.py | 462 + app/sandbox/core/terminal.py | 346 + app/schema.py | 187 + app/tool/__init__.py | 24 + app/tool/ask_human.py | 21 + app/tool/base.py | 181 + app/tool/bash.py | 158 + app/tool/browser_use_tool.py | 567 ++ app/tool/chart_visualization/README.md | 146 + app/tool/chart_visualization/README_ja.md | 114 + app/tool/chart_visualization/README_ko.md | 128 + app/tool/chart_visualization/README_zh.md | 147 + app/tool/chart_visualization/__init__.py | 6 + app/tool/chart_visualization/chart_prepare.py | 38 + .../chart_visualization/data_visualization.py | 263 + .../chart_visualization/package-lock.json | 8739 +++++++++++++++++ app/tool/chart_visualization/package.json | 22 + .../chart_visualization/python_execute.py | 36 + .../chart_visualization/src/chartVisualize.ts | 372 + .../chart_visualization/test/chart_demo.py | 191 + .../chart_visualization/test/report_demo.py | 27 + app/tool/chart_visualization/tsconfig.json | 109 + app/tool/computer_use_tool.py | 487 + app/tool/crawl4ai.py | 269 + app/tool/create_chat_completion.py | 169 + app/tool/file_operators.py | 158 + app/tool/mcp.py | 194 + app/tool/planning.py | 363 + app/tool/python_execute.py | 75 + app/tool/sandbox/sb_browser_tool.py | 450 + app/tool/sandbox/sb_files_tool.py | 361 + app/tool/sandbox/sb_shell_tool.py | 419 + app/tool/sandbox/sb_vision_tool.py | 178 + app/tool/search/__init__.py | 14 + app/tool/search/baidu_search.py | 54 + app/tool/search/base.py | 40 + app/tool/search/bing_search.py | 144 + app/tool/search/duckduckgo_search.py | 57 + app/tool/search/google_search.py | 33 + app/tool/str_replace_editor.py | 432 + app/tool/terminate.py | 25 + app/tool/tool_collection.py | 71 + app/tool/web_search.py | 418 + app/utils/__init__.py | 1 + app/utils/files_utils.py | 87 + app/utils/logger.py | 32 + assets/community_group.png | Bin 0 -> 177676 bytes assets/logo.jpg | Bin 0 -> 65677 bytes config/.gitignore | 2 + config/config.example-daytona.toml | 114 + config/config.example-model-anthropic.toml | 16 + config/config.example-model-azure.toml | 18 + config/config.example-model-google.toml | 16 + config/config.example-model-jiekouai.toml | 17 + config/config.example-model-ollama.toml | 17 + config/config.example-model-ppio.toml | 17 + config/config.example.toml | 113 + config/mcp.example.json | 8 + examples/benchmarks/__init__.py | 3 + .../japan_travel_guide_instructions.txt | 62 + .../japan_travel_handbook.html | 124 + .../japan_travel_handbook_mobile.html | 255 + .../japan_travel_handbook_print.html | 162 + .../use_case/pictures/japan-travel-plan-1.png | Bin 0 -> 168010 bytes .../use_case/pictures/japan-travel-plan-2.png | Bin 0 -> 36336 bytes examples/use_case/readme.md | 16 + main.py | 36 + protocol/a2a/__init__.py | 0 protocol/a2a/app/README.md | 194 + protocol/a2a/app/README_zh.md | 194 + protocol/a2a/app/__init__.py | 0 protocol/a2a/app/agent.py | 32 + protocol/a2a/app/agent_executor.py | 72 + protocol/a2a/app/main.py | 131 + requirements.txt | 42 + run_flow.py | 52 + run_mcp.py | 116 + run_mcp_server.py | 11 + sandbox_main.py | 36 + setup.py | 49 + tests/sandbox/test_client.py | 110 + tests/sandbox/test_docker_terminal.py | 104 + tests/sandbox/test_sandbox.py | 152 + tests/sandbox/test_sandbox_manager.py | 138 + 145 files changed, 25590 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/request_new_features.yaml create mode 100644 .github/ISSUE_TEMPLATE/show_me_the_bug.yaml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/build-package.yaml create mode 100644 .github/workflows/environment-corrupt-check.yaml create mode 100644 .github/workflows/pr-autodiff.yaml create mode 100644 .github/workflows/pre-commit.yaml create mode 100644 .github/workflows/stale.yaml create mode 100644 .github/workflows/top-issues.yaml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 CODE_OF_CONDUCT.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 README.wehub.md create mode 100644 README_ja.md create mode 100644 README_ko.md create mode 100644 README_zh.md create mode 100644 app/__init__.py create mode 100644 app/agent/__init__.py create mode 100644 app/agent/base.py create mode 100644 app/agent/browser.py create mode 100644 app/agent/data_analysis.py create mode 100644 app/agent/manus.py create mode 100644 app/agent/mcp.py create mode 100644 app/agent/react.py create mode 100644 app/agent/sandbox_agent.py create mode 100644 app/agent/swe.py create mode 100644 app/agent/toolcall.py create mode 100644 app/bedrock.py create mode 100644 app/config.py create mode 100644 app/daytona/README.md create mode 100644 app/daytona/sandbox.py create mode 100644 app/daytona/tool_base.py create mode 100644 app/exceptions.py create mode 100644 app/flow/__init__.py create mode 100644 app/flow/base.py create mode 100644 app/flow/flow_factory.py create mode 100644 app/flow/planning.py create mode 100644 app/llm.py create mode 100644 app/logger.py create mode 100644 app/mcp/__init__.py create mode 100644 app/mcp/server.py create mode 100644 app/prompt/__init__.py create mode 100644 app/prompt/browser.py create mode 100644 app/prompt/manus.py create mode 100644 app/prompt/mcp.py create mode 100644 app/prompt/planning.py create mode 100644 app/prompt/swe.py create mode 100644 app/prompt/toolcall.py create mode 100644 app/prompt/visualization.py create mode 100644 app/sandbox/__init__.py create mode 100644 app/sandbox/client.py create mode 100644 app/sandbox/core/exceptions.py create mode 100644 app/sandbox/core/manager.py create mode 100644 app/sandbox/core/sandbox.py create mode 100644 app/sandbox/core/terminal.py create mode 100644 app/schema.py create mode 100644 app/tool/__init__.py create mode 100644 app/tool/ask_human.py create mode 100644 app/tool/base.py create mode 100644 app/tool/bash.py create mode 100644 app/tool/browser_use_tool.py create mode 100644 app/tool/chart_visualization/README.md create mode 100644 app/tool/chart_visualization/README_ja.md create mode 100644 app/tool/chart_visualization/README_ko.md create mode 100644 app/tool/chart_visualization/README_zh.md create mode 100644 app/tool/chart_visualization/__init__.py create mode 100644 app/tool/chart_visualization/chart_prepare.py create mode 100644 app/tool/chart_visualization/data_visualization.py create mode 100644 app/tool/chart_visualization/package-lock.json create mode 100644 app/tool/chart_visualization/package.json create mode 100644 app/tool/chart_visualization/python_execute.py create mode 100644 app/tool/chart_visualization/src/chartVisualize.ts create mode 100644 app/tool/chart_visualization/test/chart_demo.py create mode 100644 app/tool/chart_visualization/test/report_demo.py create mode 100644 app/tool/chart_visualization/tsconfig.json create mode 100644 app/tool/computer_use_tool.py create mode 100644 app/tool/crawl4ai.py create mode 100644 app/tool/create_chat_completion.py create mode 100644 app/tool/file_operators.py create mode 100644 app/tool/mcp.py create mode 100644 app/tool/planning.py create mode 100644 app/tool/python_execute.py create mode 100644 app/tool/sandbox/sb_browser_tool.py create mode 100644 app/tool/sandbox/sb_files_tool.py create mode 100644 app/tool/sandbox/sb_shell_tool.py create mode 100644 app/tool/sandbox/sb_vision_tool.py create mode 100644 app/tool/search/__init__.py create mode 100644 app/tool/search/baidu_search.py create mode 100644 app/tool/search/base.py create mode 100644 app/tool/search/bing_search.py create mode 100644 app/tool/search/duckduckgo_search.py create mode 100644 app/tool/search/google_search.py create mode 100644 app/tool/str_replace_editor.py create mode 100644 app/tool/terminate.py create mode 100644 app/tool/tool_collection.py create mode 100644 app/tool/web_search.py create mode 100644 app/utils/__init__.py create mode 100644 app/utils/files_utils.py create mode 100644 app/utils/logger.py create mode 100644 assets/community_group.png create mode 100644 assets/logo.jpg create mode 100644 config/.gitignore create mode 100644 config/config.example-daytona.toml create mode 100644 config/config.example-model-anthropic.toml create mode 100644 config/config.example-model-azure.toml create mode 100644 config/config.example-model-google.toml create mode 100644 config/config.example-model-jiekouai.toml create mode 100644 config/config.example-model-ollama.toml create mode 100644 config/config.example-model-ppio.toml create mode 100644 config/config.example.toml create mode 100644 config/mcp.example.json create mode 100644 examples/benchmarks/__init__.py create mode 100644 examples/use_case/japan-travel-plan/japan_travel_guide_instructions.txt create mode 100644 examples/use_case/japan-travel-plan/japan_travel_handbook.html create mode 100644 examples/use_case/japan-travel-plan/japan_travel_handbook_mobile.html create mode 100644 examples/use_case/japan-travel-plan/japan_travel_handbook_print.html create mode 100644 examples/use_case/pictures/japan-travel-plan-1.png create mode 100644 examples/use_case/pictures/japan-travel-plan-2.png create mode 100644 examples/use_case/readme.md create mode 100644 main.py create mode 100644 protocol/a2a/__init__.py create mode 100644 protocol/a2a/app/README.md create mode 100644 protocol/a2a/app/README_zh.md create mode 100644 protocol/a2a/app/__init__.py create mode 100644 protocol/a2a/app/agent.py create mode 100644 protocol/a2a/app/agent_executor.py create mode 100644 protocol/a2a/app/main.py create mode 100644 requirements.txt create mode 100644 run_flow.py create mode 100644 run_mcp.py create mode 100644 run_mcp_server.py create mode 100644 sandbox_main.py create mode 100644 setup.py create mode 100644 tests/sandbox/test_client.py create mode 100644 tests/sandbox/test_docker_terminal.py create mode 100644 tests/sandbox/test_sandbox.py create mode 100644 tests/sandbox/test_sandbox_manager.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..462e473 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,30 @@ +# HTML code is incorrectly calculated into statistics, so ignore them +*.html linguist-detectable=false +# Auto detect text files and perform LF normalization +* text=auto eol=lf +# Ensure shell scripts use LF (Linux style) line endings on Windows +*.sh text eol=lf +# Treat specific binary files as binary and prevent line ending conversion +*.png binary +*.jpg binary +*.gif binary +*.ico binary +*.jpeg binary +*.mp3 binary +*.zip binary +*.bin binary +# Preserve original line endings for specific document files +*.doc text eol=crlf +*.docx text eol=crlf +*.pdf binary +# Ensure source code and script files use LF line endings +*.py text eol=lf +*.js text eol=lf +*.html text eol=lf +*.css text eol=lf +# Specify custom diff driver for specific file types +*.md diff=markdown +*.json diff=json +*.mp4 filter=lfs diff=lfs merge=lfs -text +*.mov filter=lfs diff=lfs merge=lfs -text +*.webm filter=lfs diff=lfs merge=lfs -text diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..69766c1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: "Join the Community Group" + about: Join the OpenManus community to discuss and get help from others + url: https://github.com/FoundationAgents/OpenManus?tab=readme-ov-file#community-group diff --git a/.github/ISSUE_TEMPLATE/request_new_features.yaml b/.github/ISSUE_TEMPLATE/request_new_features.yaml new file mode 100644 index 0000000..749ab7f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/request_new_features.yaml @@ -0,0 +1,21 @@ +name: "🤔 Request new features" +description: Suggest ideas or features you’d like to see implemented in OpenManus. +labels: enhancement +body: + - type: textarea + id: feature-description + attributes: + label: Feature description + description: | + Provide a clear and concise description of the proposed feature + validations: + required: true + - type: textarea + id: your-feature + attributes: + label: Your Feature + description: | + Explain your idea or implementation process, if any. Optionally, include a Pull Request URL. + Ensure accompanying docs/tests/examples are provided for review. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/show_me_the_bug.yaml b/.github/ISSUE_TEMPLATE/show_me_the_bug.yaml new file mode 100644 index 0000000..de9298e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/show_me_the_bug.yaml @@ -0,0 +1,44 @@ +name: "🪲 Show me the Bug" +description: Report a bug encountered while using OpenManus and seek assistance. +labels: bug +body: + - type: textarea + id: bug-description + attributes: + label: Bug Description + description: | + Clearly describe the bug you encountered + validations: + required: true + - type: textarea + id: solve-method + attributes: + label: Bug solved method + description: | + If resolved, explain the solution. Optionally, include a Pull Request URL. + If unresolved, provide additional details to aid investigation + validations: + required: true + - type: textarea + id: environment-information + attributes: + label: Environment information + description: | + System: e.g., Ubuntu 22.04 + Python: e.g., 3.12 + OpenManus version: e.g., 0.1.0 + value: | + - System version: + - Python version: + - OpenManus version or branch: + - Installation method (e.g., `pip install -r requirements.txt` or `pip install -e .`): + validations: + required: true + - type: textarea + id: extra-information + attributes: + label: Extra information + description: | + For example, attach screenshots or logs to help diagnose the issue + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..1859f27 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,17 @@ +**Features** + + +- Feature 1 +- Feature 2 + +**Feature Docs** + + +**Influence** + + +**Result** + + +**Other** + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1ef0e94 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,58 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 4 + groups: + # Group critical packages that might need careful review + core-dependencies: + patterns: + - "pydantic*" + - "openai" + - "fastapi" + - "tiktoken" + browsergym-related: + patterns: + - "browsergym*" + - "browser-use" + - "playwright" + search-tools: + patterns: + - "googlesearch-python" + - "baidusearch" + - "duckduckgo_search" + pre-commit: + patterns: + - "pre-commit" + security-all: + applies-to: "security-updates" + patterns: + - "*" + version-all: + applies-to: "version-updates" + patterns: + - "*" + exclude-patterns: + - "pydantic*" + - "openai" + - "fastapi" + - "tiktoken" + - "browsergym*" + - "browser-use" + - "playwright" + - "googlesearch-python" + - "baidusearch" + - "duckduckgo_search" + - "pre-commit" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 4 + groups: + actions: + patterns: + - "*" diff --git a/.github/workflows/build-package.yaml b/.github/workflows/build-package.yaml new file mode 100644 index 0000000..754c300 --- /dev/null +++ b/.github/workflows/build-package.yaml @@ -0,0 +1,33 @@ +name: Build and upload Python package + +on: + workflow_dispatch: + release: + types: [created, published] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install setuptools wheel twine + - name: Set package version + run: | + export VERSION="${GITHUB_REF#refs/tags/v}" + sed -i "s/version=.*/version=\"${VERSION}\",/" setup.py + - name: Build and publish + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: | + python setup.py bdist_wheel sdist + twine upload dist/* diff --git a/.github/workflows/environment-corrupt-check.yaml b/.github/workflows/environment-corrupt-check.yaml new file mode 100644 index 0000000..dc66fe0 --- /dev/null +++ b/.github/workflows/environment-corrupt-check.yaml @@ -0,0 +1,33 @@ +name: Environment Corruption Check +on: + push: + branches: ["main"] + paths: + - requirements.txt + pull_request: + branches: ["main"] + paths: + - requirements.txt +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true +jobs: + test-python-versions: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11.11", "3.12.8", "3.13.2"] + fail-fast: false + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Upgrade pip + run: | + python -m pip install --upgrade pip + - name: Install dependencies + run: | + pip install -r requirements.txt diff --git a/.github/workflows/pr-autodiff.yaml b/.github/workflows/pr-autodiff.yaml new file mode 100644 index 0000000..46c95c6 --- /dev/null +++ b/.github/workflows/pr-autodiff.yaml @@ -0,0 +1,138 @@ +name: PR Diff Summarization +on: + # pull_request: + # branches: [main] + # types: [opened, ready_for_review, reopened] + issue_comment: + types: [created] +permissions: + contents: read + pull-requests: write +jobs: + pr-diff-summarization: + runs-on: ubuntu-latest + if: | + (github.event_name == 'pull_request') || + (github.event_name == 'issue_comment' && + contains(github.event.comment.body, '!pr-diff') && + (github.event.comment.author_association == 'CONTRIBUTOR' || github.event.comment.author_association == 'COLLABORATOR' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && + github.event.issue.pull_request) + steps: + - name: Get PR head SHA + id: get-pr-sha + run: | + PR_URL="${{ github.event.issue.pull_request.url || github.event.pull_request.url }}" + # https://api.github.com/repos/OpenManus/pulls/1 + RESPONSE=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" $PR_URL) + SHA=$(echo $RESPONSE | jq -r '.head.sha') + TARGET_BRANCH=$(echo $RESPONSE | jq -r '.base.ref') + echo "pr_sha=$SHA" >> $GITHUB_OUTPUT + echo "target_branch=$TARGET_BRANCH" >> $GITHUB_OUTPUT + echo "Retrieved PR head SHA from API: $SHA, target branch: $TARGET_BRANCH" + - name: Check out code + uses: actions/checkout@v4 + with: + ref: ${{ steps.get-pr-sha.outputs.pr_sha }} + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install openai requests + - name: Create and run Python script + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }} + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} + TARGET_BRANCH: ${{ steps.get-pr-sha.outputs.target_branch }} + run: |- + cat << 'EOF' > /tmp/_workflow_core.py + import os + import subprocess + import json + import requests + from openai import OpenAI + + def get_diff(): + result = subprocess.run( + ['git', 'diff', 'origin/' + os.getenv('TARGET_BRANCH') + '...HEAD'], + capture_output=True, text=True, check=True) + return '\n'.join( + line for line in result.stdout.split('\n') + if any(line.startswith(c) for c in ('+', '-')) + and not line.startswith(('---', '+++')) + )[:round(200000 * 0.4)] # Truncate to prevent overflow + + def generate_comment(diff_content): + client = OpenAI( + base_url=os.getenv("OPENAI_BASE_URL"), + api_key=os.getenv("OPENAI_API_KEY") + ) + + guidelines = ''' + 1. English version first, Chinese Simplified version after + 2. Example format: + # Diff Report + ## English + - Added `ABC` class + - Fixed `f()` behavior in `foo` module + + ### Comments Highlight + - `config.toml` needs to be configured properly to make sure new features work as expected. + + ### Spelling/Offensive Content Check + - No spelling mistakes or offensive content found in the code or comments. + + ## 中文(简体) + - 新增了 `ABC` 类 + - `foo` 模块中的 `f()` 行为已修复 + + ### 评论高亮 + - `config.toml` 需要正确配置才能确保新功能正常运行。 + + ### 内容检查 + - 没有发现代码或注释中的拼写错误或不当措辞。 + + 3. Highlight non-English comments + 4. Check for spelling/offensive content''' + + response = client.chat.completions.create( + model="o3-mini", + messages=[{ + "role": "system", + "content": "Generate bilingual code review feedback." + }, { + "role": "user", + "content": f"Review these changes per guidelines:\n{guidelines}\n\nDIFF:\n{diff_content}" + }] + ) + return response.choices[0].message.content + + def post_comment(comment): + repo = os.getenv("GITHUB_REPOSITORY") + pr_number = os.getenv("PR_NUMBER") + + headers = { + "Authorization": f"Bearer {os.getenv('GH_TOKEN')}", + "Accept": "application/vnd.github.v3+json" + } + url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments" + + requests.post(url, json={"body": comment}, headers=headers) + + if __name__ == "__main__": + diff_content = get_diff() + if not diff_content.strip(): + print("No meaningful diff detected.") + exit(0) + + comment = generate_comment(diff_content) + post_comment(comment) + print("Comment posted successfully.") + EOF + + python /tmp/_workflow_core.py diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml new file mode 100644 index 0000000..c9b3b22 --- /dev/null +++ b/.github/workflows/pre-commit.yaml @@ -0,0 +1,26 @@ +name: Pre-commit checks + +on: + pull_request: + branches: + - '**' + push: + branches: + - '**' + +jobs: + pre-commit-check: + runs-on: ubuntu-latest + steps: + - name: Checkout Source Code + uses: actions/checkout@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install pre-commit and tools + run: | + python -m pip install --upgrade pip + pip install pre-commit black==23.1.0 isort==5.12.0 autoflake==2.0.1 + - name: Run pre-commit hooks + run: pre-commit run --all-files diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml new file mode 100644 index 0000000..ea52562 --- /dev/null +++ b/.github/workflows/stale.yaml @@ -0,0 +1,23 @@ +name: Close inactive issues + +on: + schedule: + - cron: "5 0 * * *" + +jobs: + close-issues: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v9 + with: + days-before-issue-stale: 30 + days-before-issue-close: 14 + stale-issue-label: "inactive" + stale-issue-message: "This issue has been inactive for 30 days. Please comment if you have updates." + close-issue-message: "This issue was closed due to 45 days of inactivity. Reopen if still relevant." + days-before-pr-stale: -1 + days-before-pr-close: -1 + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/top-issues.yaml b/.github/workflows/top-issues.yaml new file mode 100644 index 0000000..6efcbb1 --- /dev/null +++ b/.github/workflows/top-issues.yaml @@ -0,0 +1,29 @@ +name: Top issues +on: + schedule: + - cron: '0 0/2 * * *' + workflow_dispatch: +jobs: + ShowAndLabelTopIssues: + permissions: + issues: write + pull-requests: write + actions: read + contents: read + name: Display and label top issues + runs-on: ubuntu-latest + if: github.repository == 'FoundationAgents/OpenManus' + steps: + - name: Run top issues action + uses: rickstaa/top-issues-action@7e8dda5d5ae3087670f9094b9724a9a091fc3ba1 # v1.3.101 + env: + github_token: ${{ secrets.GITHUB_TOKEN }} + with: + label: true + dashboard: true + dashboard_show_total_reactions: true + top_issues: true + top_features: true + top_bugs: true + top_pull_requests: true + top_list_size: 14 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..41dbbf2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,202 @@ +### Project-specific ### +# Logs +logs/ + +# Data +data/ + +# Workspace +workspace/ + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# PyPI configuration file +.pypirc + +### Visual Studio Code ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +# OSX +.DS_Store + +# node +node_modules diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..84be372 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,37 @@ +repos: + - repo: https://github.com/psf/black + rev: 23.1.0 + hooks: + - id: black + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + + - repo: https://github.com/PyCQA/autoflake + rev: v2.0.1 + hooks: + - id: autoflake + args: + [ + --remove-all-unused-imports, + --ignore-init-module-imports, + --expand-star-imports, + --remove-duplicate-keys, + --remove-unused-variables, + --recursive, + --in-place, + --exclude=__init__.py, + ] + files: \.py$ + + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + args: + ["--profile", "black", "--filter-files", "--lines-after-imports=2"] diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..f2c6cd0 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "tamasfe.even-better-toml", + "ms-python.black-formatter", + "ms-python.isort" + ], + "unwantedRecommendations": [] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d3aa302 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.codeActionsOnSave": { + "source.organizeImports": "always" + } + }, + "[toml]": { + "editor.defaultFormatter": "tamasfe.even-better-toml", + }, + "pre-commit-helper.runOnSave": "none", + "pre-commit-helper.config": ".pre-commit-config.yaml", + "evenBetterToml.schema.enabled": true, + "evenBetterToml.schema.associations": { + "^.+config[/\\\\].+\\.toml$": "../config/schema.config.json" + }, + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true, + "editor.formatOnSave": true +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..856ecb2 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,162 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people. +* Being respectful of differing opinions, viewpoints, and experiences. +* Giving and gracefully accepting constructive feedback. +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience. +* Focusing on what is best not just for us as individuals, but for the overall + community. + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind. +* Trolling, insulting or derogatory comments, and personal or political attacks. +* Public or private harassment. +* Publishing others' private information, such as a physical or email address, + without their explicit permission. +* Other conduct which could reasonably be considered inappropriate in a + professional setting. + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +mannaandpoem@gmail.com +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +### Slack and Discord Etiquettes + +These Slack and Discord etiquette guidelines are designed to foster an inclusive, respectful, and productive environment +for all community members. By following these best practices, we ensure effective communication and collaboration while +minimizing disruptions. Let’s work together to build a supportive and welcoming community! + +- Communicate respectfully and professionally, avoiding sarcasm or harsh language, and remember that tone can be + difficult to interpret in text. +- Use threads for specific discussions to keep channels organized and easier to follow. +- Tag others only when their input is critical or urgent, and use @here, @channel or @everyone sparingly to minimize + disruptions. +- Be patient, as open-source contributors and maintainers often have other commitments and may need time to respond. +- Post questions or discussions in the most relevant + channel ([discord - #general](https://discord.com/channels/1125308739348594758/1138430348557025341)). +- When asking for help or raising issues, include necessary details like links, screenshots, or clear explanations to + provide context. +- Keep discussions in public channels whenever possible to allow others to benefit from the conversation, unless the + matter is sensitive or private. +- Always adhere to [our standards](https://github.com/FoundationAgents/OpenManus/blob/main/CODE_OF_CONDUCT.md#our-standards) + to ensure a welcoming and collaborative environment. +- If you choose to mute a channel, consider setting up alerts for topics that still interest you to stay engaged. For + Slack, Go to Settings → Notifications → My Keywords to add specific keywords that will notify you when mentioned. For + example, if you're here for discussions about LLMs, mute the channel if it’s too busy, but set notifications to alert + you only when “LLMs” appears in messages. Also for Discord, go to the channel notifications and choose the option that + best describes your need. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org + +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html + +[Mozilla CoC]: https://github.com/mozilla/diversity + +[FAQ]: https://www.contributor-covenant.org/faq + +[translations]: https://www.contributor-covenant.org/translations diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9f7a190 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.12-slim + +WORKDIR /app/OpenManus + +RUN apt-get update && apt-get install -y --no-install-recommends git curl \ + && rm -rf /var/lib/apt/lists/* \ + && (command -v uv >/dev/null 2>&1 || pip install --no-cache-dir uv) + +COPY . . + +RUN uv pip install --system -r requirements.txt + +CMD ["bash"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..db2216e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 manna_and_poem + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d4a8437 --- /dev/null +++ b/README.md @@ -0,0 +1,195 @@ +

+ +

+ +English | [中文](README_zh.md) | [한국어](README_ko.md) | [日本語](README_ja.md) + +[![GitHub stars](https://img.shields.io/github/stars/FoundationAgents/OpenManus?style=social)](https://github.com/FoundationAgents/OpenManus/stargazers) +  +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)   +[![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z) +[![Demo](https://img.shields.io/badge/Demo-Hugging%20Face-yellow)](https://huggingface.co/spaces/lyh-917/OpenManusDemo) +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15186407.svg)](https://doi.org/10.5281/zenodo.15186407) + +# 👋 OpenManus + +Manus is incredible, but OpenManus can achieve any idea without an *Invite Code* 🛫! + +Our team members [@Xinbin Liang](https://github.com/mannaandpoem) and [@Jinyu Xiang](https://github.com/XiangJinyu) (core authors), along with [@Zhaoyang Yu](https://github.com/MoshiQAQ), [@Jiayi Zhang](https://github.com/didiforgithub), and [@Sirui Hong](https://github.com/stellaHSR), we are from [@MetaGPT](https://github.com/geekan/MetaGPT). The prototype is launched within 3 hours and we are keeping building! + +It's a simple implementation, so we welcome any suggestions, contributions, and feedback! + +Enjoy your own agent with OpenManus! + +We're also excited to introduce [OpenManus-RL](https://github.com/OpenManus/OpenManus-RL), an open-source project dedicated to reinforcement learning (RL)- based (such as GRPO) tuning methods for LLM agents, developed collaboratively by researchers from UIUC and OpenManus. + +## Project Demo + + + +## Installation + +We provide two installation methods. Method 2 (using uv) is recommended for faster installation and better dependency management. + +### Method 1: Using conda + +1. Create a new conda environment: + +```bash +conda create -n open_manus python=3.12 +conda activate open_manus +``` + +2. Clone the repository: + +```bash +git clone https://github.com/FoundationAgents/OpenManus.git +cd OpenManus +``` + +3. Install dependencies: + +```bash +pip install -r requirements.txt +``` + +### Method 2: Using uv (Recommended) + +1. Install uv (A fast Python package installer and resolver): + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +2. Clone the repository: + +```bash +git clone https://github.com/FoundationAgents/OpenManus.git +cd OpenManus +``` + +3. Create a new virtual environment and activate it: + +```bash +uv venv --python 3.12 +source .venv/bin/activate # On Unix/macOS +# Or on Windows: +# .venv\Scripts\activate +``` + +4. Install dependencies: + +```bash +uv pip install -r requirements.txt +``` + +### Browser Automation Tool (Optional) +```bash +playwright install +``` + +## Configuration + +OpenManus requires configuration for the LLM APIs it uses. Follow these steps to set up your configuration: + +1. Create a `config.toml` file in the `config` directory (you can copy from the example): + +```bash +cp config/config.example.toml config/config.toml +``` + +2. Edit `config/config.toml` to add your API keys and customize settings: + +```toml +# Global LLM configuration +[llm] +model = "gpt-4o" +base_url = "https://api.openai.com/v1" +api_key = "sk-..." # Replace with your actual API key +max_tokens = 4096 +temperature = 0.0 + +# Optional configuration for specific LLM models +[llm.vision] +model = "gpt-4o" +base_url = "https://api.openai.com/v1" +api_key = "sk-..." # Replace with your actual API key +``` + +## Quick Start + +One line for run OpenManus: + +```bash +python main.py +``` + +Then input your idea via terminal! + +For MCP tool version, you can run: +```bash +python run_mcp.py +``` + +For unstable multi-agent version, you also can run: + +```bash +python run_flow.py +``` + +### Custom Adding Multiple Agents + +Currently, besides the general OpenManus Agent, we have also integrated the DataAnalysis Agent, which is suitable for data analysis and data visualization tasks. You can add this agent to `run_flow` in `config.toml`. + +```toml +# Optional configuration for run-flow +[runflow] +use_data_analysis_agent = true # Disabled by default, change to true to activate +``` +In addition, you need to install the relevant dependencies to ensure the agent runs properly: [Detailed Installation Guide](app/tool/chart_visualization/README.md##Installation) + +## How to contribute + +We welcome any friendly suggestions and helpful contributions! Just create issues or submit pull requests. + +Or contact @mannaandpoem via 📧email: mannaandpoem@gmail.com + +**Note**: Before submitting a pull request, please use the pre-commit tool to check your changes. Run `pre-commit run --all-files` to execute the checks. + +## Community Group +Join our networking group on Feishu and share your experience with other developers! + +
+ OpenManus 交流群 +
+ +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=FoundationAgents/OpenManus&type=Date)](https://star-history.com/#FoundationAgents/OpenManus&Date) + +## Sponsors +Thanks to [PPIO](https://ppinfra.com/user/register?invited_by=OCPKCN&utm_source=github_openmanus&utm_medium=github_readme&utm_campaign=link) for computing source support. +> PPIO: The most affordable and easily-integrated MaaS and GPU cloud solution. + + +## Acknowledgement + +Thanks to [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo), [browser-use](https://github.com/browser-use/browser-use) and [crawl4ai](https://github.com/unclecode/crawl4ai) for providing basic support for this project! + +Additionally, we are grateful to [AAAJ](https://github.com/metauto-ai/agent-as-a-judge), [MetaGPT](https://github.com/geekan/MetaGPT), [OpenHands](https://github.com/All-Hands-AI/OpenHands) and [SWE-agent](https://github.com/SWE-agent/SWE-agent). + +We also thank stepfun(阶跃星辰) for supporting our Hugging Face demo space. + +OpenManus is built by contributors from MetaGPT. Huge thanks to this agent community! + +## Cite +```bibtex +@misc{openmanus2025, + author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong and Sheng Fan and Xiao Tang and Bang Liu and Yuyu Luo and Chenglin Wu}, + title = {OpenManus: An open-source framework for building general AI agents}, + year = {2025}, + publisher = {Zenodo}, + doi = {10.5281/zenodo.15186407}, + url = {https://doi.org/10.5281/zenodo.15186407}, +} +``` diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..8ed5afc --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`FoundationAgents/OpenManus` +- 原始仓库:https://github.com/FoundationAgents/OpenManus +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/README_ja.md b/README_ja.md new file mode 100644 index 0000000..d5fd281 --- /dev/null +++ b/README_ja.md @@ -0,0 +1,193 @@ +

+ +

+ +[English](README.md) | [中文](README_zh.md) | [한국어](README_ko.md) | 日本語 + +[![GitHub stars](https://img.shields.io/github/stars/FoundationAgents/OpenManus?style=social)](https://github.com/FoundationAgents/OpenManus/stargazers) +  +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)   +[![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z) +[![Demo](https://img.shields.io/badge/Demo-Hugging%20Face-yellow)](https://huggingface.co/spaces/lyh-917/OpenManusDemo) +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15186407.svg)](https://doi.org/10.5281/zenodo.15186407) + +# 👋 OpenManus + +Manusは素晴らしいですが、OpenManusは*招待コード*なしでどんなアイデアも実現できます!🛫 + +私たちのチームメンバー [@Xinbin Liang](https://github.com/mannaandpoem) と [@Jinyu Xiang](https://github.com/XiangJinyu)(主要開発者)、そして [@Zhaoyang Yu](https://github.com/MoshiQAQ)、[@Jiayi Zhang](https://github.com/didiforgithub)、[@Sirui Hong](https://github.com/stellaHSR) は [@MetaGPT](https://github.com/geekan/MetaGPT) から来ました。プロトタイプは3時間以内に立ち上げられ、継続的に開発を進めています! + +これはシンプルな実装ですので、どんな提案、貢献、フィードバックも歓迎します! + +OpenManusで自分だけのエージェントを楽しみましょう! + +また、UIUCとOpenManusの研究者が共同開発した[OpenManus-RL](https://github.com/OpenManus/OpenManus-RL)をご紹介できることを嬉しく思います。これは強化学習(RL)ベース(GRPOなど)のLLMエージェントチューニング手法に特化したオープンソースプロジェクトです。 + +## プロジェクトデモ + + + +## インストール方法 + +インストール方法は2つ提供しています。方法2(uvを使用)は、より高速なインストールと優れた依存関係管理のため推奨されています。 + +### 方法1:condaを使用 + +1. 新しいconda環境を作成します: + +```bash +conda create -n open_manus python=3.12 +conda activate open_manus +``` + +2. リポジトリをクローンします: + +```bash +git clone https://github.com/FoundationAgents/OpenManus.git +cd OpenManus +``` + +3. 依存関係をインストールします: + +```bash +pip install -r requirements.txt +``` + +### 方法2:uvを使用(推奨) + +1. uv(高速なPythonパッケージインストーラーと管理機能)をインストールします: + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +2. リポジトリをクローンします: + +```bash +git clone https://github.com/FoundationAgents/OpenManus.git +cd OpenManus +``` + +3. 新しい仮想環境を作成してアクティベートします: + +```bash +uv venv --python 3.12 +source .venv/bin/activate # Unix/macOSの場合 +# Windowsの場合: +# .venv\Scripts\activate +``` + +4. 依存関係をインストールします: + +```bash +uv pip install -r requirements.txt +``` + +### ブラウザ自動化ツール(オプション) +```bash +playwright install +``` + +## 設定 + +OpenManusを使用するには、LLM APIの設定が必要です。以下の手順に従って設定してください: + +1. `config`ディレクトリに`config.toml`ファイルを作成します(サンプルからコピーできます): + +```bash +cp config/config.example.toml config/config.toml +``` + +2. `config/config.toml`を編集してAPIキーを追加し、設定をカスタマイズします: + +```toml +# グローバルLLM設定 +[llm] +model = "gpt-4o" +base_url = "https://api.openai.com/v1" +api_key = "sk-..." # 実際のAPIキーに置き換えてください +max_tokens = 4096 +temperature = 0.0 + +# 特定のLLMモデル用のオプション設定 +[llm.vision] +model = "gpt-4o" +base_url = "https://api.openai.com/v1" +api_key = "sk-..." # 実際のAPIキーに置き換えてください +``` + +## クイックスタート + +OpenManusを実行する一行コマンド: + +```bash +python main.py +``` + +その後、ターミナルからプロンプトを入力してください! + +MCP ツールバージョンを使用する場合は、以下を実行します: +```bash +python run_mcp.py +``` + +開発中のマルチエージェントバージョンを試すには、以下を実行します: + +```bash +python run_flow.py +``` + +## カスタムマルチエージェントの追加 + +現在、一般的なOpenManusエージェントに加えて、データ分析とデータ可視化タスクに適したDataAnalysisエージェントが組み込まれています。このエージェントを`config.toml`の`run_flow`に追加することができます。 + +```toml +# run-flowのオプション設定 +[runflow] +use_data_analysis_agent = true # デフォルトでは無効、trueに変更すると有効化されます +``` + +これに加えて、エージェントが正常に動作するために必要な依存関係をインストールする必要があります:[具体的なインストールガイド](app/tool/chart_visualization/README_ja.md##インストール) + + +## 貢献方法 + +我々は建設的な意見や有益な貢献を歓迎します!issueを作成するか、プルリクエストを提出してください。 + +または @mannaandpoem に📧メールでご連絡ください:mannaandpoem@gmail.com + +**注意**: プルリクエストを送信する前に、pre-commitツールを使用して変更を確認してください。`pre-commit run --all-files`を実行してチェックを実行します。 + +## コミュニティグループ +Feishuのネットワーキンググループに参加して、他の開発者と経験を共有しましょう! + +
+ OpenManus 交流群 +
+ +## スター履歴 + +[![Star History Chart](https://api.star-history.com/svg?repos=FoundationAgents/OpenManus&type=Date)](https://star-history.com/#FoundationAgents/OpenManus&Date) + +## 謝辞 + +このプロジェクトの基本的なサポートを提供してくれた[anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo) +と[browser-use](https://github.com/browser-use/browser-use)に感謝します! + +さらに、[AAAJ](https://github.com/metauto-ai/agent-as-a-judge)、[MetaGPT](https://github.com/geekan/MetaGPT)、[OpenHands](https://github.com/All-Hands-AI/OpenHands)、[SWE-agent](https://github.com/SWE-agent/SWE-agent)にも感謝します。 + +また、Hugging Face デモスペースをサポートしてくださった阶跃星辰 (stepfun)にも感謝いたします。 + +OpenManusはMetaGPTのコントリビューターによって構築されました。このエージェントコミュニティに大きな感謝を! + +## 引用 +```bibtex +@misc{openmanus2025, + author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong and Sheng Fan and Xiao Tang}, + title = {OpenManus: An open-source framework for building general AI agents}, + year = {2025}, + publisher = {Zenodo}, + doi = {10.5281/zenodo.15186407}, + url = {https://doi.org/10.5281/zenodo.15186407}, +} +``` diff --git a/README_ko.md b/README_ko.md new file mode 100644 index 0000000..5080f43 --- /dev/null +++ b/README_ko.md @@ -0,0 +1,192 @@ +

+ +

+ +[English](README.md) | [中文](README_zh.md) | 한국어 | [日本語](README_ja.md) + +[![GitHub stars](https://img.shields.io/github/stars/FoundationAgents/OpenManus?style=social)](https://github.com/FoundationAgents/OpenManus/stargazers) +  +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)   +[![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z) +[![Demo](https://img.shields.io/badge/Demo-Hugging%20Face-yellow)](https://huggingface.co/spaces/lyh-917/OpenManusDemo) +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15186407.svg)](https://doi.org/10.5281/zenodo.15186407) + +# 👋 OpenManus + +Manus는 놀라운 도구지만, OpenManus는 *초대 코드* 없이도 모든 아이디어를 실현할 수 있습니다! 🛫 + +우리 팀의 멤버인 [@Xinbin Liang](https://github.com/mannaandpoem)와 [@Jinyu Xiang](https://github.com/XiangJinyu) (핵심 작성자), 그리고 [@Zhaoyang Yu](https://github.com/MoshiQAQ), [@Jiayi Zhang](https://github.com/didiforgithub), [@Sirui Hong](https://github.com/stellaHSR)이 함께 했습니다. 우리는 [@MetaGPT](https://github.com/geekan/MetaGPT)로부터 왔습니다. 프로토타입은 단 3시간 만에 출시되었으며, 계속해서 발전하고 있습니다! + +이 프로젝트는 간단한 구현에서 시작되었으며, 여러분의 제안, 기여 및 피드백을 환영합니다! + +OpenManus를 통해 여러분만의 에이전트를 즐겨보세요! + +또한 [OpenManus-RL](https://github.com/OpenManus/OpenManus-RL)을 소개하게 되어 기쁩니다. OpenManus와 UIUC 연구자들이 공동 개발한 이 오픈소스 프로젝트는 LLM 에이전트에 대해 강화 학습(RL) 기반 (예: GRPO) 튜닝 방법을 제공합니다. + +## 프로젝트 데모 + + + +## 설치 방법 + +두 가지 설치 방법을 제공합니다. **방법 2 (uv 사용)** 이 더 빠른 설치와 효율적인 종속성 관리를 위해 권장됩니다. + +### 방법 1: conda 사용 + +1. 새로운 conda 환경을 생성합니다: + +```bash +conda create -n open_manus python=3.12 +conda activate open_manus +``` + +2. 저장소를 클론합니다: + +```bash +git clone https://github.com/FoundationAgents/OpenManus.git +cd OpenManus +``` + +3. 종속성을 설치합니다: + +```bash +pip install -r requirements.txt +``` + +### 방법 2: uv 사용 (권장) + +1. uv를 설치합니다. (빠른 Python 패키지 설치 및 종속성 관리 도구): + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +2. 저장소를 클론합니다: + +```bash +git clone https://github.com/FoundationAgents/OpenManus.git +cd OpenManus +``` + +3. 새로운 가상 환경을 생성하고 활성화합니다: + +```bash +uv venv --python 3.12 +source .venv/bin/activate # Unix/macOS의 경우 +# Windows의 경우: +# .venv\Scripts\activate +``` + +4. 종속성을 설치합니다: + +```bash +uv pip install -r requirements.txt +``` + +### 브라우저 자동화 도구 (선택사항) +```bash +playwright install +``` + +## 설정 방법 + +OpenManus를 사용하려면 사용하는 LLM API에 대한 설정이 필요합니다. 아래 단계를 따라 설정을 완료하세요: + +1. `config` 디렉토리에 `config.toml` 파일을 생성하세요 (예제 파일을 복사하여 사용할 수 있습니다): + +```bash +cp config/config.example.toml config/config.toml +``` + +2. `config/config.toml` 파일을 편집하여 API 키를 추가하고 설정을 커스터마이징하세요: + +```toml +# 전역 LLM 설정 +[llm] +model = "gpt-4o" +base_url = "https://api.openai.com/v1" +api_key = "sk-..." # 실제 API 키로 변경하세요 +max_tokens = 4096 +temperature = 0.0 + +# 특정 LLM 모델에 대한 선택적 설정 +[llm.vision] +model = "gpt-4o" +base_url = "https://api.openai.com/v1" +api_key = "sk-..." # 실제 API 키로 변경하세요 +``` + +## 빠른 시작 + +OpenManus를 실행하는 한 줄 명령어: + +```bash +python main.py +``` + +이후 터미널에서 아이디어를 작성하세요! + +MCP 도구 버전을 사용하려면 다음을 실행하세요: +```bash +python run_mcp.py +``` + +불안정한 멀티 에이전트 버전을 실행하려면 다음을 실행할 수 있습니다: + +```bash +python run_flow.py +``` + +### 사용자 정의 다중 에이전트 추가 + +현재 일반 OpenManus 에이전트 외에도 데이터 분석 및 데이터 시각화 작업에 적합한 DataAnalysis 에이전트를 통합했습니다. 이 에이전트를 `config.toml`의 `run_flow`에 추가할 수 있습니다. + +```toml +# run-flow에 대한 선택적 구성 +[runflow] +use_data_analysis_agent = true # 기본적으로 비활성화되어 있으며, 활성화하려면 true로 변경 +``` + +또한, 에이전트가 제대로 작동하도록 관련 종속성을 설치해야 합니다: [상세 설치 가이드](app/tool/chart_visualization/README.md##Installation) + +## 기여 방법 + +모든 친절한 제안과 유용한 기여를 환영합니다! 이슈를 생성하거나 풀 리퀘스트를 제출해 주세요. + +또는 📧 메일로 연락주세요. @mannaandpoem : mannaandpoem@gmail.com + +**참고**: pull request를 제출하기 전에 pre-commit 도구를 사용하여 변경 사항을 확인하십시오. `pre-commit run --all-files`를 실행하여 검사를 실행합니다. + +## 커뮤니티 그룹 +Feishu 네트워킹 그룹에 참여하여 다른 개발자들과 경험을 공유하세요! + +
+ OpenManus 交流群 +
+ +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=FoundationAgents/OpenManus&type=Date)](https://star-history.com/#FoundationAgents/OpenManus&Date) + +## 감사의 글 + +이 프로젝트에 기본적인 지원을 제공해 주신 [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo)와 +[browser-use](https://github.com/browser-use/browser-use)에게 감사드립니다! + +또한, [AAAJ](https://github.com/metauto-ai/agent-as-a-judge), [MetaGPT](https://github.com/geekan/MetaGPT), [OpenHands](https://github.com/All-Hands-AI/OpenHands), [SWE-agent](https://github.com/SWE-agent/SWE-agent)에 깊은 감사를 드립니다. + +또한 Hugging Face 데모 공간을 지원해 주신 阶跃星辰 (stepfun)에게 감사드립니다. + +OpenManus는 MetaGPT 기여자들에 의해 개발되었습니다. 이 에이전트 커뮤니티에 깊은 감사를 전합니다! + +## 인용 +```bibtex +@misc{openmanus2025, + author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong and Sheng Fan and Xiao Tang}, + title = {OpenManus: An open-source framework for building general AI agents}, + year = {2025}, + publisher = {Zenodo}, + doi = {10.5281/zenodo.15186407}, + url = {https://doi.org/10.5281/zenodo.15186407}, +} +``` diff --git a/README_zh.md b/README_zh.md new file mode 100644 index 0000000..680ccc1 --- /dev/null +++ b/README_zh.md @@ -0,0 +1,198 @@ +

+ +

+ +[English](README.md) | 中文 | [한국어](README_ko.md) | [日本語](README_ja.md) + +[![GitHub stars](https://img.shields.io/github/stars/FoundationAgents/OpenManus?style=social)](https://github.com/FoundationAgents/OpenManus/stargazers) +  +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)   +[![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z) +[![Demo](https://img.shields.io/badge/Demo-Hugging%20Face-yellow)](https://huggingface.co/spaces/lyh-917/OpenManusDemo) +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15186407.svg)](https://doi.org/10.5281/zenodo.15186407) + +# 👋 OpenManus + +Manus 非常棒,但 OpenManus 无需邀请码即可实现任何创意 🛫! + +我们的团队成员 [@Xinbin Liang](https://github.com/mannaandpoem) 和 [@Jinyu Xiang](https://github.com/XiangJinyu)(核心作者),以及 [@Zhaoyang Yu](https://github.com/MoshiQAQ)、[@Jiayi Zhang](https://github.com/didiforgithub) 和 [@Sirui Hong](https://github.com/stellaHSR),来自 [@MetaGPT](https://github.com/geekan/MetaGPT)团队。我们在 3 +小时内完成了开发并持续迭代中! + +这是一个简洁的实现方案,欢迎任何建议、贡献和反馈! + +用 OpenManus 开启你的智能体之旅吧! + +我们也非常高兴地向大家介绍 [OpenManus-RL](https://github.com/OpenManus/OpenManus-RL),这是一个专注于基于强化学习(RL,例如 GRPO)的方法来优化大语言模型(LLM)智能体的开源项目,由来自UIUC 和 OpenManus 的研究人员合作开发。 + +## 项目演示 + + + +## 安装指南 + +我们提供两种安装方式。推荐使用方式二(uv),因为它能提供更快的安装速度和更好的依赖管理。 + +### 方式一:使用 conda + +1. 创建新的 conda 环境: + +```bash +conda create -n open_manus python=3.12 +conda activate open_manus +``` + +2. 克隆仓库: + +```bash +git clone https://github.com/FoundationAgents/OpenManus.git +cd OpenManus +``` + +3. 安装依赖: + +```bash +pip install -r requirements.txt +``` + +### 方式二:使用 uv(推荐) + +1. 安装 uv(一个快速的 Python 包管理器): + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +2. 克隆仓库: + +```bash +git clone https://github.com/FoundationAgents/OpenManus.git +cd OpenManus +``` + +3. 创建并激活虚拟环境: + +```bash +uv venv --python 3.12 +source .venv/bin/activate # Unix/macOS 系统 +# Windows 系统使用: +# .venv\Scripts\activate +``` + +4. 安装依赖: + +```bash +uv pip install -r requirements.txt +``` + +### 浏览器自动化工具(可选) +```bash +playwright install +``` + +## 配置说明 + +OpenManus 需要配置使用的 LLM API,请按以下步骤设置: + +1. 在 `config` 目录创建 `config.toml` 文件(可从示例复制): + +```bash +cp config/config.example.toml config/config.toml +``` + +2. 编辑 `config/config.toml` 添加 API 密钥和自定义设置: + +```toml +# 全局 LLM 配置 +[llm] +model = "gpt-4o" +base_url = "https://api.openai.com/v1" +api_key = "sk-..." # 替换为真实 API 密钥 +max_tokens = 4096 +temperature = 0.0 + +# 可选特定 LLM 模型配置 +[llm.vision] +model = "gpt-4o" +base_url = "https://api.openai.com/v1" +api_key = "sk-..." # 替换为真实 API 密钥 +``` + +## 快速启动 + +一行命令运行 OpenManus: + +```bash +python main.py +``` + +然后通过终端输入你的创意! + +如需使用 MCP 工具版本,可运行: +```bash +python run_mcp.py +``` + +如需体验不稳定的多智能体版本,可运行: + +```bash +python run_flow.py +``` + +## 添加自定义多智能体 + +目前除了通用的 OpenManus Agent, 我们还内置了DataAnalysis Agent,适用于数据分析和数据可视化任务,你可以在`config.toml`中将这个智能体加入到`run_flow`中 +```toml +# run-flow可选配置 +[runflow] +use_data_analysis_agent = true # 默认关闭,将其改为true则为激活 +``` +除此之外,你还需要安装相关的依赖来确保智能体正常运行:[具体安装指南](app/tool/chart_visualization/README_zh.md##安装) + + +## 贡献指南 + +我们欢迎任何友好的建议和有价值的贡献!可以直接创建 issue 或提交 pull request。 + +或通过 📧 邮件联系 @mannaandpoem:mannaandpoem@gmail.com + +**注意**: 在提交 pull request 之前,请使用 pre-commit 工具检查您的更改。运行 `pre-commit run --all-files` 来执行检查。 + +## 交流群 + +加入我们的飞书交流群,与其他开发者分享经验! + +
+ OpenManus 交流群 +
+ +## Star 数量 + +[![Star History Chart](https://api.star-history.com/svg?repos=FoundationAgents/OpenManus&type=Date)](https://star-history.com/#FoundationAgents/OpenManus&Date) + + +## 赞助商 +感谢[PPIO](https://ppinfra.com/user/register?invited_by=OCPKCN&utm_source=github_openmanus&utm_medium=github_readme&utm_campaign=link) 提供的算力支持。 +> PPIO派欧云:一键调用高性价比的开源模型API和GPU容器 + +## 致谢 + +特别感谢 [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo) +和 [browser-use](https://github.com/browser-use/browser-use) 为本项目提供的基础支持! + +此外,我们感谢 [AAAJ](https://github.com/metauto-ai/agent-as-a-judge),[MetaGPT](https://github.com/geekan/MetaGPT),[OpenHands](https://github.com/All-Hands-AI/OpenHands) 和 [SWE-agent](https://github.com/SWE-agent/SWE-agent). + +我们也感谢阶跃星辰 (stepfun) 提供的 Hugging Face 演示空间支持。 + +OpenManus 由 MetaGPT 社区的贡献者共同构建,感谢这个充满活力的智能体开发者社区! + +## 引用 +```bibtex +@misc{openmanus2025, + author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong and Sheng Fan and Xiao Tang}, + title = {OpenManus: An open-source framework for building general AI agents}, + year = {2025}, + publisher = {Zenodo}, + doi = {10.5281/zenodo.15186407}, + url = {https://doi.org/10.5281/zenodo.15186407}, +} +``` diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..0749c6d --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,10 @@ +# Python version check: 3.11-3.13 +import sys + + +if sys.version_info < (3, 11) or sys.version_info > (3, 13): + print( + "Warning: Unsupported Python version {ver}, please use 3.11-3.13".format( + ver=".".join(map(str, sys.version_info)) + ) + ) diff --git a/app/agent/__init__.py b/app/agent/__init__.py new file mode 100644 index 0000000..f7df2b9 --- /dev/null +++ b/app/agent/__init__.py @@ -0,0 +1,16 @@ +from app.agent.base import BaseAgent +from app.agent.browser import BrowserAgent +from app.agent.mcp import MCPAgent +from app.agent.react import ReActAgent +from app.agent.swe import SWEAgent +from app.agent.toolcall import ToolCallAgent + + +__all__ = [ + "BaseAgent", + "BrowserAgent", + "ReActAgent", + "SWEAgent", + "ToolCallAgent", + "MCPAgent", +] diff --git a/app/agent/base.py b/app/agent/base.py new file mode 100644 index 0000000..65f6600 --- /dev/null +++ b/app/agent/base.py @@ -0,0 +1,196 @@ +from abc import ABC, abstractmethod +from contextlib import asynccontextmanager +from typing import List, Optional + +from pydantic import BaseModel, Field, model_validator + +from app.llm import LLM +from app.logger import logger +from app.sandbox.client import SANDBOX_CLIENT +from app.schema import ROLE_TYPE, AgentState, Memory, Message + + +class BaseAgent(BaseModel, ABC): + """Abstract base class for managing agent state and execution. + + Provides foundational functionality for state transitions, memory management, + and a step-based execution loop. Subclasses must implement the `step` method. + """ + + # Core attributes + name: str = Field(..., description="Unique name of the agent") + description: Optional[str] = Field(None, description="Optional agent description") + + # Prompts + system_prompt: Optional[str] = Field( + None, description="System-level instruction prompt" + ) + next_step_prompt: Optional[str] = Field( + None, description="Prompt for determining next action" + ) + + # Dependencies + llm: LLM = Field(default_factory=LLM, description="Language model instance") + memory: Memory = Field(default_factory=Memory, description="Agent's memory store") + state: AgentState = Field( + default=AgentState.IDLE, description="Current agent state" + ) + + # Execution control + max_steps: int = Field(default=10, description="Maximum steps before termination") + current_step: int = Field(default=0, description="Current step in execution") + + duplicate_threshold: int = 2 + + class Config: + arbitrary_types_allowed = True + extra = "allow" # Allow extra fields for flexibility in subclasses + + @model_validator(mode="after") + def initialize_agent(self) -> "BaseAgent": + """Initialize agent with default settings if not provided.""" + if self.llm is None or not isinstance(self.llm, LLM): + self.llm = LLM(config_name=self.name.lower()) + if not isinstance(self.memory, Memory): + self.memory = Memory() + return self + + @asynccontextmanager + async def state_context(self, new_state: AgentState): + """Context manager for safe agent state transitions. + + Args: + new_state: The state to transition to during the context. + + Yields: + None: Allows execution within the new state. + + Raises: + ValueError: If the new_state is invalid. + """ + if not isinstance(new_state, AgentState): + raise ValueError(f"Invalid state: {new_state}") + + previous_state = self.state + self.state = new_state + try: + yield + except Exception as e: + self.state = AgentState.ERROR # Transition to ERROR on failure + raise e + finally: + self.state = previous_state # Revert to previous state + + def update_memory( + self, + role: ROLE_TYPE, # type: ignore + content: str, + base64_image: Optional[str] = None, + **kwargs, + ) -> None: + """Add a message to the agent's memory. + + Args: + role: The role of the message sender (user, system, assistant, tool). + content: The message content. + base64_image: Optional base64 encoded image. + **kwargs: Additional arguments (e.g., tool_call_id for tool messages). + + Raises: + ValueError: If the role is unsupported. + """ + message_map = { + "user": Message.user_message, + "system": Message.system_message, + "assistant": Message.assistant_message, + "tool": lambda content, **kw: Message.tool_message(content, **kw), + } + + if role not in message_map: + raise ValueError(f"Unsupported message role: {role}") + + # Create message with appropriate parameters based on role + kwargs = {"base64_image": base64_image, **(kwargs if role == "tool" else {})} + self.memory.add_message(message_map[role](content, **kwargs)) + + async def run(self, request: Optional[str] = None) -> str: + """Execute the agent's main loop asynchronously. + + Args: + request: Optional initial user request to process. + + Returns: + A string summarizing the execution results. + + Raises: + RuntimeError: If the agent is not in IDLE state at start. + """ + if self.state != AgentState.IDLE: + raise RuntimeError(f"Cannot run agent from state: {self.state}") + + if request: + self.update_memory("user", request) + + results: List[str] = [] + async with self.state_context(AgentState.RUNNING): + while ( + self.current_step < self.max_steps and self.state != AgentState.FINISHED + ): + self.current_step += 1 + logger.info(f"Executing step {self.current_step}/{self.max_steps}") + step_result = await self.step() + + # Check for stuck state + if self.is_stuck(): + self.handle_stuck_state() + + results.append(f"Step {self.current_step}: {step_result}") + + if self.current_step >= self.max_steps: + self.current_step = 0 + self.state = AgentState.IDLE + results.append(f"Terminated: Reached max steps ({self.max_steps})") + await SANDBOX_CLIENT.cleanup() + return "\n".join(results) if results else "No steps executed" + + @abstractmethod + async def step(self) -> str: + """Execute a single step in the agent's workflow. + + Must be implemented by subclasses to define specific behavior. + """ + + def handle_stuck_state(self): + """Handle stuck state by adding a prompt to change strategy""" + stuck_prompt = "\ + Observed duplicate responses. Consider new strategies and avoid repeating ineffective paths already attempted." + self.next_step_prompt = f"{stuck_prompt}\n{self.next_step_prompt}" + logger.warning(f"Agent detected stuck state. Added prompt: {stuck_prompt}") + + def is_stuck(self) -> bool: + """Check if the agent is stuck in a loop by detecting duplicate content""" + if len(self.memory.messages) < 2: + return False + + last_message = self.memory.messages[-1] + if not last_message.content: + return False + + # Count identical content occurrences + duplicate_count = sum( + 1 + for msg in reversed(self.memory.messages[:-1]) + if msg.role == "assistant" and msg.content == last_message.content + ) + + return duplicate_count >= self.duplicate_threshold + + @property + def messages(self) -> List[Message]: + """Retrieve a list of messages from the agent's memory.""" + return self.memory.messages + + @messages.setter + def messages(self, value: List[Message]): + """Set the list of messages in the agent's memory.""" + self.memory.messages = value diff --git a/app/agent/browser.py b/app/agent/browser.py new file mode 100644 index 0000000..3f25c45 --- /dev/null +++ b/app/agent/browser.py @@ -0,0 +1,129 @@ +import json +from typing import TYPE_CHECKING, Optional + +from pydantic import Field, model_validator + +from app.agent.toolcall import ToolCallAgent +from app.logger import logger +from app.prompt.browser import NEXT_STEP_PROMPT, SYSTEM_PROMPT +from app.schema import Message, ToolChoice +from app.tool import BrowserUseTool, Terminate, ToolCollection +from app.tool.sandbox.sb_browser_tool import SandboxBrowserTool + + +# Avoid circular import if BrowserAgent needs BrowserContextHelper +if TYPE_CHECKING: + from app.agent.base import BaseAgent # Or wherever memory is defined + + +class BrowserContextHelper: + def __init__(self, agent: "BaseAgent"): + self.agent = agent + self._current_base64_image: Optional[str] = None + + async def get_browser_state(self) -> Optional[dict]: + browser_tool = self.agent.available_tools.get_tool(BrowserUseTool().name) + if not browser_tool: + browser_tool = self.agent.available_tools.get_tool( + SandboxBrowserTool().name + ) + if not browser_tool or not hasattr(browser_tool, "get_current_state"): + logger.warning("BrowserUseTool not found or doesn't have get_current_state") + return None + try: + result = await browser_tool.get_current_state() + if result.error: + logger.debug(f"Browser state error: {result.error}") + return None + if hasattr(result, "base64_image") and result.base64_image: + self._current_base64_image = result.base64_image + else: + self._current_base64_image = None + return json.loads(result.output) + except Exception as e: + logger.debug(f"Failed to get browser state: {str(e)}") + return None + + async def format_next_step_prompt(self) -> str: + """Gets browser state and formats the browser prompt.""" + browser_state = await self.get_browser_state() + url_info, tabs_info, content_above_info, content_below_info = "", "", "", "" + results_info = "" # Or get from agent if needed elsewhere + + if browser_state and not browser_state.get("error"): + url_info = f"\n URL: {browser_state.get('url', 'N/A')}\n Title: {browser_state.get('title', 'N/A')}" + tabs = browser_state.get("tabs", []) + if tabs: + tabs_info = f"\n {len(tabs)} tab(s) available" + pixels_above = browser_state.get("pixels_above", 0) + pixels_below = browser_state.get("pixels_below", 0) + if pixels_above > 0: + content_above_info = f" ({pixels_above} pixels)" + if pixels_below > 0: + content_below_info = f" ({pixels_below} pixels)" + + if self._current_base64_image: + image_message = Message.user_message( + content="Current browser screenshot:", + base64_image=self._current_base64_image, + ) + self.agent.memory.add_message(image_message) + self._current_base64_image = None # Consume the image after adding + + return NEXT_STEP_PROMPT.format( + url_placeholder=url_info, + tabs_placeholder=tabs_info, + content_above_placeholder=content_above_info, + content_below_placeholder=content_below_info, + results_placeholder=results_info, + ) + + async def cleanup_browser(self): + browser_tool = self.agent.available_tools.get_tool(BrowserUseTool().name) + if browser_tool and hasattr(browser_tool, "cleanup"): + await browser_tool.cleanup() + + +class BrowserAgent(ToolCallAgent): + """ + A browser agent that uses the browser_use library to control a browser. + + This agent can navigate web pages, interact with elements, fill forms, + extract content, and perform other browser-based actions to accomplish tasks. + """ + + name: str = "browser" + description: str = "A browser agent that can control a browser to accomplish tasks" + + system_prompt: str = SYSTEM_PROMPT + next_step_prompt: str = NEXT_STEP_PROMPT + + max_observe: int = 10000 + max_steps: int = 20 + + # Configure the available tools + available_tools: ToolCollection = Field( + default_factory=lambda: ToolCollection(BrowserUseTool(), Terminate()) + ) + + # Use Auto for tool choice to allow both tool usage and free-form responses + tool_choices: ToolChoice = ToolChoice.AUTO + special_tool_names: list[str] = Field(default_factory=lambda: [Terminate().name]) + + browser_context_helper: Optional[BrowserContextHelper] = None + + @model_validator(mode="after") + def initialize_helper(self) -> "BrowserAgent": + self.browser_context_helper = BrowserContextHelper(self) + return self + + async def think(self) -> bool: + """Process current state and decide next actions using tools, with browser state info added""" + self.next_step_prompt = ( + await self.browser_context_helper.format_next_step_prompt() + ) + return await super().think() + + async def cleanup(self): + """Clean up browser agent resources by calling parent cleanup.""" + await self.browser_context_helper.cleanup_browser() diff --git a/app/agent/data_analysis.py b/app/agent/data_analysis.py new file mode 100644 index 0000000..79b9c29 --- /dev/null +++ b/app/agent/data_analysis.py @@ -0,0 +1,37 @@ +from pydantic import Field + +from app.agent.toolcall import ToolCallAgent +from app.config import config +from app.prompt.visualization import NEXT_STEP_PROMPT, SYSTEM_PROMPT +from app.tool import Terminate, ToolCollection +from app.tool.chart_visualization.chart_prepare import VisualizationPrepare +from app.tool.chart_visualization.data_visualization import DataVisualization +from app.tool.chart_visualization.python_execute import NormalPythonExecute + + +class DataAnalysis(ToolCallAgent): + """ + A data analysis agent that uses planning to solve various data analysis tasks. + + This agent extends ToolCallAgent with a comprehensive set of tools and capabilities, + including Data Analysis, Chart Visualization, Data Report. + """ + + name: str = "Data_Analysis" + description: str = "An analytical agent that utilizes python and data visualization tools to solve diverse data analysis tasks" + + system_prompt: str = SYSTEM_PROMPT.format(directory=config.workspace_root) + next_step_prompt: str = NEXT_STEP_PROMPT + + max_observe: int = 15000 + max_steps: int = 20 + + # Add general-purpose tools to the tool collection + available_tools: ToolCollection = Field( + default_factory=lambda: ToolCollection( + NormalPythonExecute(), + VisualizationPrepare(), + DataVisualization(), + Terminate(), + ) + ) diff --git a/app/agent/manus.py b/app/agent/manus.py new file mode 100644 index 0000000..df40edb --- /dev/null +++ b/app/agent/manus.py @@ -0,0 +1,165 @@ +from typing import Dict, List, Optional + +from pydantic import Field, model_validator + +from app.agent.browser import BrowserContextHelper +from app.agent.toolcall import ToolCallAgent +from app.config import config +from app.logger import logger +from app.prompt.manus import NEXT_STEP_PROMPT, SYSTEM_PROMPT +from app.tool import Terminate, ToolCollection +from app.tool.ask_human import AskHuman +from app.tool.browser_use_tool import BrowserUseTool +from app.tool.mcp import MCPClients, MCPClientTool +from app.tool.python_execute import PythonExecute +from app.tool.str_replace_editor import StrReplaceEditor + + +class Manus(ToolCallAgent): + """A versatile general-purpose agent with support for both local and MCP tools.""" + + name: str = "Manus" + description: str = "A versatile agent that can solve various tasks using multiple tools including MCP-based tools" + + system_prompt: str = SYSTEM_PROMPT.format(directory=config.workspace_root) + next_step_prompt: str = NEXT_STEP_PROMPT + + max_observe: int = 10000 + max_steps: int = 20 + + # MCP clients for remote tool access + mcp_clients: MCPClients = Field(default_factory=MCPClients) + + # Add general-purpose tools to the tool collection + available_tools: ToolCollection = Field( + default_factory=lambda: ToolCollection( + PythonExecute(), + BrowserUseTool(), + StrReplaceEditor(), + AskHuman(), + Terminate(), + ) + ) + + special_tool_names: list[str] = Field(default_factory=lambda: [Terminate().name]) + browser_context_helper: Optional[BrowserContextHelper] = None + + # Track connected MCP servers + connected_servers: Dict[str, str] = Field( + default_factory=dict + ) # server_id -> url/command + _initialized: bool = False + + @model_validator(mode="after") + def initialize_helper(self) -> "Manus": + """Initialize basic components synchronously.""" + self.browser_context_helper = BrowserContextHelper(self) + return self + + @classmethod + async def create(cls, **kwargs) -> "Manus": + """Factory method to create and properly initialize a Manus instance.""" + instance = cls(**kwargs) + await instance.initialize_mcp_servers() + instance._initialized = True + return instance + + async def initialize_mcp_servers(self) -> None: + """Initialize connections to configured MCP servers.""" + for server_id, server_config in config.mcp_config.servers.items(): + try: + if server_config.type == "sse": + if server_config.url: + await self.connect_mcp_server(server_config.url, server_id) + logger.info( + f"Connected to MCP server {server_id} at {server_config.url}" + ) + elif server_config.type == "stdio": + if server_config.command: + await self.connect_mcp_server( + server_config.command, + server_id, + use_stdio=True, + stdio_args=server_config.args, + ) + logger.info( + f"Connected to MCP server {server_id} using command {server_config.command}" + ) + except Exception as e: + logger.error(f"Failed to connect to MCP server {server_id}: {e}") + + async def connect_mcp_server( + self, + server_url: str, + server_id: str = "", + use_stdio: bool = False, + stdio_args: List[str] = None, + ) -> None: + """Connect to an MCP server and add its tools.""" + if use_stdio: + await self.mcp_clients.connect_stdio( + server_url, stdio_args or [], server_id + ) + self.connected_servers[server_id or server_url] = server_url + else: + await self.mcp_clients.connect_sse(server_url, server_id) + self.connected_servers[server_id or server_url] = server_url + + # Update available tools with only the new tools from this server + new_tools = [ + tool for tool in self.mcp_clients.tools if tool.server_id == server_id + ] + self.available_tools.add_tools(*new_tools) + + async def disconnect_mcp_server(self, server_id: str = "") -> None: + """Disconnect from an MCP server and remove its tools.""" + await self.mcp_clients.disconnect(server_id) + if server_id: + self.connected_servers.pop(server_id, None) + else: + self.connected_servers.clear() + + # Rebuild available tools without the disconnected server's tools + base_tools = [ + tool + for tool in self.available_tools.tools + if not isinstance(tool, MCPClientTool) + ] + self.available_tools = ToolCollection(*base_tools) + self.available_tools.add_tools(*self.mcp_clients.tools) + + async def cleanup(self): + """Clean up Manus agent resources.""" + if self.browser_context_helper: + await self.browser_context_helper.cleanup_browser() + # Disconnect from all MCP servers only if we were initialized + if self._initialized: + await self.disconnect_mcp_server() + self._initialized = False + + async def think(self) -> bool: + """Process current state and decide next actions with appropriate context.""" + if not self._initialized: + await self.initialize_mcp_servers() + self._initialized = True + + original_prompt = self.next_step_prompt + recent_messages = self.memory.messages[-3:] if self.memory.messages else [] + browser_in_use = any( + tc.function.name == BrowserUseTool().name + for msg in recent_messages + if msg.tool_calls + for tc in msg.tool_calls + ) + + if browser_in_use: + self.next_step_prompt = ( + await self.browser_context_helper.format_next_step_prompt() + ) + + result = await super().think() + + # Restore original prompt + self.next_step_prompt = original_prompt + + return result diff --git a/app/agent/mcp.py b/app/agent/mcp.py new file mode 100644 index 0000000..9c6da6a --- /dev/null +++ b/app/agent/mcp.py @@ -0,0 +1,185 @@ +from typing import Any, Dict, List, Optional, Tuple + +from pydantic import Field + +from app.agent.toolcall import ToolCallAgent +from app.logger import logger +from app.prompt.mcp import MULTIMEDIA_RESPONSE_PROMPT, NEXT_STEP_PROMPT, SYSTEM_PROMPT +from app.schema import AgentState, Message +from app.tool.base import ToolResult +from app.tool.mcp import MCPClients + + +class MCPAgent(ToolCallAgent): + """Agent for interacting with MCP (Model Context Protocol) servers. + + This agent connects to an MCP server using either SSE or stdio transport + and makes the server's tools available through the agent's tool interface. + """ + + name: str = "mcp_agent" + description: str = "An agent that connects to an MCP server and uses its tools." + + system_prompt: str = SYSTEM_PROMPT + next_step_prompt: str = NEXT_STEP_PROMPT + + # Initialize MCP tool collection + mcp_clients: MCPClients = Field(default_factory=MCPClients) + available_tools: MCPClients = None # Will be set in initialize() + + max_steps: int = 20 + connection_type: str = "stdio" # "stdio" or "sse" + + # Track tool schemas to detect changes + tool_schemas: Dict[str, Dict[str, Any]] = Field(default_factory=dict) + _refresh_tools_interval: int = 5 # Refresh tools every N steps + + # Special tool names that should trigger termination + special_tool_names: List[str] = Field(default_factory=lambda: ["terminate"]) + + async def initialize( + self, + connection_type: Optional[str] = None, + server_url: Optional[str] = None, + command: Optional[str] = None, + args: Optional[List[str]] = None, + ) -> None: + """Initialize the MCP connection. + + Args: + connection_type: Type of connection to use ("stdio" or "sse") + server_url: URL of the MCP server (for SSE connection) + command: Command to run (for stdio connection) + args: Arguments for the command (for stdio connection) + """ + if connection_type: + self.connection_type = connection_type + + # Connect to the MCP server based on connection type + if self.connection_type == "sse": + if not server_url: + raise ValueError("Server URL is required for SSE connection") + await self.mcp_clients.connect_sse(server_url=server_url) + elif self.connection_type == "stdio": + if not command: + raise ValueError("Command is required for stdio connection") + await self.mcp_clients.connect_stdio(command=command, args=args or []) + else: + raise ValueError(f"Unsupported connection type: {self.connection_type}") + + # Set available_tools to our MCP instance + self.available_tools = self.mcp_clients + + # Store initial tool schemas + await self._refresh_tools() + + # Add system message about available tools + tool_names = list(self.mcp_clients.tool_map.keys()) + tools_info = ", ".join(tool_names) + + # Add system prompt and available tools information + self.memory.add_message( + Message.system_message( + f"{self.system_prompt}\n\nAvailable MCP tools: {tools_info}" + ) + ) + + async def _refresh_tools(self) -> Tuple[List[str], List[str]]: + """Refresh the list of available tools from the MCP server. + + Returns: + A tuple of (added_tools, removed_tools) + """ + if not self.mcp_clients.sessions: + return [], [] + + # Get current tool schemas directly from the server + response = await self.mcp_clients.list_tools() + current_tools = {tool.name: tool.inputSchema for tool in response.tools} + + # Determine added, removed, and changed tools + current_names = set(current_tools.keys()) + previous_names = set(self.tool_schemas.keys()) + + added_tools = list(current_names - previous_names) + removed_tools = list(previous_names - current_names) + + # Check for schema changes in existing tools + changed_tools = [] + for name in current_names.intersection(previous_names): + if current_tools[name] != self.tool_schemas.get(name): + changed_tools.append(name) + + # Update stored schemas + self.tool_schemas = current_tools + + # Log and notify about changes + if added_tools: + logger.info(f"Added MCP tools: {added_tools}") + self.memory.add_message( + Message.system_message(f"New tools available: {', '.join(added_tools)}") + ) + if removed_tools: + logger.info(f"Removed MCP tools: {removed_tools}") + self.memory.add_message( + Message.system_message( + f"Tools no longer available: {', '.join(removed_tools)}" + ) + ) + if changed_tools: + logger.info(f"Changed MCP tools: {changed_tools}") + + return added_tools, removed_tools + + async def think(self) -> bool: + """Process current state and decide next action.""" + # Check MCP session and tools availability + if not self.mcp_clients.sessions or not self.mcp_clients.tool_map: + logger.info("MCP service is no longer available, ending interaction") + self.state = AgentState.FINISHED + return False + + # Refresh tools periodically + if self.current_step % self._refresh_tools_interval == 0: + await self._refresh_tools() + # All tools removed indicates shutdown + if not self.mcp_clients.tool_map: + logger.info("MCP service has shut down, ending interaction") + self.state = AgentState.FINISHED + return False + + # Use the parent class's think method + return await super().think() + + async def _handle_special_tool(self, name: str, result: Any, **kwargs) -> None: + """Handle special tool execution and state changes""" + # First process with parent handler + await super()._handle_special_tool(name, result, **kwargs) + + # Handle multimedia responses + if isinstance(result, ToolResult) and result.base64_image: + self.memory.add_message( + Message.system_message( + MULTIMEDIA_RESPONSE_PROMPT.format(tool_name=name) + ) + ) + + def _should_finish_execution(self, name: str, **kwargs) -> bool: + """Determine if tool execution should finish the agent""" + # Terminate if the tool name is 'terminate' + return name.lower() == "terminate" + + async def cleanup(self) -> None: + """Clean up MCP connection when done.""" + if self.mcp_clients.sessions: + await self.mcp_clients.disconnect() + logger.info("MCP connection closed") + + async def run(self, request: Optional[str] = None) -> str: + """Run the agent with cleanup when done.""" + try: + result = await super().run(request) + return result + finally: + # Ensure cleanup happens even if there's an error + await self.cleanup() diff --git a/app/agent/react.py b/app/agent/react.py new file mode 100644 index 0000000..7f94820 --- /dev/null +++ b/app/agent/react.py @@ -0,0 +1,38 @@ +from abc import ABC, abstractmethod +from typing import Optional + +from pydantic import Field + +from app.agent.base import BaseAgent +from app.llm import LLM +from app.schema import AgentState, Memory + + +class ReActAgent(BaseAgent, ABC): + name: str + description: Optional[str] = None + + system_prompt: Optional[str] = None + next_step_prompt: Optional[str] = None + + llm: Optional[LLM] = Field(default_factory=LLM) + memory: Memory = Field(default_factory=Memory) + state: AgentState = AgentState.IDLE + + max_steps: int = 10 + current_step: int = 0 + + @abstractmethod + async def think(self) -> bool: + """Process current state and decide next action""" + + @abstractmethod + async def act(self) -> str: + """Execute decided actions""" + + async def step(self) -> str: + """Execute a single step: think and act.""" + should_act = await self.think() + if not should_act: + return "Thinking complete - no action needed" + return await self.act() diff --git a/app/agent/sandbox_agent.py b/app/agent/sandbox_agent.py new file mode 100644 index 0000000..58612d2 --- /dev/null +++ b/app/agent/sandbox_agent.py @@ -0,0 +1,223 @@ +from typing import Dict, List, Optional + +from pydantic import Field, model_validator + +from app.agent.browser import BrowserContextHelper +from app.agent.toolcall import ToolCallAgent +from app.config import config +from app.daytona.sandbox import create_sandbox, delete_sandbox +from app.daytona.tool_base import SandboxToolsBase +from app.logger import logger +from app.prompt.manus import NEXT_STEP_PROMPT, SYSTEM_PROMPT +from app.tool import Terminate, ToolCollection +from app.tool.ask_human import AskHuman +from app.tool.mcp import MCPClients, MCPClientTool +from app.tool.sandbox.sb_browser_tool import SandboxBrowserTool +from app.tool.sandbox.sb_files_tool import SandboxFilesTool +from app.tool.sandbox.sb_shell_tool import SandboxShellTool +from app.tool.sandbox.sb_vision_tool import SandboxVisionTool + + +class SandboxManus(ToolCallAgent): + """A versatile general-purpose agent with support for both local and MCP tools.""" + + name: str = "SandboxManus" + description: str = "A versatile agent that can solve various tasks using multiple sandbox-tools including MCP-based tools" + + system_prompt: str = SYSTEM_PROMPT.format(directory=config.workspace_root) + next_step_prompt: str = NEXT_STEP_PROMPT + + max_observe: int = 10000 + max_steps: int = 20 + + # MCP clients for remote tool access + mcp_clients: MCPClients = Field(default_factory=MCPClients) + + # Add general-purpose tools to the tool collection + available_tools: ToolCollection = Field( + default_factory=lambda: ToolCollection( + # PythonExecute(), + # BrowserUseTool(), + # StrReplaceEditor(), + AskHuman(), + Terminate(), + ) + ) + + special_tool_names: list[str] = Field(default_factory=lambda: [Terminate().name]) + browser_context_helper: Optional[BrowserContextHelper] = None + + # Track connected MCP servers + connected_servers: Dict[str, str] = Field( + default_factory=dict + ) # server_id -> url/command + _initialized: bool = False + sandbox_link: Optional[dict[str, dict[str, str]]] = Field(default_factory=dict) + + @model_validator(mode="after") + def initialize_helper(self) -> "SandboxManus": + """Initialize basic components synchronously.""" + self.browser_context_helper = BrowserContextHelper(self) + return self + + @classmethod + async def create(cls, **kwargs) -> "SandboxManus": + """Factory method to create and properly initialize a Manus instance.""" + instance = cls(**kwargs) + await instance.initialize_mcp_servers() + await instance.initialize_sandbox_tools() + instance._initialized = True + return instance + + async def initialize_sandbox_tools( + self, + password: str = config.daytona.VNC_password, + ) -> None: + try: + # 创建新沙箱 + if password: + sandbox = create_sandbox(password=password) + self.sandbox = sandbox + else: + raise ValueError("password must be provided") + vnc_link = sandbox.get_preview_link(6080) + website_link = sandbox.get_preview_link(8080) + vnc_url = vnc_link.url if hasattr(vnc_link, "url") else str(vnc_link) + website_url = ( + website_link.url if hasattr(website_link, "url") else str(website_link) + ) + + # Get the actual sandbox_id from the created sandbox + actual_sandbox_id = sandbox.id if hasattr(sandbox, "id") else "new_sandbox" + if not self.sandbox_link: + self.sandbox_link = {} + self.sandbox_link[actual_sandbox_id] = { + "vnc": vnc_url, + "website": website_url, + } + logger.info(f"VNC URL: {vnc_url}") + logger.info(f"Website URL: {website_url}") + SandboxToolsBase._urls_printed = True + sb_tools = [ + SandboxBrowserTool(sandbox), + SandboxFilesTool(sandbox), + SandboxShellTool(sandbox), + SandboxVisionTool(sandbox), + ] + self.available_tools.add_tools(*sb_tools) + + except Exception as e: + logger.error(f"Error initializing sandbox tools: {e}") + raise + + async def initialize_mcp_servers(self) -> None: + """Initialize connections to configured MCP servers.""" + for server_id, server_config in config.mcp_config.servers.items(): + try: + if server_config.type == "sse": + if server_config.url: + await self.connect_mcp_server(server_config.url, server_id) + logger.info( + f"Connected to MCP server {server_id} at {server_config.url}" + ) + elif server_config.type == "stdio": + if server_config.command: + await self.connect_mcp_server( + server_config.command, + server_id, + use_stdio=True, + stdio_args=server_config.args, + ) + logger.info( + f"Connected to MCP server {server_id} using command {server_config.command}" + ) + except Exception as e: + logger.error(f"Failed to connect to MCP server {server_id}: {e}") + + async def connect_mcp_server( + self, + server_url: str, + server_id: str = "", + use_stdio: bool = False, + stdio_args: List[str] = None, + ) -> None: + """Connect to an MCP server and add its tools.""" + if use_stdio: + await self.mcp_clients.connect_stdio( + server_url, stdio_args or [], server_id + ) + self.connected_servers[server_id or server_url] = server_url + else: + await self.mcp_clients.connect_sse(server_url, server_id) + self.connected_servers[server_id or server_url] = server_url + + # Update available tools with only the new tools from this server + new_tools = [ + tool for tool in self.mcp_clients.tools if tool.server_id == server_id + ] + self.available_tools.add_tools(*new_tools) + + async def disconnect_mcp_server(self, server_id: str = "") -> None: + """Disconnect from an MCP server and remove its tools.""" + await self.mcp_clients.disconnect(server_id) + if server_id: + self.connected_servers.pop(server_id, None) + else: + self.connected_servers.clear() + + # Rebuild available tools without the disconnected server's tools + base_tools = [ + tool + for tool in self.available_tools.tools + if not isinstance(tool, MCPClientTool) + ] + self.available_tools = ToolCollection(*base_tools) + self.available_tools.add_tools(*self.mcp_clients.tools) + + async def delete_sandbox(self, sandbox_id: str) -> None: + """Delete a sandbox by ID.""" + try: + await delete_sandbox(sandbox_id) + logger.info(f"Sandbox {sandbox_id} deleted successfully") + if sandbox_id in self.sandbox_link: + del self.sandbox_link[sandbox_id] + except Exception as e: + logger.error(f"Error deleting sandbox {sandbox_id}: {e}") + raise e + + async def cleanup(self): + """Clean up Manus agent resources.""" + if self.browser_context_helper: + await self.browser_context_helper.cleanup_browser() + # Disconnect from all MCP servers only if we were initialized + if self._initialized: + await self.disconnect_mcp_server() + await self.delete_sandbox(self.sandbox.id if self.sandbox else "unknown") + self._initialized = False + + async def think(self) -> bool: + """Process current state and decide next actions with appropriate context.""" + if not self._initialized: + await self.initialize_mcp_servers() + self._initialized = True + + original_prompt = self.next_step_prompt + recent_messages = self.memory.messages[-3:] if self.memory.messages else [] + browser_in_use = any( + tc.function.name == SandboxBrowserTool().name + for msg in recent_messages + if msg.tool_calls + for tc in msg.tool_calls + ) + + if browser_in_use: + self.next_step_prompt = ( + await self.browser_context_helper.format_next_step_prompt() + ) + + result = await super().think() + + # Restore original prompt + self.next_step_prompt = original_prompt + + return result diff --git a/app/agent/swe.py b/app/agent/swe.py new file mode 100644 index 0000000..e655a5b --- /dev/null +++ b/app/agent/swe.py @@ -0,0 +1,24 @@ +from typing import List + +from pydantic import Field + +from app.agent.toolcall import ToolCallAgent +from app.prompt.swe import SYSTEM_PROMPT +from app.tool import Bash, StrReplaceEditor, Terminate, ToolCollection + + +class SWEAgent(ToolCallAgent): + """An agent that implements the SWEAgent paradigm for executing code and natural conversations.""" + + name: str = "swe" + description: str = "an autonomous AI programmer that interacts directly with the computer to solve tasks." + + system_prompt: str = SYSTEM_PROMPT + next_step_prompt: str = "" + + available_tools: ToolCollection = ToolCollection( + Bash(), StrReplaceEditor(), Terminate() + ) + special_tool_names: List[str] = Field(default_factory=lambda: [Terminate().name]) + + max_steps: int = 20 diff --git a/app/agent/toolcall.py b/app/agent/toolcall.py new file mode 100644 index 0000000..65f31d9 --- /dev/null +++ b/app/agent/toolcall.py @@ -0,0 +1,250 @@ +import asyncio +import json +from typing import Any, List, Optional, Union + +from pydantic import Field + +from app.agent.react import ReActAgent +from app.exceptions import TokenLimitExceeded +from app.logger import logger +from app.prompt.toolcall import NEXT_STEP_PROMPT, SYSTEM_PROMPT +from app.schema import TOOL_CHOICE_TYPE, AgentState, Message, ToolCall, ToolChoice +from app.tool import CreateChatCompletion, Terminate, ToolCollection + + +TOOL_CALL_REQUIRED = "Tool calls required but none provided" + + +class ToolCallAgent(ReActAgent): + """Base agent class for handling tool/function calls with enhanced abstraction""" + + name: str = "toolcall" + description: str = "an agent that can execute tool calls." + + system_prompt: str = SYSTEM_PROMPT + next_step_prompt: str = NEXT_STEP_PROMPT + + available_tools: ToolCollection = ToolCollection( + CreateChatCompletion(), Terminate() + ) + tool_choices: TOOL_CHOICE_TYPE = ToolChoice.AUTO # type: ignore + special_tool_names: List[str] = Field(default_factory=lambda: [Terminate().name]) + + tool_calls: List[ToolCall] = Field(default_factory=list) + _current_base64_image: Optional[str] = None + + max_steps: int = 30 + max_observe: Optional[Union[int, bool]] = None + + async def think(self) -> bool: + """Process current state and decide next actions using tools""" + if self.next_step_prompt: + user_msg = Message.user_message(self.next_step_prompt) + self.messages += [user_msg] + + try: + # Get response with tool options + response = await self.llm.ask_tool( + messages=self.messages, + system_msgs=( + [Message.system_message(self.system_prompt)] + if self.system_prompt + else None + ), + tools=self.available_tools.to_params(), + tool_choice=self.tool_choices, + ) + except ValueError: + raise + except Exception as e: + # Check if this is a RetryError containing TokenLimitExceeded + if hasattr(e, "__cause__") and isinstance(e.__cause__, TokenLimitExceeded): + token_limit_error = e.__cause__ + logger.error( + f"🚨 Token limit error (from RetryError): {token_limit_error}" + ) + self.memory.add_message( + Message.assistant_message( + f"Maximum token limit reached, cannot continue execution: {str(token_limit_error)}" + ) + ) + self.state = AgentState.FINISHED + return False + raise + + self.tool_calls = tool_calls = ( + response.tool_calls if response and response.tool_calls else [] + ) + content = response.content if response and response.content else "" + + # Log response info + logger.info(f"✨ {self.name}'s thoughts: {content}") + logger.info( + f"🛠️ {self.name} selected {len(tool_calls) if tool_calls else 0} tools to use" + ) + if tool_calls: + logger.info( + f"🧰 Tools being prepared: {[call.function.name for call in tool_calls]}" + ) + logger.info(f"🔧 Tool arguments: {tool_calls[0].function.arguments}") + + try: + if response is None: + raise RuntimeError("No response received from the LLM") + + # Handle different tool_choices modes + if self.tool_choices == ToolChoice.NONE: + if tool_calls: + logger.warning( + f"🤔 Hmm, {self.name} tried to use tools when they weren't available!" + ) + if content: + self.memory.add_message(Message.assistant_message(content)) + return True + return False + + # Create and add assistant message + assistant_msg = ( + Message.from_tool_calls(content=content, tool_calls=self.tool_calls) + if self.tool_calls + else Message.assistant_message(content) + ) + self.memory.add_message(assistant_msg) + + if self.tool_choices == ToolChoice.REQUIRED and not self.tool_calls: + return True # Will be handled in act() + + # For 'auto' mode, continue with content if no commands but content exists + if self.tool_choices == ToolChoice.AUTO and not self.tool_calls: + return bool(content) + + return bool(self.tool_calls) + except Exception as e: + logger.error(f"🚨 Oops! The {self.name}'s thinking process hit a snag: {e}") + self.memory.add_message( + Message.assistant_message( + f"Error encountered while processing: {str(e)}" + ) + ) + return False + + async def act(self) -> str: + """Execute tool calls and handle their results""" + if not self.tool_calls: + if self.tool_choices == ToolChoice.REQUIRED: + raise ValueError(TOOL_CALL_REQUIRED) + + # Return last message content if no tool calls + return self.messages[-1].content or "No content or commands to execute" + + results = [] + for command in self.tool_calls: + # Reset base64_image for each tool call + self._current_base64_image = None + + result = await self.execute_tool(command) + + if self.max_observe: + result = result[: self.max_observe] + + logger.info( + f"🎯 Tool '{command.function.name}' completed its mission! Result: {result}" + ) + + # Add tool response to memory + tool_msg = Message.tool_message( + content=result, + tool_call_id=command.id, + name=command.function.name, + base64_image=self._current_base64_image, + ) + self.memory.add_message(tool_msg) + results.append(result) + + return "\n\n".join(results) + + async def execute_tool(self, command: ToolCall) -> str: + """Execute a single tool call with robust error handling""" + if not command or not command.function or not command.function.name: + return "Error: Invalid command format" + + name = command.function.name + if name not in self.available_tools.tool_map: + return f"Error: Unknown tool '{name}'" + + try: + # Parse arguments + args = json.loads(command.function.arguments or "{}") + + # Execute the tool + logger.info(f"🔧 Activating tool: '{name}'...") + result = await self.available_tools.execute(name=name, tool_input=args) + + # Handle special tools + await self._handle_special_tool(name=name, result=result) + + # Check if result is a ToolResult with base64_image + if hasattr(result, "base64_image") and result.base64_image: + # Store the base64_image for later use in tool_message + self._current_base64_image = result.base64_image + + # Format result for display (standard case) + observation = ( + f"Observed output of cmd `{name}` executed:\n{str(result)}" + if result + else f"Cmd `{name}` completed with no output" + ) + + return observation + except json.JSONDecodeError: + error_msg = f"Error parsing arguments for {name}: Invalid JSON format" + logger.error( + f"📝 Oops! The arguments for '{name}' don't make sense - invalid JSON, arguments:{command.function.arguments}" + ) + return f"Error: {error_msg}" + except Exception as e: + error_msg = f"⚠️ Tool '{name}' encountered a problem: {str(e)}" + logger.exception(error_msg) + return f"Error: {error_msg}" + + async def _handle_special_tool(self, name: str, result: Any, **kwargs): + """Handle special tool execution and state changes""" + if not self._is_special_tool(name): + return + + if self._should_finish_execution(name=name, result=result, **kwargs): + # Set agent state to finished + logger.info(f"🏁 Special tool '{name}' has completed the task!") + self.state = AgentState.FINISHED + + @staticmethod + def _should_finish_execution(**kwargs) -> bool: + """Determine if tool execution should finish the agent""" + return True + + def _is_special_tool(self, name: str) -> bool: + """Check if tool name is in special tools list""" + return name.lower() in [n.lower() for n in self.special_tool_names] + + async def cleanup(self): + """Clean up resources used by the agent's tools.""" + logger.info(f"🧹 Cleaning up resources for agent '{self.name}'...") + for tool_name, tool_instance in self.available_tools.tool_map.items(): + if hasattr(tool_instance, "cleanup") and asyncio.iscoroutinefunction( + tool_instance.cleanup + ): + try: + logger.debug(f"🧼 Cleaning up tool: {tool_name}") + await tool_instance.cleanup() + except Exception as e: + logger.error( + f"🚨 Error cleaning up tool '{tool_name}': {e}", exc_info=True + ) + logger.info(f"✨ Cleanup complete for agent '{self.name}'.") + + async def run(self, request: Optional[str] = None) -> str: + """Run the agent with cleanup when done.""" + try: + return await super().run(request) + finally: + await self.cleanup() diff --git a/app/bedrock.py b/app/bedrock.py new file mode 100644 index 0000000..2806329 --- /dev/null +++ b/app/bedrock.py @@ -0,0 +1,334 @@ +import json +import sys +import time +import uuid +from datetime import datetime +from typing import Dict, List, Literal, Optional + +import boto3 + + +# Global variables to track the current tool use ID across function calls +# Tmp solution +CURRENT_TOOLUSE_ID = None + + +# Class to handle OpenAI-style response formatting +class OpenAIResponse: + def __init__(self, data): + # Recursively convert nested dicts and lists to OpenAIResponse objects + for key, value in data.items(): + if isinstance(value, dict): + value = OpenAIResponse(value) + elif isinstance(value, list): + value = [ + OpenAIResponse(item) if isinstance(item, dict) else item + for item in value + ] + setattr(self, key, value) + + def model_dump(self, *args, **kwargs): + # Convert object to dict and add timestamp + data = self.__dict__ + data["created_at"] = datetime.now().isoformat() + return data + + +# Main client class for interacting with Amazon Bedrock +class BedrockClient: + def __init__(self): + # Initialize Bedrock client, you need to configure AWS env first + try: + self.client = boto3.client("bedrock-runtime") + self.chat = Chat(self.client) + except Exception as e: + print(f"Error initializing Bedrock client: {e}") + sys.exit(1) + + +# Chat interface class +class Chat: + def __init__(self, client): + self.completions = ChatCompletions(client) + + +# Core class handling chat completions functionality +class ChatCompletions: + def __init__(self, client): + self.client = client + + def _convert_openai_tools_to_bedrock_format(self, tools): + # Convert OpenAI function calling format to Bedrock tool format + bedrock_tools = [] + for tool in tools: + if tool.get("type") == "function": + function = tool.get("function", {}) + bedrock_tool = { + "toolSpec": { + "name": function.get("name", ""), + "description": function.get("description", ""), + "inputSchema": { + "json": { + "type": "object", + "properties": function.get("parameters", {}).get( + "properties", {} + ), + "required": function.get("parameters", {}).get( + "required", [] + ), + } + }, + } + } + bedrock_tools.append(bedrock_tool) + return bedrock_tools + + def _convert_openai_messages_to_bedrock_format(self, messages): + # Convert OpenAI message format to Bedrock message format + bedrock_messages = [] + system_prompt = [] + for message in messages: + if message.get("role") == "system": + system_prompt = [{"text": message.get("content")}] + elif message.get("role") == "user": + bedrock_message = { + "role": message.get("role", "user"), + "content": [{"text": message.get("content")}], + } + bedrock_messages.append(bedrock_message) + elif message.get("role") == "assistant": + bedrock_message = { + "role": "assistant", + "content": [{"text": message.get("content")}], + } + openai_tool_calls = message.get("tool_calls", []) + if openai_tool_calls: + bedrock_tool_use = { + "toolUseId": openai_tool_calls[0]["id"], + "name": openai_tool_calls[0]["function"]["name"], + "input": json.loads( + openai_tool_calls[0]["function"]["arguments"] + ), + } + bedrock_message["content"].append({"toolUse": bedrock_tool_use}) + global CURRENT_TOOLUSE_ID + CURRENT_TOOLUSE_ID = openai_tool_calls[0]["id"] + bedrock_messages.append(bedrock_message) + elif message.get("role") == "tool": + bedrock_message = { + "role": "user", + "content": [ + { + "toolResult": { + "toolUseId": CURRENT_TOOLUSE_ID, + "content": [{"text": message.get("content")}], + } + } + ], + } + bedrock_messages.append(bedrock_message) + else: + raise ValueError(f"Invalid role: {message.get('role')}") + return system_prompt, bedrock_messages + + def _convert_bedrock_response_to_openai_format(self, bedrock_response): + # Convert Bedrock response format to OpenAI format + content = "" + if bedrock_response.get("output", {}).get("message", {}).get("content"): + content_array = bedrock_response["output"]["message"]["content"] + content = "".join(item.get("text", "") for item in content_array) + if content == "": + content = "." + + # Handle tool calls in response + openai_tool_calls = [] + if bedrock_response.get("output", {}).get("message", {}).get("content"): + for content_item in bedrock_response["output"]["message"]["content"]: + if content_item.get("toolUse"): + bedrock_tool_use = content_item["toolUse"] + global CURRENT_TOOLUSE_ID + CURRENT_TOOLUSE_ID = bedrock_tool_use["toolUseId"] + openai_tool_call = { + "id": CURRENT_TOOLUSE_ID, + "type": "function", + "function": { + "name": bedrock_tool_use["name"], + "arguments": json.dumps(bedrock_tool_use["input"]), + }, + } + openai_tool_calls.append(openai_tool_call) + + # Construct final OpenAI format response + openai_format = { + "id": f"chatcmpl-{uuid.uuid4()}", + "created": int(time.time()), + "object": "chat.completion", + "system_fingerprint": None, + "choices": [ + { + "finish_reason": bedrock_response.get("stopReason", "end_turn"), + "index": 0, + "message": { + "content": content, + "role": bedrock_response.get("output", {}) + .get("message", {}) + .get("role", "assistant"), + "tool_calls": openai_tool_calls + if openai_tool_calls != [] + else None, + "function_call": None, + }, + } + ], + "usage": { + "completion_tokens": bedrock_response.get("usage", {}).get( + "outputTokens", 0 + ), + "prompt_tokens": bedrock_response.get("usage", {}).get( + "inputTokens", 0 + ), + "total_tokens": bedrock_response.get("usage", {}).get("totalTokens", 0), + }, + } + return OpenAIResponse(openai_format) + + async def _invoke_bedrock( + self, + model: str, + messages: List[Dict[str, str]], + max_tokens: int, + temperature: float, + tools: Optional[List[dict]] = None, + tool_choice: Literal["none", "auto", "required"] = "auto", + **kwargs, + ) -> OpenAIResponse: + # Non-streaming invocation of Bedrock model + ( + system_prompt, + bedrock_messages, + ) = self._convert_openai_messages_to_bedrock_format(messages) + response = self.client.converse( + modelId=model, + system=system_prompt, + messages=bedrock_messages, + inferenceConfig={"temperature": temperature, "maxTokens": max_tokens}, + toolConfig={"tools": tools} if tools else None, + ) + openai_response = self._convert_bedrock_response_to_openai_format(response) + return openai_response + + async def _invoke_bedrock_stream( + self, + model: str, + messages: List[Dict[str, str]], + max_tokens: int, + temperature: float, + tools: Optional[List[dict]] = None, + tool_choice: Literal["none", "auto", "required"] = "auto", + **kwargs, + ) -> OpenAIResponse: + # Streaming invocation of Bedrock model + ( + system_prompt, + bedrock_messages, + ) = self._convert_openai_messages_to_bedrock_format(messages) + response = self.client.converse_stream( + modelId=model, + system=system_prompt, + messages=bedrock_messages, + inferenceConfig={"temperature": temperature, "maxTokens": max_tokens}, + toolConfig={"tools": tools} if tools else None, + ) + + # Initialize response structure + bedrock_response = { + "output": {"message": {"role": "", "content": []}}, + "stopReason": "", + "usage": {}, + "metrics": {}, + } + bedrock_response_text = "" + bedrock_response_tool_input = "" + + # Process streaming response + stream = response.get("stream") + if stream: + for event in stream: + if event.get("messageStart", {}).get("role"): + bedrock_response["output"]["message"]["role"] = event[ + "messageStart" + ]["role"] + if event.get("contentBlockDelta", {}).get("delta", {}).get("text"): + bedrock_response_text += event["contentBlockDelta"]["delta"]["text"] + print( + event["contentBlockDelta"]["delta"]["text"], end="", flush=True + ) + if event.get("contentBlockStop", {}).get("contentBlockIndex") == 0: + bedrock_response["output"]["message"]["content"].append( + {"text": bedrock_response_text} + ) + if event.get("contentBlockStart", {}).get("start", {}).get("toolUse"): + bedrock_tool_use = event["contentBlockStart"]["start"]["toolUse"] + tool_use = { + "toolUseId": bedrock_tool_use["toolUseId"], + "name": bedrock_tool_use["name"], + } + bedrock_response["output"]["message"]["content"].append( + {"toolUse": tool_use} + ) + global CURRENT_TOOLUSE_ID + CURRENT_TOOLUSE_ID = bedrock_tool_use["toolUseId"] + if event.get("contentBlockDelta", {}).get("delta", {}).get("toolUse"): + bedrock_response_tool_input += event["contentBlockDelta"]["delta"][ + "toolUse" + ]["input"] + print( + event["contentBlockDelta"]["delta"]["toolUse"]["input"], + end="", + flush=True, + ) + if event.get("contentBlockStop", {}).get("contentBlockIndex") == 1: + bedrock_response["output"]["message"]["content"][1]["toolUse"][ + "input" + ] = json.loads(bedrock_response_tool_input) + print() + openai_response = self._convert_bedrock_response_to_openai_format( + bedrock_response + ) + return openai_response + + def create( + self, + model: str, + messages: List[Dict[str, str]], + max_tokens: int, + temperature: float, + stream: Optional[bool] = True, + tools: Optional[List[dict]] = None, + tool_choice: Literal["none", "auto", "required"] = "auto", + **kwargs, + ) -> OpenAIResponse: + # Main entry point for chat completion + bedrock_tools = [] + if tools is not None: + bedrock_tools = self._convert_openai_tools_to_bedrock_format(tools) + if stream: + return self._invoke_bedrock_stream( + model, + messages, + max_tokens, + temperature, + bedrock_tools, + tool_choice, + **kwargs, + ) + else: + return self._invoke_bedrock( + model, + messages, + max_tokens, + temperature, + bedrock_tools, + tool_choice, + **kwargs, + ) diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..a881e2a --- /dev/null +++ b/app/config.py @@ -0,0 +1,372 @@ +import json +import threading +import tomllib +from pathlib import Path +from typing import Dict, List, Optional + +from pydantic import BaseModel, Field + + +def get_project_root() -> Path: + """Get the project root directory""" + return Path(__file__).resolve().parent.parent + + +PROJECT_ROOT = get_project_root() +WORKSPACE_ROOT = PROJECT_ROOT / "workspace" + + +class LLMSettings(BaseModel): + model: str = Field(..., description="Model name") + base_url: str = Field(..., description="API base URL") + api_key: str = Field(..., description="API key") + max_tokens: int = Field(4096, description="Maximum number of tokens per request") + max_input_tokens: Optional[int] = Field( + None, + description="Maximum input tokens to use across all requests (None for unlimited)", + ) + temperature: float = Field(1.0, description="Sampling temperature") + api_type: str = Field(..., description="Azure, Openai, or Ollama") + api_version: str = Field(..., description="Azure Openai version if AzureOpenai") + + +class ProxySettings(BaseModel): + server: str = Field(None, description="Proxy server address") + username: Optional[str] = Field(None, description="Proxy username") + password: Optional[str] = Field(None, description="Proxy password") + + +class SearchSettings(BaseModel): + engine: str = Field(default="Google", description="Search engine the llm to use") + fallback_engines: List[str] = Field( + default_factory=lambda: ["DuckDuckGo", "Baidu", "Bing"], + description="Fallback search engines to try if the primary engine fails", + ) + retry_delay: int = Field( + default=60, + description="Seconds to wait before retrying all engines again after they all fail", + ) + max_retries: int = Field( + default=3, + description="Maximum number of times to retry all engines when all fail", + ) + lang: str = Field( + default="en", + description="Language code for search results (e.g., en, zh, fr)", + ) + country: str = Field( + default="us", + description="Country code for search results (e.g., us, cn, uk)", + ) + + +class RunflowSettings(BaseModel): + use_data_analysis_agent: bool = Field( + default=False, description="Enable data analysis agent in run flow" + ) + + +class BrowserSettings(BaseModel): + headless: bool = Field(False, description="Whether to run browser in headless mode") + disable_security: bool = Field( + True, description="Disable browser security features" + ) + extra_chromium_args: List[str] = Field( + default_factory=list, description="Extra arguments to pass to the browser" + ) + chrome_instance_path: Optional[str] = Field( + None, description="Path to a Chrome instance to use" + ) + wss_url: Optional[str] = Field( + None, description="Connect to a browser instance via WebSocket" + ) + cdp_url: Optional[str] = Field( + None, description="Connect to a browser instance via CDP" + ) + proxy: Optional[ProxySettings] = Field( + None, description="Proxy settings for the browser" + ) + max_content_length: int = Field( + 2000, description="Maximum length for content retrieval operations" + ) + + +class SandboxSettings(BaseModel): + """Configuration for the execution sandbox""" + + use_sandbox: bool = Field(False, description="Whether to use the sandbox") + image: str = Field("python:3.12-slim", description="Base image") + work_dir: str = Field("/workspace", description="Container working directory") + memory_limit: str = Field("512m", description="Memory limit") + cpu_limit: float = Field(1.0, description="CPU limit") + timeout: int = Field(300, description="Default command timeout (seconds)") + network_enabled: bool = Field( + False, description="Whether network access is allowed" + ) + + +class DaytonaSettings(BaseModel): + daytona_api_key: str + daytona_server_url: Optional[str] = Field( + "https://app.daytona.io/api", description="" + ) + daytona_target: Optional[str] = Field("us", description="enum ['eu', 'us']") + sandbox_image_name: Optional[str] = Field("whitezxj/sandbox:0.1.0", description="") + sandbox_entrypoint: Optional[str] = Field( + "/usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf", + description="", + ) + # sandbox_id: Optional[str] = Field( + # None, description="ID of the daytona sandbox to use, if any" + # ) + VNC_password: Optional[str] = Field( + "123456", description="VNC password for the vnc service in sandbox" + ) + + +class MCPServerConfig(BaseModel): + """Configuration for a single MCP server""" + + type: str = Field(..., description="Server connection type (sse or stdio)") + url: Optional[str] = Field(None, description="Server URL for SSE connections") + command: Optional[str] = Field(None, description="Command for stdio connections") + args: List[str] = Field( + default_factory=list, description="Arguments for stdio command" + ) + + +class MCPSettings(BaseModel): + """Configuration for MCP (Model Context Protocol)""" + + server_reference: str = Field( + "app.mcp.server", description="Module reference for the MCP server" + ) + servers: Dict[str, MCPServerConfig] = Field( + default_factory=dict, description="MCP server configurations" + ) + + @classmethod + def load_server_config(cls) -> Dict[str, MCPServerConfig]: + """Load MCP server configuration from JSON file""" + config_path = PROJECT_ROOT / "config" / "mcp.json" + + try: + config_file = config_path if config_path.exists() else None + if not config_file: + return {} + + with config_file.open() as f: + data = json.load(f) + servers = {} + + for server_id, server_config in data.get("mcpServers", {}).items(): + servers[server_id] = MCPServerConfig( + type=server_config["type"], + url=server_config.get("url"), + command=server_config.get("command"), + args=server_config.get("args", []), + ) + return servers + except Exception as e: + raise ValueError(f"Failed to load MCP server config: {e}") + + +class AppConfig(BaseModel): + llm: Dict[str, LLMSettings] + sandbox: Optional[SandboxSettings] = Field( + None, description="Sandbox configuration" + ) + browser_config: Optional[BrowserSettings] = Field( + None, description="Browser configuration" + ) + search_config: Optional[SearchSettings] = Field( + None, description="Search configuration" + ) + mcp_config: Optional[MCPSettings] = Field(None, description="MCP configuration") + run_flow_config: Optional[RunflowSettings] = Field( + None, description="Run flow configuration" + ) + daytona_config: Optional[DaytonaSettings] = Field( + None, description="Daytona configuration" + ) + + class Config: + arbitrary_types_allowed = True + + +class Config: + _instance = None + _lock = threading.Lock() + _initialized = False + + def __new__(cls): + if cls._instance is None: + with cls._lock: + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + if not self._initialized: + with self._lock: + if not self._initialized: + self._config = None + self._load_initial_config() + self._initialized = True + + @staticmethod + def _get_config_path() -> Path: + root = PROJECT_ROOT + config_path = root / "config" / "config.toml" + if config_path.exists(): + return config_path + example_path = root / "config" / "config.example.toml" + if example_path.exists(): + return example_path + raise FileNotFoundError("No configuration file found in config directory") + + def _load_config(self) -> dict: + config_path = self._get_config_path() + with config_path.open("rb") as f: + return tomllib.load(f) + + def _load_initial_config(self): + raw_config = self._load_config() + base_llm = raw_config.get("llm", {}) + llm_overrides = { + k: v for k, v in raw_config.get("llm", {}).items() if isinstance(v, dict) + } + + default_settings = { + "model": base_llm.get("model"), + "base_url": base_llm.get("base_url"), + "api_key": base_llm.get("api_key"), + "max_tokens": base_llm.get("max_tokens", 4096), + "max_input_tokens": base_llm.get("max_input_tokens"), + "temperature": base_llm.get("temperature", 1.0), + "api_type": base_llm.get("api_type", ""), + "api_version": base_llm.get("api_version", ""), + } + + # handle browser config. + browser_config = raw_config.get("browser", {}) + browser_settings = None + + if browser_config: + # handle proxy settings. + proxy_config = browser_config.get("proxy", {}) + proxy_settings = None + + if proxy_config and proxy_config.get("server"): + proxy_settings = ProxySettings( + **{ + k: v + for k, v in proxy_config.items() + if k in ["server", "username", "password"] and v + } + ) + + # filter valid browser config parameters. + valid_browser_params = { + k: v + for k, v in browser_config.items() + if k in BrowserSettings.__annotations__ and v is not None + } + + # if there is proxy settings, add it to the parameters. + if proxy_settings: + valid_browser_params["proxy"] = proxy_settings + + # only create BrowserSettings when there are valid parameters. + if valid_browser_params: + browser_settings = BrowserSettings(**valid_browser_params) + + search_config = raw_config.get("search", {}) + search_settings = None + if search_config: + search_settings = SearchSettings(**search_config) + sandbox_config = raw_config.get("sandbox", {}) + if sandbox_config: + sandbox_settings = SandboxSettings(**sandbox_config) + else: + sandbox_settings = SandboxSettings() + daytona_config = raw_config.get("daytona", {}) + if daytona_config: + daytona_settings = DaytonaSettings(**daytona_config) + else: + daytona_settings = DaytonaSettings() + + mcp_config = raw_config.get("mcp", {}) + mcp_settings = None + if mcp_config: + # Load server configurations from JSON + mcp_config["servers"] = MCPSettings.load_server_config() + mcp_settings = MCPSettings(**mcp_config) + else: + mcp_settings = MCPSettings(servers=MCPSettings.load_server_config()) + + run_flow_config = raw_config.get("runflow") + if run_flow_config: + run_flow_settings = RunflowSettings(**run_flow_config) + else: + run_flow_settings = RunflowSettings() + config_dict = { + "llm": { + "default": default_settings, + **{ + name: {**default_settings, **override_config} + for name, override_config in llm_overrides.items() + }, + }, + "sandbox": sandbox_settings, + "browser_config": browser_settings, + "search_config": search_settings, + "mcp_config": mcp_settings, + "run_flow_config": run_flow_settings, + "daytona_config": daytona_settings, + } + + self._config = AppConfig(**config_dict) + + @property + def llm(self) -> Dict[str, LLMSettings]: + return self._config.llm + + @property + def sandbox(self) -> SandboxSettings: + return self._config.sandbox + + @property + def daytona(self) -> DaytonaSettings: + return self._config.daytona_config + + @property + def browser_config(self) -> Optional[BrowserSettings]: + return self._config.browser_config + + @property + def search_config(self) -> Optional[SearchSettings]: + return self._config.search_config + + @property + def mcp_config(self) -> MCPSettings: + """Get the MCP configuration""" + return self._config.mcp_config + + @property + def run_flow_config(self) -> RunflowSettings: + """Get the Run Flow configuration""" + return self._config.run_flow_config + + @property + def workspace_root(self) -> Path: + """Get the workspace root directory""" + return WORKSPACE_ROOT + + @property + def root_path(self) -> Path: + """Get the root path of the application""" + return PROJECT_ROOT + + +config = Config() diff --git a/app/daytona/README.md b/app/daytona/README.md new file mode 100644 index 0000000..6f80206 --- /dev/null +++ b/app/daytona/README.md @@ -0,0 +1,57 @@ +# Agent with Daytona sandbox + + + + +## Prerequisites +- conda activate 'Your OpenManus python env' +- pip install daytona==0.21.8 structlog==25.4.0 + + + +## Setup & Running + +1. daytona config : + ```bash + cd OpenManus + cp config/config.example-daytona.toml config/config.toml + ``` +2. get daytona apikey : + goto https://app.daytona.io/dashboard/keys and create your apikey + +3. set your apikey in config.toml + ```toml + # daytona config + [daytona] + daytona_api_key = "" + #daytona_server_url = "https://app.daytona.io/api" + #daytona_target = "us" #Daytona is currently available in the following regions:United States (us)、Europe (eu) + #sandbox_image_name = "whitezxj/sandbox:0.1.0" #If you don't use this default image,sandbox tools may be useless + #sandbox_entrypoint = "/usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf" #If you change this entrypoint,server in sandbox may be useless + #VNC_password = #The password you set to log in sandbox by VNC,it will be 123456 if you don't set + ``` +2. Run : + + ```bash + cd OpenManus + python sandbox_main.py + ``` + +3. Send tasks to Agent + You can sent tasks to Agent by terminate,agent will use sandbox tools to handle your tasks. + +4. See results + If agent use sb_browser_use tool, you can see the operations by VNC link, The VNC link will print in the termination,e.g.:https://6080-sandbox-123456.h7890.daytona.work. + If agent use sb_shell tool, you can see the results by terminate of sandbox in https://app.daytona.io/dashboard/sandboxes. + Agent can use sb_files tool to operate files to sandbox. + + +## Example + + You can send task e.g.:"帮我在https://hk.trip.com/travel-guide/guidebook/nanjing-9696/?ishideheader=true&isHideNavBar=YES&disableFontScaling=1&catalogId=514634&locale=zh-HK查询相关信息上制定一份南京旅游攻略,并在工作区保存为index.html" + + Then you can see the agent's browser action in VNC link(https://6080-sandbox-123456.h7890.proxy.daytona.work) and you can see the html made by agent in Website URL(https://8080-sandbox-123456.h7890.proxy.daytona.work). + +## Learn More + +- [Daytona Documentation](https://www.daytona.io/docs/) diff --git a/app/daytona/sandbox.py b/app/daytona/sandbox.py new file mode 100644 index 0000000..8970b9c --- /dev/null +++ b/app/daytona/sandbox.py @@ -0,0 +1,165 @@ +import time + +from daytona import ( + CreateSandboxFromImageParams, + Daytona, + DaytonaConfig, + Resources, + Sandbox, + SandboxState, + SessionExecuteRequest, +) + +from app.config import config +from app.utils.logger import logger + + +# load_dotenv() +daytona_settings = config.daytona +logger.info("Initializing Daytona sandbox configuration") +daytona_config = DaytonaConfig( + api_key=daytona_settings.daytona_api_key, + server_url=daytona_settings.daytona_server_url, + target=daytona_settings.daytona_target, +) + +if daytona_config.api_key: + logger.info("Daytona API key configured successfully") +else: + logger.warning("No Daytona API key found in environment variables") + +if daytona_config.server_url: + logger.info(f"Daytona server URL set to: {daytona_config.server_url}") +else: + logger.warning("No Daytona server URL found in environment variables") + +if daytona_config.target: + logger.info(f"Daytona target set to: {daytona_config.target}") +else: + logger.warning("No Daytona target found in environment variables") + +daytona = Daytona(daytona_config) +logger.info("Daytona client initialized") + + +async def get_or_start_sandbox(sandbox_id: str): + """Retrieve a sandbox by ID, check its state, and start it if needed.""" + + logger.info(f"Getting or starting sandbox with ID: {sandbox_id}") + + try: + sandbox = daytona.get(sandbox_id) + + # Check if sandbox needs to be started + if ( + sandbox.state == SandboxState.ARCHIVED + or sandbox.state == SandboxState.STOPPED + ): + logger.info(f"Sandbox is in {sandbox.state} state. Starting...") + try: + daytona.start(sandbox) + # Wait a moment for the sandbox to initialize + # sleep(5) + # Refresh sandbox state after starting + sandbox = daytona.get(sandbox_id) + + # Start supervisord in a session when restarting + start_supervisord_session(sandbox) + except Exception as e: + logger.error(f"Error starting sandbox: {e}") + raise e + + logger.info(f"Sandbox {sandbox_id} is ready") + return sandbox + + except Exception as e: + logger.error(f"Error retrieving or starting sandbox: {str(e)}") + raise e + + +def start_supervisord_session(sandbox: Sandbox): + """Start supervisord in a session.""" + session_id = "supervisord-session" + try: + logger.info(f"Creating session {session_id} for supervisord") + sandbox.process.create_session(session_id) + + # Execute supervisord command + sandbox.process.execute_session_command( + session_id, + SessionExecuteRequest( + command="exec /usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf", + var_async=True, + ), + ) + time.sleep(25) # Wait a bit to ensure supervisord starts properly + logger.info(f"Supervisord started in session {session_id}") + except Exception as e: + logger.error(f"Error starting supervisord session: {str(e)}") + raise e + + +def create_sandbox(password: str, project_id: str = None): + """Create a new sandbox with all required services configured and running.""" + + logger.info("Creating new Daytona sandbox environment") + logger.info("Configuring sandbox with browser-use image and environment variables") + + labels = None + if project_id: + logger.info(f"Using sandbox_id as label: {project_id}") + labels = {"id": project_id} + + params = CreateSandboxFromImageParams( + image=daytona_settings.sandbox_image_name, + public=True, + labels=labels, + env_vars={ + "CHROME_PERSISTENT_SESSION": "true", + "RESOLUTION": "1024x768x24", + "RESOLUTION_WIDTH": "1024", + "RESOLUTION_HEIGHT": "768", + "VNC_PASSWORD": password, + "ANONYMIZED_TELEMETRY": "false", + "CHROME_PATH": "", + "CHROME_USER_DATA": "", + "CHROME_DEBUGGING_PORT": "9222", + "CHROME_DEBUGGING_HOST": "localhost", + "CHROME_CDP": "", + }, + resources=Resources( + cpu=2, + memory=4, + disk=5, + ), + auto_stop_interval=15, + auto_archive_interval=24 * 60, + ) + + # Create the sandbox + sandbox = daytona.create(params) + logger.info(f"Sandbox created with ID: {sandbox.id}") + + # Start supervisord in a session for new sandbox + start_supervisord_session(sandbox) + + logger.info(f"Sandbox environment successfully initialized") + return sandbox + + +async def delete_sandbox(sandbox_id: str): + """Delete a sandbox by its ID.""" + logger.info(f"Deleting sandbox with ID: {sandbox_id}") + + try: + # Get the sandbox + sandbox = daytona.get(sandbox_id) + + # Delete the sandbox + daytona.delete(sandbox) + + logger.info(f"Successfully deleted sandbox {sandbox_id}") + return True + except Exception as e: + logger.error(f"Error deleting sandbox {sandbox_id}: {str(e)}") + raise e diff --git a/app/daytona/tool_base.py b/app/daytona/tool_base.py new file mode 100644 index 0000000..043578a --- /dev/null +++ b/app/daytona/tool_base.py @@ -0,0 +1,138 @@ +from dataclasses import dataclass, field +from datetime import datetime +from typing import Any, ClassVar, Dict, Optional + +from daytona import Daytona, DaytonaConfig, Sandbox, SandboxState +from pydantic import Field + +from app.config import config +from app.daytona.sandbox import create_sandbox, start_supervisord_session +from app.tool.base import BaseTool +from app.utils.files_utils import clean_path +from app.utils.logger import logger + + +# load_dotenv() +daytona_settings = config.daytona +daytona_config = DaytonaConfig( + api_key=daytona_settings.daytona_api_key, + server_url=daytona_settings.daytona_server_url, + target=daytona_settings.daytona_target, +) +daytona = Daytona(daytona_config) + + +@dataclass +class ThreadMessage: + """ + Represents a message to be added to a thread. + """ + + type: str + content: Dict[str, Any] + is_llm_message: bool = False + metadata: Optional[Dict[str, Any]] = None + timestamp: Optional[float] = field( + default_factory=lambda: datetime.now().timestamp() + ) + + def to_dict(self) -> Dict[str, Any]: + """Convert the message to a dictionary for API calls""" + return { + "type": self.type, + "content": self.content, + "is_llm_message": self.is_llm_message, + "metadata": self.metadata or {}, + "timestamp": self.timestamp, + } + + +class SandboxToolsBase(BaseTool): + """Base class for all sandbox tools that provides project-based sandbox access.""" + + # Class variable to track if sandbox URLs have been printed + _urls_printed: ClassVar[bool] = False + + # Required fields + project_id: Optional[str] = None + # thread_manager: Optional[ThreadManager] = None + + # Private fields (not part of the model schema) + _sandbox: Optional[Sandbox] = None + _sandbox_id: Optional[str] = None + _sandbox_pass: Optional[str] = None + workspace_path: str = Field(default="/workspace", exclude=True) + _sessions: dict[str, str] = {} + + class Config: + arbitrary_types_allowed = True # Allow non-pydantic types like ThreadManager + underscore_attrs_are_private = True + + async def _ensure_sandbox(self) -> Sandbox: + """Ensure we have a valid sandbox instance, retrieving it from the project if needed.""" + if self._sandbox is None: + # Get or start the sandbox + try: + self._sandbox = create_sandbox(password=config.daytona.VNC_password) + # Log URLs if not already printed + if not SandboxToolsBase._urls_printed: + vnc_link = self._sandbox.get_preview_link(6080) + website_link = self._sandbox.get_preview_link(8080) + + vnc_url = ( + vnc_link.url if hasattr(vnc_link, "url") else str(vnc_link) + ) + website_url = ( + website_link.url + if hasattr(website_link, "url") + else str(website_link) + ) + + print("\033[95m***") + print(f"VNC URL: {vnc_url}") + print(f"Website URL: {website_url}") + print("***\033[0m") + SandboxToolsBase._urls_printed = True + except Exception as e: + logger.error(f"Error retrieving or starting sandbox: {str(e)}") + raise e + else: + if ( + self._sandbox.state == SandboxState.ARCHIVED + or self._sandbox.state == SandboxState.STOPPED + ): + logger.info(f"Sandbox is in {self._sandbox.state} state. Starting...") + try: + daytona.start(self._sandbox) + # Wait a moment for the sandbox to initialize + # sleep(5) + # Refresh sandbox state after starting + + # Start supervisord in a session when restarting + start_supervisord_session(self._sandbox) + except Exception as e: + logger.error(f"Error starting sandbox: {e}") + raise e + return self._sandbox + + @property + def sandbox(self) -> Sandbox: + """Get the sandbox instance, ensuring it exists.""" + if self._sandbox is None: + raise RuntimeError("Sandbox not initialized. Call _ensure_sandbox() first.") + return self._sandbox + + @property + def sandbox_id(self) -> str: + """Get the sandbox ID, ensuring it exists.""" + if self._sandbox_id is None: + raise RuntimeError( + "Sandbox ID not initialized. Call _ensure_sandbox() first." + ) + return self._sandbox_id + + def clean_path(self, path: str) -> str: + """Clean and normalize a path to be relative to /workspace.""" + cleaned_path = clean_path(path, self.workspace_path) + logger.debug(f"Cleaned path: {path} -> {cleaned_path}") + return cleaned_path diff --git a/app/exceptions.py b/app/exceptions.py new file mode 100644 index 0000000..fc90087 --- /dev/null +++ b/app/exceptions.py @@ -0,0 +1,13 @@ +class ToolError(Exception): + """Raised when a tool encounters an error.""" + + def __init__(self, message): + self.message = message + + +class OpenManusError(Exception): + """Base exception for all OpenManus errors""" + + +class TokenLimitExceeded(OpenManusError): + """Exception raised when the token limit is exceeded""" diff --git a/app/flow/__init__.py b/app/flow/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/flow/base.py b/app/flow/base.py new file mode 100644 index 0000000..dc57b39 --- /dev/null +++ b/app/flow/base.py @@ -0,0 +1,57 @@ +from abc import ABC, abstractmethod +from typing import Dict, List, Optional, Union + +from pydantic import BaseModel + +from app.agent.base import BaseAgent + + +class BaseFlow(BaseModel, ABC): + """Base class for execution flows supporting multiple agents""" + + agents: Dict[str, BaseAgent] + tools: Optional[List] = None + primary_agent_key: Optional[str] = None + + class Config: + arbitrary_types_allowed = True + + def __init__( + self, agents: Union[BaseAgent, List[BaseAgent], Dict[str, BaseAgent]], **data + ): + # Handle different ways of providing agents + if isinstance(agents, BaseAgent): + agents_dict = {"default": agents} + elif isinstance(agents, list): + agents_dict = {f"agent_{i}": agent for i, agent in enumerate(agents)} + else: + agents_dict = agents + + # If primary agent not specified, use first agent + primary_key = data.get("primary_agent_key") + if not primary_key and agents_dict: + primary_key = next(iter(agents_dict)) + data["primary_agent_key"] = primary_key + + # Set the agents dictionary + data["agents"] = agents_dict + + # Initialize using BaseModel's init + super().__init__(**data) + + @property + def primary_agent(self) -> Optional[BaseAgent]: + """Get the primary agent for the flow""" + return self.agents.get(self.primary_agent_key) + + def get_agent(self, key: str) -> Optional[BaseAgent]: + """Get a specific agent by key""" + return self.agents.get(key) + + def add_agent(self, key: str, agent: BaseAgent) -> None: + """Add a new agent to the flow""" + self.agents[key] = agent + + @abstractmethod + async def execute(self, input_text: str) -> str: + """Execute the flow with given input""" diff --git a/app/flow/flow_factory.py b/app/flow/flow_factory.py new file mode 100644 index 0000000..2936940 --- /dev/null +++ b/app/flow/flow_factory.py @@ -0,0 +1,30 @@ +from enum import Enum +from typing import Dict, List, Union + +from app.agent.base import BaseAgent +from app.flow.base import BaseFlow +from app.flow.planning import PlanningFlow + + +class FlowType(str, Enum): + PLANNING = "planning" + + +class FlowFactory: + """Factory for creating different types of flows with support for multiple agents""" + + @staticmethod + def create_flow( + flow_type: FlowType, + agents: Union[BaseAgent, List[BaseAgent], Dict[str, BaseAgent]], + **kwargs, + ) -> BaseFlow: + flows = { + FlowType.PLANNING: PlanningFlow, + } + + flow_class = flows.get(flow_type) + if not flow_class: + raise ValueError(f"Unknown flow type: {flow_type}") + + return flow_class(agents, **kwargs) diff --git a/app/flow/planning.py b/app/flow/planning.py new file mode 100644 index 0000000..b60596a --- /dev/null +++ b/app/flow/planning.py @@ -0,0 +1,442 @@ +import json +import time +from enum import Enum +from typing import Dict, List, Optional, Union + +from pydantic import Field + +from app.agent.base import BaseAgent +from app.flow.base import BaseFlow +from app.llm import LLM +from app.logger import logger +from app.schema import AgentState, Message, ToolChoice +from app.tool import PlanningTool + + +class PlanStepStatus(str, Enum): + """Enum class defining possible statuses of a plan step""" + + NOT_STARTED = "not_started" + IN_PROGRESS = "in_progress" + COMPLETED = "completed" + BLOCKED = "blocked" + + @classmethod + def get_all_statuses(cls) -> list[str]: + """Return a list of all possible step status values""" + return [status.value for status in cls] + + @classmethod + def get_active_statuses(cls) -> list[str]: + """Return a list of values representing active statuses (not started or in progress)""" + return [cls.NOT_STARTED.value, cls.IN_PROGRESS.value] + + @classmethod + def get_status_marks(cls) -> Dict[str, str]: + """Return a mapping of statuses to their marker symbols""" + return { + cls.COMPLETED.value: "[✓]", + cls.IN_PROGRESS.value: "[→]", + cls.BLOCKED.value: "[!]", + cls.NOT_STARTED.value: "[ ]", + } + + +class PlanningFlow(BaseFlow): + """A flow that manages planning and execution of tasks using agents.""" + + llm: LLM = Field(default_factory=lambda: LLM()) + planning_tool: PlanningTool = Field(default_factory=PlanningTool) + executor_keys: List[str] = Field(default_factory=list) + active_plan_id: str = Field(default_factory=lambda: f"plan_{int(time.time())}") + current_step_index: Optional[int] = None + + def __init__( + self, agents: Union[BaseAgent, List[BaseAgent], Dict[str, BaseAgent]], **data + ): + # Set executor keys before super().__init__ + if "executors" in data: + data["executor_keys"] = data.pop("executors") + + # Set plan ID if provided + if "plan_id" in data: + data["active_plan_id"] = data.pop("plan_id") + + # Initialize the planning tool if not provided + if "planning_tool" not in data: + planning_tool = PlanningTool() + data["planning_tool"] = planning_tool + + # Call parent's init with the processed data + super().__init__(agents, **data) + + # Set executor_keys to all agent keys if not specified + if not self.executor_keys: + self.executor_keys = list(self.agents.keys()) + + def get_executor(self, step_type: Optional[str] = None) -> BaseAgent: + """ + Get an appropriate executor agent for the current step. + Can be extended to select agents based on step type/requirements. + """ + # If step type is provided and matches an agent key, use that agent + if step_type and step_type in self.agents: + return self.agents[step_type] + + # Otherwise use the first available executor or fall back to primary agent + for key in self.executor_keys: + if key in self.agents: + return self.agents[key] + + # Fallback to primary agent + return self.primary_agent + + async def execute(self, input_text: str) -> str: + """Execute the planning flow with agents.""" + try: + if not self.primary_agent: + raise ValueError("No primary agent available") + + # Create initial plan if input provided + if input_text: + await self._create_initial_plan(input_text) + + # Verify plan was created successfully + if self.active_plan_id not in self.planning_tool.plans: + logger.error( + f"Plan creation failed. Plan ID {self.active_plan_id} not found in planning tool." + ) + return f"Failed to create plan for: {input_text}" + + result = "" + while True: + # Get current step to execute + self.current_step_index, step_info = await self._get_current_step_info() + + # Exit if no more steps or plan completed + if self.current_step_index is None: + result += await self._finalize_plan() + break + + # Execute current step with appropriate agent + step_type = step_info.get("type") if step_info else None + executor = self.get_executor(step_type) + step_result = await self._execute_step(executor, step_info) + result += step_result + "\n" + + # Check if agent wants to terminate + if hasattr(executor, "state") and executor.state == AgentState.FINISHED: + break + + return result + except Exception as e: + logger.error(f"Error in PlanningFlow: {str(e)}") + return f"Execution failed: {str(e)}" + + async def _create_initial_plan(self, request: str) -> None: + """Create an initial plan based on the request using the flow's LLM and PlanningTool.""" + logger.info(f"Creating initial plan with ID: {self.active_plan_id}") + + system_message_content = ( + "You are a planning assistant. Create a concise, actionable plan with clear steps. " + "Focus on key milestones rather than detailed sub-steps. " + "Optimize for clarity and efficiency." + ) + agents_description = [] + for key in self.executor_keys: + if key in self.agents: + agents_description.append( + { + "name": key.upper(), + "description": self.agents[key].description, + } + ) + if len(agents_description) > 1: + # Add description of agents to select + system_message_content += ( + f"\nNow we have {agents_description} agents. " + f"The infomation of them are below: {json.dumps(agents_description)}\n" + "When creating steps in the planning tool, please specify the agent names using the format '[agent_name]'." + ) + + # Create a system message for plan creation + system_message = Message.system_message(system_message_content) + + # Create a user message with the request + user_message = Message.user_message( + f"Create a reasonable plan with clear steps to accomplish the task: {request}" + ) + + # Call LLM with PlanningTool + response = await self.llm.ask_tool( + messages=[user_message], + system_msgs=[system_message], + tools=[self.planning_tool.to_param()], + tool_choice=ToolChoice.AUTO, + ) + + # Process tool calls if present + if response.tool_calls: + for tool_call in response.tool_calls: + if tool_call.function.name == "planning": + # Parse the arguments + args = tool_call.function.arguments + if isinstance(args, str): + try: + args = json.loads(args) + except json.JSONDecodeError: + logger.error(f"Failed to parse tool arguments: {args}") + continue + + # Ensure plan_id is set correctly and execute the tool + args["plan_id"] = self.active_plan_id + + # Execute the tool via ToolCollection instead of directly + result = await self.planning_tool.execute(**args) + + logger.info(f"Plan creation result: {str(result)}") + return + + # If execution reached here, create a default plan + logger.warning("Creating default plan") + + # Create default plan using the ToolCollection + await self.planning_tool.execute( + **{ + "command": "create", + "plan_id": self.active_plan_id, + "title": f"Plan for: {request[:50]}{'...' if len(request) > 50 else ''}", + "steps": ["Analyze request", "Execute task", "Verify results"], + } + ) + + async def _get_current_step_info(self) -> tuple[Optional[int], Optional[dict]]: + """ + Parse the current plan to identify the first non-completed step's index and info. + Returns (None, None) if no active step is found. + """ + if ( + not self.active_plan_id + or self.active_plan_id not in self.planning_tool.plans + ): + logger.error(f"Plan with ID {self.active_plan_id} not found") + return None, None + + try: + # Direct access to plan data from planning tool storage + plan_data = self.planning_tool.plans[self.active_plan_id] + steps = plan_data.get("steps", []) + step_statuses = plan_data.get("step_statuses", []) + + # Find first non-completed step + for i, step in enumerate(steps): + if i >= len(step_statuses): + status = PlanStepStatus.NOT_STARTED.value + else: + status = step_statuses[i] + + if status in PlanStepStatus.get_active_statuses(): + # Extract step type/category if available + step_info = {"text": step} + + # Try to extract step type from the text (e.g., [SEARCH] or [CODE]) + import re + + type_match = re.search(r"\[([A-Z_]+)\]", step) + if type_match: + step_info["type"] = type_match.group(1).lower() + + # Mark current step as in_progress + try: + await self.planning_tool.execute( + command="mark_step", + plan_id=self.active_plan_id, + step_index=i, + step_status=PlanStepStatus.IN_PROGRESS.value, + ) + except Exception as e: + logger.warning(f"Error marking step as in_progress: {e}") + # Update step status directly if needed + if i < len(step_statuses): + step_statuses[i] = PlanStepStatus.IN_PROGRESS.value + else: + while len(step_statuses) < i: + step_statuses.append(PlanStepStatus.NOT_STARTED.value) + step_statuses.append(PlanStepStatus.IN_PROGRESS.value) + + plan_data["step_statuses"] = step_statuses + + return i, step_info + + return None, None # No active step found + + except Exception as e: + logger.warning(f"Error finding current step index: {e}") + return None, None + + async def _execute_step(self, executor: BaseAgent, step_info: dict) -> str: + """Execute the current step with the specified agent using agent.run().""" + # Prepare context for the agent with current plan status + plan_status = await self._get_plan_text() + step_text = step_info.get("text", f"Step {self.current_step_index}") + + # Create a prompt for the agent to execute the current step + step_prompt = f""" + CURRENT PLAN STATUS: + {plan_status} + + YOUR CURRENT TASK: + You are now working on step {self.current_step_index}: "{step_text}" + + Please only execute this current step using the appropriate tools. When you're done, provide a summary of what you accomplished. + """ + + # Use agent.run() to execute the step + try: + step_result = await executor.run(step_prompt) + + # Mark the step as completed after successful execution + await self._mark_step_completed() + + return step_result + except Exception as e: + logger.error(f"Error executing step {self.current_step_index}: {e}") + return f"Error executing step {self.current_step_index}: {str(e)}" + + async def _mark_step_completed(self) -> None: + """Mark the current step as completed.""" + if self.current_step_index is None: + return + + try: + # Mark the step as completed + await self.planning_tool.execute( + command="mark_step", + plan_id=self.active_plan_id, + step_index=self.current_step_index, + step_status=PlanStepStatus.COMPLETED.value, + ) + logger.info( + f"Marked step {self.current_step_index} as completed in plan {self.active_plan_id}" + ) + except Exception as e: + logger.warning(f"Failed to update plan status: {e}") + # Update step status directly in planning tool storage + if self.active_plan_id in self.planning_tool.plans: + plan_data = self.planning_tool.plans[self.active_plan_id] + step_statuses = plan_data.get("step_statuses", []) + + # Ensure the step_statuses list is long enough + while len(step_statuses) <= self.current_step_index: + step_statuses.append(PlanStepStatus.NOT_STARTED.value) + + # Update the status + step_statuses[self.current_step_index] = PlanStepStatus.COMPLETED.value + plan_data["step_statuses"] = step_statuses + + async def _get_plan_text(self) -> str: + """Get the current plan as formatted text.""" + try: + result = await self.planning_tool.execute( + command="get", plan_id=self.active_plan_id + ) + return result.output if hasattr(result, "output") else str(result) + except Exception as e: + logger.error(f"Error getting plan: {e}") + return self._generate_plan_text_from_storage() + + def _generate_plan_text_from_storage(self) -> str: + """Generate plan text directly from storage if the planning tool fails.""" + try: + if self.active_plan_id not in self.planning_tool.plans: + return f"Error: Plan with ID {self.active_plan_id} not found" + + plan_data = self.planning_tool.plans[self.active_plan_id] + title = plan_data.get("title", "Untitled Plan") + steps = plan_data.get("steps", []) + step_statuses = plan_data.get("step_statuses", []) + step_notes = plan_data.get("step_notes", []) + + # Ensure step_statuses and step_notes match the number of steps + while len(step_statuses) < len(steps): + step_statuses.append(PlanStepStatus.NOT_STARTED.value) + while len(step_notes) < len(steps): + step_notes.append("") + + # Count steps by status + status_counts = {status: 0 for status in PlanStepStatus.get_all_statuses()} + + for status in step_statuses: + if status in status_counts: + status_counts[status] += 1 + + completed = status_counts[PlanStepStatus.COMPLETED.value] + total = len(steps) + progress = (completed / total) * 100 if total > 0 else 0 + + plan_text = f"Plan: {title} (ID: {self.active_plan_id})\n" + plan_text += "=" * len(plan_text) + "\n\n" + + plan_text += ( + f"Progress: {completed}/{total} steps completed ({progress:.1f}%)\n" + ) + plan_text += f"Status: {status_counts[PlanStepStatus.COMPLETED.value]} completed, {status_counts[PlanStepStatus.IN_PROGRESS.value]} in progress, " + plan_text += f"{status_counts[PlanStepStatus.BLOCKED.value]} blocked, {status_counts[PlanStepStatus.NOT_STARTED.value]} not started\n\n" + plan_text += "Steps:\n" + + status_marks = PlanStepStatus.get_status_marks() + + for i, (step, status, notes) in enumerate( + zip(steps, step_statuses, step_notes) + ): + # Use status marks to indicate step status + status_mark = status_marks.get( + status, status_marks[PlanStepStatus.NOT_STARTED.value] + ) + + plan_text += f"{i}. {status_mark} {step}\n" + if notes: + plan_text += f" Notes: {notes}\n" + + return plan_text + except Exception as e: + logger.error(f"Error generating plan text from storage: {e}") + return f"Error: Unable to retrieve plan with ID {self.active_plan_id}" + + async def _finalize_plan(self) -> str: + """Finalize the plan and provide a summary using the flow's LLM directly.""" + plan_text = await self._get_plan_text() + + # Create a summary using the flow's LLM directly + try: + system_message = Message.system_message( + "You are a planning assistant. Your task is to summarize the completed plan." + ) + + user_message = Message.user_message( + f"The plan has been completed. Here is the final plan status:\n\n{plan_text}\n\nPlease provide a summary of what was accomplished and any final thoughts." + ) + + response = await self.llm.ask( + messages=[user_message], system_msgs=[system_message] + ) + + return f"Plan completed:\n\n{response}" + except Exception as e: + logger.error(f"Error finalizing plan with LLM: {e}") + + # Fallback to using an agent for the summary + try: + agent = self.primary_agent + summary_prompt = f""" + The plan has been completed. Here is the final plan status: + + {plan_text} + + Please provide a summary of what was accomplished and any final thoughts. + """ + summary = await agent.run(summary_prompt) + return f"Plan completed:\n\n{summary}" + except Exception as e2: + logger.error(f"Error finalizing plan with agent: {e2}") + return "Plan completed. Error generating summary." diff --git a/app/llm.py b/app/llm.py new file mode 100644 index 0000000..82ebe88 --- /dev/null +++ b/app/llm.py @@ -0,0 +1,766 @@ +import math +from typing import Dict, List, Optional, Union + +import tiktoken +from openai import ( + APIError, + AsyncAzureOpenAI, + AsyncOpenAI, + AuthenticationError, + OpenAIError, + RateLimitError, +) +from openai.types.chat import ChatCompletion, ChatCompletionMessage +from tenacity import ( + retry, + retry_if_exception_type, + stop_after_attempt, + wait_random_exponential, +) + +from app.bedrock import BedrockClient +from app.config import LLMSettings, config +from app.exceptions import TokenLimitExceeded +from app.logger import logger # Assuming a logger is set up in your app +from app.schema import ( + ROLE_VALUES, + TOOL_CHOICE_TYPE, + TOOL_CHOICE_VALUES, + Message, + ToolChoice, +) + + +REASONING_MODELS = ["o1", "o3-mini"] +MULTIMODAL_MODELS = [ + "gpt-4-vision-preview", + "gpt-4o", + "gpt-4o-mini", + "claude-3-opus-20240229", + "claude-3-sonnet-20240229", + "claude-3-haiku-20240307", +] + + +class TokenCounter: + # Token constants + BASE_MESSAGE_TOKENS = 4 + FORMAT_TOKENS = 2 + LOW_DETAIL_IMAGE_TOKENS = 85 + HIGH_DETAIL_TILE_TOKENS = 170 + + # Image processing constants + MAX_SIZE = 2048 + HIGH_DETAIL_TARGET_SHORT_SIDE = 768 + TILE_SIZE = 512 + + def __init__(self, tokenizer): + self.tokenizer = tokenizer + + def count_text(self, text: str) -> int: + """Calculate tokens for a text string""" + return 0 if not text else len(self.tokenizer.encode(text)) + + def count_image(self, image_item: dict) -> int: + """ + Calculate tokens for an image based on detail level and dimensions + + For "low" detail: fixed 85 tokens + For "high" detail: + 1. Scale to fit in 2048x2048 square + 2. Scale shortest side to 768px + 3. Count 512px tiles (170 tokens each) + 4. Add 85 tokens + """ + detail = image_item.get("detail", "medium") + + # For low detail, always return fixed token count + if detail == "low": + return self.LOW_DETAIL_IMAGE_TOKENS + + # For medium detail (default in OpenAI), use high detail calculation + # OpenAI doesn't specify a separate calculation for medium + + # For high detail, calculate based on dimensions if available + if detail == "high" or detail == "medium": + # If dimensions are provided in the image_item + if "dimensions" in image_item: + width, height = image_item["dimensions"] + return self._calculate_high_detail_tokens(width, height) + + return ( + self._calculate_high_detail_tokens(1024, 1024) if detail == "high" else 1024 + ) + + def _calculate_high_detail_tokens(self, width: int, height: int) -> int: + """Calculate tokens for high detail images based on dimensions""" + # Step 1: Scale to fit in MAX_SIZE x MAX_SIZE square + if width > self.MAX_SIZE or height > self.MAX_SIZE: + scale = self.MAX_SIZE / max(width, height) + width = int(width * scale) + height = int(height * scale) + + # Step 2: Scale so shortest side is HIGH_DETAIL_TARGET_SHORT_SIDE + scale = self.HIGH_DETAIL_TARGET_SHORT_SIDE / min(width, height) + scaled_width = int(width * scale) + scaled_height = int(height * scale) + + # Step 3: Count number of 512px tiles + tiles_x = math.ceil(scaled_width / self.TILE_SIZE) + tiles_y = math.ceil(scaled_height / self.TILE_SIZE) + total_tiles = tiles_x * tiles_y + + # Step 4: Calculate final token count + return ( + total_tiles * self.HIGH_DETAIL_TILE_TOKENS + ) + self.LOW_DETAIL_IMAGE_TOKENS + + def count_content(self, content: Union[str, List[Union[str, dict]]]) -> int: + """Calculate tokens for message content""" + if not content: + return 0 + + if isinstance(content, str): + return self.count_text(content) + + token_count = 0 + for item in content: + if isinstance(item, str): + token_count += self.count_text(item) + elif isinstance(item, dict): + if "text" in item: + token_count += self.count_text(item["text"]) + elif "image_url" in item: + token_count += self.count_image(item) + return token_count + + def count_tool_calls(self, tool_calls: List[dict]) -> int: + """Calculate tokens for tool calls""" + token_count = 0 + for tool_call in tool_calls: + if "function" in tool_call: + function = tool_call["function"] + token_count += self.count_text(function.get("name", "")) + token_count += self.count_text(function.get("arguments", "")) + return token_count + + def count_message_tokens(self, messages: List[dict]) -> int: + """Calculate the total number of tokens in a message list""" + total_tokens = self.FORMAT_TOKENS # Base format tokens + + for message in messages: + tokens = self.BASE_MESSAGE_TOKENS # Base tokens per message + + # Add role tokens + tokens += self.count_text(message.get("role", "")) + + # Add content tokens + if "content" in message: + tokens += self.count_content(message["content"]) + + # Add tool calls tokens + if "tool_calls" in message: + tokens += self.count_tool_calls(message["tool_calls"]) + + # Add name and tool_call_id tokens + tokens += self.count_text(message.get("name", "")) + tokens += self.count_text(message.get("tool_call_id", "")) + + total_tokens += tokens + + return total_tokens + + +class LLM: + _instances: Dict[str, "LLM"] = {} + + def __new__( + cls, config_name: str = "default", llm_config: Optional[LLMSettings] = None + ): + if config_name not in cls._instances: + instance = super().__new__(cls) + instance.__init__(config_name, llm_config) + cls._instances[config_name] = instance + return cls._instances[config_name] + + def __init__( + self, config_name: str = "default", llm_config: Optional[LLMSettings] = None + ): + if not hasattr(self, "client"): # Only initialize if not already initialized + llm_config = llm_config or config.llm + llm_config = llm_config.get(config_name, llm_config["default"]) + self.model = llm_config.model + self.max_tokens = llm_config.max_tokens + self.temperature = llm_config.temperature + self.api_type = llm_config.api_type + self.api_key = llm_config.api_key + self.api_version = llm_config.api_version + self.base_url = llm_config.base_url + + # Add token counting related attributes + self.total_input_tokens = 0 + self.total_completion_tokens = 0 + self.max_input_tokens = ( + llm_config.max_input_tokens + if hasattr(llm_config, "max_input_tokens") + else None + ) + + # Initialize tokenizer + try: + self.tokenizer = tiktoken.encoding_for_model(self.model) + except KeyError: + # If the model is not in tiktoken's presets, use cl100k_base as default + self.tokenizer = tiktoken.get_encoding("cl100k_base") + + if self.api_type == "azure": + self.client = AsyncAzureOpenAI( + base_url=self.base_url, + api_key=self.api_key, + api_version=self.api_version, + ) + elif self.api_type == "aws": + self.client = BedrockClient() + else: + self.client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url) + + self.token_counter = TokenCounter(self.tokenizer) + + def count_tokens(self, text: str) -> int: + """Calculate the number of tokens in a text""" + if not text: + return 0 + return len(self.tokenizer.encode(text)) + + def count_message_tokens(self, messages: List[dict]) -> int: + return self.token_counter.count_message_tokens(messages) + + def update_token_count(self, input_tokens: int, completion_tokens: int = 0) -> None: + """Update token counts""" + # Only track tokens if max_input_tokens is set + self.total_input_tokens += input_tokens + self.total_completion_tokens += completion_tokens + logger.info( + f"Token usage: Input={input_tokens}, Completion={completion_tokens}, " + f"Cumulative Input={self.total_input_tokens}, Cumulative Completion={self.total_completion_tokens}, " + f"Total={input_tokens + completion_tokens}, Cumulative Total={self.total_input_tokens + self.total_completion_tokens}" + ) + + def check_token_limit(self, input_tokens: int) -> bool: + """Check if token limits are exceeded""" + if self.max_input_tokens is not None: + return (self.total_input_tokens + input_tokens) <= self.max_input_tokens + # If max_input_tokens is not set, always return True + return True + + def get_limit_error_message(self, input_tokens: int) -> str: + """Generate error message for token limit exceeded""" + if ( + self.max_input_tokens is not None + and (self.total_input_tokens + input_tokens) > self.max_input_tokens + ): + return f"Request may exceed input token limit (Current: {self.total_input_tokens}, Needed: {input_tokens}, Max: {self.max_input_tokens})" + + return "Token limit exceeded" + + @staticmethod + def format_messages( + messages: List[Union[dict, Message]], supports_images: bool = False + ) -> List[dict]: + """ + Format messages for LLM by converting them to OpenAI message format. + + Args: + messages: List of messages that can be either dict or Message objects + supports_images: Flag indicating if the target model supports image inputs + + Returns: + List[dict]: List of formatted messages in OpenAI format + + Raises: + ValueError: If messages are invalid or missing required fields + TypeError: If unsupported message types are provided + + Examples: + >>> msgs = [ + ... Message.system_message("You are a helpful assistant"), + ... {"role": "user", "content": "Hello"}, + ... Message.user_message("How are you?") + ... ] + >>> formatted = LLM.format_messages(msgs) + """ + formatted_messages = [] + + for message in messages: + # Convert Message objects to dictionaries + if isinstance(message, Message): + message = message.to_dict() + + if isinstance(message, dict): + # If message is a dict, ensure it has required fields + if "role" not in message: + raise ValueError("Message dict must contain 'role' field") + + # Process base64 images if present and model supports images + if supports_images and message.get("base64_image"): + # Initialize or convert content to appropriate format + if not message.get("content"): + message["content"] = [] + elif isinstance(message["content"], str): + message["content"] = [ + {"type": "text", "text": message["content"]} + ] + elif isinstance(message["content"], list): + # Convert string items to proper text objects + message["content"] = [ + ( + {"type": "text", "text": item} + if isinstance(item, str) + else item + ) + for item in message["content"] + ] + + # Add the image to content + message["content"].append( + { + "type": "image_url", + "image_url": { + "url": f"data:image/jpeg;base64,{message['base64_image']}" + }, + } + ) + + # Remove the base64_image field + del message["base64_image"] + # If model doesn't support images but message has base64_image, handle gracefully + elif not supports_images and message.get("base64_image"): + # Just remove the base64_image field and keep the text content + del message["base64_image"] + + if "content" in message or "tool_calls" in message: + formatted_messages.append(message) + # else: do not include the message + else: + raise TypeError(f"Unsupported message type: {type(message)}") + + # Validate all messages have required fields + for msg in formatted_messages: + if msg["role"] not in ROLE_VALUES: + raise ValueError(f"Invalid role: {msg['role']}") + + return formatted_messages + + @retry( + wait=wait_random_exponential(min=1, max=60), + stop=stop_after_attempt(6), + retry=retry_if_exception_type( + (OpenAIError, Exception, ValueError) + ), # Don't retry TokenLimitExceeded + ) + async def ask( + self, + messages: List[Union[dict, Message]], + system_msgs: Optional[List[Union[dict, Message]]] = None, + stream: bool = True, + temperature: Optional[float] = None, + ) -> str: + """ + Send a prompt to the LLM and get the response. + + Args: + messages: List of conversation messages + system_msgs: Optional system messages to prepend + stream (bool): Whether to stream the response + temperature (float): Sampling temperature for the response + + Returns: + str: The generated response + + Raises: + TokenLimitExceeded: If token limits are exceeded + ValueError: If messages are invalid or response is empty + OpenAIError: If API call fails after retries + Exception: For unexpected errors + """ + try: + # Check if the model supports images + supports_images = self.model in MULTIMODAL_MODELS + + # Format system and user messages with image support check + if system_msgs: + system_msgs = self.format_messages(system_msgs, supports_images) + messages = system_msgs + self.format_messages(messages, supports_images) + else: + messages = self.format_messages(messages, supports_images) + + # Calculate input token count + input_tokens = self.count_message_tokens(messages) + + # Check if token limits are exceeded + if not self.check_token_limit(input_tokens): + error_message = self.get_limit_error_message(input_tokens) + # Raise a special exception that won't be retried + raise TokenLimitExceeded(error_message) + + params = { + "model": self.model, + "messages": messages, + } + + if self.model in REASONING_MODELS: + params["max_completion_tokens"] = self.max_tokens + else: + params["max_tokens"] = self.max_tokens + params["temperature"] = ( + temperature if temperature is not None else self.temperature + ) + + if not stream: + # Non-streaming request + response = await self.client.chat.completions.create( + **params, stream=False + ) + + if not response.choices or not response.choices[0].message.content: + raise ValueError("Empty or invalid response from LLM") + + # Update token counts + self.update_token_count( + response.usage.prompt_tokens, response.usage.completion_tokens + ) + + return response.choices[0].message.content + + # Streaming request, For streaming, update estimated token count before making the request + self.update_token_count(input_tokens) + + response = await self.client.chat.completions.create(**params, stream=True) + + collected_messages = [] + completion_text = "" + async for chunk in response: + chunk_message = chunk.choices[0].delta.content or "" + collected_messages.append(chunk_message) + completion_text += chunk_message + print(chunk_message, end="", flush=True) + + print() # Newline after streaming + full_response = "".join(collected_messages).strip() + if not full_response: + raise ValueError("Empty response from streaming LLM") + + # estimate completion tokens for streaming response + completion_tokens = self.count_tokens(completion_text) + logger.info( + f"Estimated completion tokens for streaming response: {completion_tokens}" + ) + self.total_completion_tokens += completion_tokens + + return full_response + + except TokenLimitExceeded: + # Re-raise token limit errors without logging + raise + except ValueError: + logger.exception(f"Validation error") + raise + except OpenAIError as oe: + logger.exception(f"OpenAI API error") + if isinstance(oe, AuthenticationError): + logger.error("Authentication failed. Check API key.") + elif isinstance(oe, RateLimitError): + logger.error("Rate limit exceeded. Consider increasing retry attempts.") + elif isinstance(oe, APIError): + logger.error(f"API error: {oe}") + raise + except Exception: + logger.exception(f"Unexpected error in ask") + raise + + @retry( + wait=wait_random_exponential(min=1, max=60), + stop=stop_after_attempt(6), + retry=retry_if_exception_type( + (OpenAIError, Exception, ValueError) + ), # Don't retry TokenLimitExceeded + ) + async def ask_with_images( + self, + messages: List[Union[dict, Message]], + images: List[Union[str, dict]], + system_msgs: Optional[List[Union[dict, Message]]] = None, + stream: bool = False, + temperature: Optional[float] = None, + ) -> str: + """ + Send a prompt with images to the LLM and get the response. + + Args: + messages: List of conversation messages + images: List of image URLs or image data dictionaries + system_msgs: Optional system messages to prepend + stream (bool): Whether to stream the response + temperature (float): Sampling temperature for the response + + Returns: + str: The generated response + + Raises: + TokenLimitExceeded: If token limits are exceeded + ValueError: If messages are invalid or response is empty + OpenAIError: If API call fails after retries + Exception: For unexpected errors + """ + try: + # For ask_with_images, we always set supports_images to True because + # this method should only be called with models that support images + if self.model not in MULTIMODAL_MODELS: + raise ValueError( + f"Model {self.model} does not support images. Use a model from {MULTIMODAL_MODELS}" + ) + + # Format messages with image support + formatted_messages = self.format_messages(messages, supports_images=True) + + # Ensure the last message is from the user to attach images + if not formatted_messages or formatted_messages[-1]["role"] != "user": + raise ValueError( + "The last message must be from the user to attach images" + ) + + # Process the last user message to include images + last_message = formatted_messages[-1] + + # Convert content to multimodal format if needed + content = last_message["content"] + multimodal_content = ( + [{"type": "text", "text": content}] + if isinstance(content, str) + else content + if isinstance(content, list) + else [] + ) + + # Add images to content + for image in images: + if isinstance(image, str): + multimodal_content.append( + {"type": "image_url", "image_url": {"url": image}} + ) + elif isinstance(image, dict) and "url" in image: + multimodal_content.append({"type": "image_url", "image_url": image}) + elif isinstance(image, dict) and "image_url" in image: + multimodal_content.append(image) + else: + raise ValueError(f"Unsupported image format: {image}") + + # Update the message with multimodal content + last_message["content"] = multimodal_content + + # Add system messages if provided + if system_msgs: + all_messages = ( + self.format_messages(system_msgs, supports_images=True) + + formatted_messages + ) + else: + all_messages = formatted_messages + + # Calculate tokens and check limits + input_tokens = self.count_message_tokens(all_messages) + if not self.check_token_limit(input_tokens): + raise TokenLimitExceeded(self.get_limit_error_message(input_tokens)) + + # Set up API parameters + params = { + "model": self.model, + "messages": all_messages, + "stream": stream, + } + + # Add model-specific parameters + if self.model in REASONING_MODELS: + params["max_completion_tokens"] = self.max_tokens + else: + params["max_tokens"] = self.max_tokens + params["temperature"] = ( + temperature if temperature is not None else self.temperature + ) + + # Handle non-streaming request + if not stream: + response = await self.client.chat.completions.create(**params) + + if not response.choices or not response.choices[0].message.content: + raise ValueError("Empty or invalid response from LLM") + + self.update_token_count(response.usage.prompt_tokens) + return response.choices[0].message.content + + # Handle streaming request + self.update_token_count(input_tokens) + response = await self.client.chat.completions.create(**params) + + collected_messages = [] + async for chunk in response: + chunk_message = chunk.choices[0].delta.content or "" + collected_messages.append(chunk_message) + print(chunk_message, end="", flush=True) + + print() # Newline after streaming + full_response = "".join(collected_messages).strip() + + if not full_response: + raise ValueError("Empty response from streaming LLM") + + return full_response + + except TokenLimitExceeded: + raise + except ValueError as ve: + logger.error(f"Validation error in ask_with_images: {ve}") + raise + except OpenAIError as oe: + logger.error(f"OpenAI API error: {oe}") + if isinstance(oe, AuthenticationError): + logger.error("Authentication failed. Check API key.") + elif isinstance(oe, RateLimitError): + logger.error("Rate limit exceeded. Consider increasing retry attempts.") + elif isinstance(oe, APIError): + logger.error(f"API error: {oe}") + raise + except Exception as e: + logger.error(f"Unexpected error in ask_with_images: {e}") + raise + + @retry( + wait=wait_random_exponential(min=1, max=60), + stop=stop_after_attempt(6), + retry=retry_if_exception_type( + (OpenAIError, Exception, ValueError) + ), # Don't retry TokenLimitExceeded + ) + async def ask_tool( + self, + messages: List[Union[dict, Message]], + system_msgs: Optional[List[Union[dict, Message]]] = None, + timeout: int = 300, + tools: Optional[List[dict]] = None, + tool_choice: TOOL_CHOICE_TYPE = ToolChoice.AUTO, # type: ignore + temperature: Optional[float] = None, + **kwargs, + ) -> ChatCompletionMessage | None: + """ + Ask LLM using functions/tools and return the response. + + Args: + messages: List of conversation messages + system_msgs: Optional system messages to prepend + timeout: Request timeout in seconds + tools: List of tools to use + tool_choice: Tool choice strategy + temperature: Sampling temperature for the response + **kwargs: Additional completion arguments + + Returns: + ChatCompletionMessage: The model's response + + Raises: + TokenLimitExceeded: If token limits are exceeded + ValueError: If tools, tool_choice, or messages are invalid + OpenAIError: If API call fails after retries + Exception: For unexpected errors + """ + try: + # Validate tool_choice + if tool_choice not in TOOL_CHOICE_VALUES: + raise ValueError(f"Invalid tool_choice: {tool_choice}") + + # Check if the model supports images + supports_images = self.model in MULTIMODAL_MODELS + + # Format messages + if system_msgs: + system_msgs = self.format_messages(system_msgs, supports_images) + messages = system_msgs + self.format_messages(messages, supports_images) + else: + messages = self.format_messages(messages, supports_images) + + # Calculate input token count + input_tokens = self.count_message_tokens(messages) + + # If there are tools, calculate token count for tool descriptions + tools_tokens = 0 + if tools: + for tool in tools: + tools_tokens += self.count_tokens(str(tool)) + + input_tokens += tools_tokens + + # Check if token limits are exceeded + if not self.check_token_limit(input_tokens): + error_message = self.get_limit_error_message(input_tokens) + # Raise a special exception that won't be retried + raise TokenLimitExceeded(error_message) + + # Validate tools if provided + if tools: + for tool in tools: + if not isinstance(tool, dict) or "type" not in tool: + raise ValueError("Each tool must be a dict with 'type' field") + + # Set up the completion request + params = { + "model": self.model, + "messages": messages, + "tools": tools, + "tool_choice": tool_choice, + "timeout": timeout, + **kwargs, + } + + if self.model in REASONING_MODELS: + params["max_completion_tokens"] = self.max_tokens + else: + params["max_tokens"] = self.max_tokens + params["temperature"] = ( + temperature if temperature is not None else self.temperature + ) + + params["stream"] = False # Always use non-streaming for tool requests + response: ChatCompletion = await self.client.chat.completions.create( + **params + ) + + # Check if response is valid + if not response.choices or not response.choices[0].message: + print(response) + # raise ValueError("Invalid or empty response from LLM") + return None + + # Update token counts + self.update_token_count( + response.usage.prompt_tokens, response.usage.completion_tokens + ) + + return response.choices[0].message + + except TokenLimitExceeded: + # Re-raise token limit errors without logging + raise + except ValueError as ve: + logger.error(f"Validation error in ask_tool: {ve}") + raise + except OpenAIError as oe: + logger.error(f"OpenAI API error: {oe}") + if isinstance(oe, AuthenticationError): + logger.error("Authentication failed. Check API key.") + elif isinstance(oe, RateLimitError): + logger.error("Rate limit exceeded. Consider increasing retry attempts.") + elif isinstance(oe, APIError): + logger.error(f"API error: {oe}") + raise + except Exception as e: + logger.error(f"Unexpected error in ask_tool: {e}") + raise diff --git a/app/logger.py b/app/logger.py new file mode 100644 index 0000000..c5d9ce1 --- /dev/null +++ b/app/logger.py @@ -0,0 +1,42 @@ +import sys +from datetime import datetime + +from loguru import logger as _logger + +from app.config import PROJECT_ROOT + + +_print_level = "INFO" + + +def define_log_level(print_level="INFO", logfile_level="DEBUG", name: str = None): + """Adjust the log level to above level""" + global _print_level + _print_level = print_level + + current_date = datetime.now() + formatted_date = current_date.strftime("%Y%m%d%H%M%S") + log_name = ( + f"{name}_{formatted_date}" if name else formatted_date + ) # name a log with prefix name + + _logger.remove() + _logger.add(sys.stderr, level=print_level) + _logger.add(PROJECT_ROOT / f"logs/{log_name}.log", level=logfile_level) + return _logger + + +logger = define_log_level() + + +if __name__ == "__main__": + logger.info("Starting application") + logger.debug("Debug message") + logger.warning("Warning message") + logger.error("Error message") + logger.critical("Critical message") + + try: + raise ValueError("Test error") + except Exception as e: + logger.exception(f"An error occurred: {e}") diff --git a/app/mcp/__init__.py b/app/mcp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/mcp/server.py b/app/mcp/server.py new file mode 100644 index 0000000..3ee8b08 --- /dev/null +++ b/app/mcp/server.py @@ -0,0 +1,180 @@ +import logging +import sys + + +logging.basicConfig(level=logging.INFO, handlers=[logging.StreamHandler(sys.stderr)]) + +import argparse +import asyncio +import atexit +import json +from inspect import Parameter, Signature +from typing import Any, Dict, Optional + +from mcp.server.fastmcp import FastMCP + +from app.logger import logger +from app.tool.base import BaseTool +from app.tool.bash import Bash +from app.tool.browser_use_tool import BrowserUseTool +from app.tool.str_replace_editor import StrReplaceEditor +from app.tool.terminate import Terminate + + +class MCPServer: + """MCP Server implementation with tool registration and management.""" + + def __init__(self, name: str = "openmanus"): + self.server = FastMCP(name) + self.tools: Dict[str, BaseTool] = {} + + # Initialize standard tools + self.tools["bash"] = Bash() + self.tools["browser"] = BrowserUseTool() + self.tools["editor"] = StrReplaceEditor() + self.tools["terminate"] = Terminate() + + def register_tool(self, tool: BaseTool, method_name: Optional[str] = None) -> None: + """Register a tool with parameter validation and documentation.""" + tool_name = method_name or tool.name + tool_param = tool.to_param() + tool_function = tool_param["function"] + + # Define the async function to be registered + async def tool_method(**kwargs): + logger.info(f"Executing {tool_name}: {kwargs}") + result = await tool.execute(**kwargs) + + logger.info(f"Result of {tool_name}: {result}") + + # Handle different types of results (match original logic) + if hasattr(result, "model_dump"): + return json.dumps(result.model_dump()) + elif isinstance(result, dict): + return json.dumps(result) + return result + + # Set method metadata + tool_method.__name__ = tool_name + tool_method.__doc__ = self._build_docstring(tool_function) + tool_method.__signature__ = self._build_signature(tool_function) + + # Store parameter schema (important for tools that access it programmatically) + param_props = tool_function.get("parameters", {}).get("properties", {}) + required_params = tool_function.get("parameters", {}).get("required", []) + tool_method._parameter_schema = { + param_name: { + "description": param_details.get("description", ""), + "type": param_details.get("type", "any"), + "required": param_name in required_params, + } + for param_name, param_details in param_props.items() + } + + # Register with server + self.server.tool()(tool_method) + logger.info(f"Registered tool: {tool_name}") + + def _build_docstring(self, tool_function: dict) -> str: + """Build a formatted docstring from tool function metadata.""" + description = tool_function.get("description", "") + param_props = tool_function.get("parameters", {}).get("properties", {}) + required_params = tool_function.get("parameters", {}).get("required", []) + + # Build docstring (match original format) + docstring = description + if param_props: + docstring += "\n\nParameters:\n" + for param_name, param_details in param_props.items(): + required_str = ( + "(required)" if param_name in required_params else "(optional)" + ) + param_type = param_details.get("type", "any") + param_desc = param_details.get("description", "") + docstring += ( + f" {param_name} ({param_type}) {required_str}: {param_desc}\n" + ) + + return docstring + + def _build_signature(self, tool_function: dict) -> Signature: + """Build a function signature from tool function metadata.""" + param_props = tool_function.get("parameters", {}).get("properties", {}) + required_params = tool_function.get("parameters", {}).get("required", []) + + parameters = [] + + # Follow original type mapping + for param_name, param_details in param_props.items(): + param_type = param_details.get("type", "") + default = Parameter.empty if param_name in required_params else None + + # Map JSON Schema types to Python types (same as original) + annotation = Any + if param_type == "string": + annotation = str + elif param_type == "integer": + annotation = int + elif param_type == "number": + annotation = float + elif param_type == "boolean": + annotation = bool + elif param_type == "object": + annotation = dict + elif param_type == "array": + annotation = list + + # Create parameter with same structure as original + param = Parameter( + name=param_name, + kind=Parameter.KEYWORD_ONLY, + default=default, + annotation=annotation, + ) + parameters.append(param) + + return Signature(parameters=parameters) + + async def cleanup(self) -> None: + """Clean up server resources.""" + logger.info("Cleaning up resources") + # Follow original cleanup logic - only clean browser tool + if "browser" in self.tools and hasattr(self.tools["browser"], "cleanup"): + await self.tools["browser"].cleanup() + + def register_all_tools(self) -> None: + """Register all tools with the server.""" + for tool in self.tools.values(): + self.register_tool(tool) + + def run(self, transport: str = "stdio") -> None: + """Run the MCP server.""" + # Register all tools + self.register_all_tools() + + # Register cleanup function (match original behavior) + atexit.register(lambda: asyncio.run(self.cleanup())) + + # Start server (with same logging as original) + logger.info(f"Starting OpenManus server ({transport} mode)") + self.server.run(transport=transport) + + +def parse_args() -> argparse.Namespace: + """Parse command line arguments.""" + parser = argparse.ArgumentParser(description="OpenManus MCP Server") + parser.add_argument( + "--transport", + choices=["stdio"], + default="stdio", + help="Communication method: stdio or http (default: stdio)", + ) + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + + # Create and run server (maintaining original flow) + server = MCPServer() + server.run(transport=args.transport) diff --git a/app/prompt/__init__.py b/app/prompt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/prompt/browser.py b/app/prompt/browser.py new file mode 100644 index 0000000..b27714d --- /dev/null +++ b/app/prompt/browser.py @@ -0,0 +1,94 @@ +SYSTEM_PROMPT = """\ +You are an AI agent designed to automate browser tasks. Your goal is to accomplish the ultimate task following the rules. + +# Input Format +Task +Previous steps +Current URL +Open Tabs +Interactive Elements +[index]text +- index: Numeric identifier for interaction +- type: HTML element type (button, input, etc.) +- text: Element description +Example: +[33] + +- Only elements with numeric indexes in [] are interactive +- elements without [] provide only context + +# Response Rules +1. RESPONSE FORMAT: You must ALWAYS respond with valid JSON in this exact format: +{{"current_state": {{"evaluation_previous_goal": "Success|Failed|Unknown - Analyze the current elements and the image to check if the previous goals/actions are successful like intended by the task. Mention if something unexpected happened. Shortly state why/why not", +"memory": "Description of what has been done and what you need to remember. Be very specific. Count here ALWAYS how many times you have done something and how many remain. E.g. 0 out of 10 websites analyzed. Continue with abc and xyz", +"next_goal": "What needs to be done with the next immediate action"}}, +"action":[{{"one_action_name": {{// action-specific parameter}}}}, // ... more actions in sequence]}} + +2. ACTIONS: You can specify multiple actions in the list to be executed in sequence. But always specify only one action name per item. Use maximum {{max_actions}} actions per sequence. +Common action sequences: +- Form filling: [{{"input_text": {{"index": 1, "text": "username"}}}}, {{"input_text": {{"index": 2, "text": "password"}}}}, {{"click_element": {{"index": 3}}}}] +- Navigation and extraction: [{{"go_to_url": {{"url": "https://example.com"}}}}, {{"extract_content": {{"goal": "extract the names"}}}}] +- Actions are executed in the given order +- If the page changes after an action, the sequence is interrupted and you get the new state. +- Only provide the action sequence until an action which changes the page state significantly. +- Try to be efficient, e.g. fill forms at once, or chain actions where nothing changes on the page +- only use multiple actions if it makes sense. + +3. ELEMENT INTERACTION: +- Only use indexes of the interactive elements +- Elements marked with "[]Non-interactive text" are non-interactive + +4. NAVIGATION & ERROR HANDLING: +- If no suitable elements exist, use other functions to complete the task +- If stuck, try alternative approaches - like going back to a previous page, new search, new tab etc. +- Handle popups/cookies by accepting or closing them +- Use scroll to find elements you are looking for +- If you want to research something, open a new tab instead of using the current tab +- If captcha pops up, try to solve it - else try a different approach +- If the page is not fully loaded, use wait action + +5. TASK COMPLETION: +- Use the done action as the last action as soon as the ultimate task is complete +- Dont use "done" before you are done with everything the user asked you, except you reach the last step of max_steps. +- If you reach your last step, use the done action even if the task is not fully finished. Provide all the information you have gathered so far. If the ultimate task is completly finished set success to true. If not everything the user asked for is completed set success in done to false! +- If you have to do something repeatedly for example the task says for "each", or "for all", or "x times", count always inside "memory" how many times you have done it and how many remain. Don't stop until you have completed like the task asked you. Only call done after the last step. +- Don't hallucinate actions +- Make sure you include everything you found out for the ultimate task in the done text parameter. Do not just say you are done, but include the requested information of the task. + +6. VISUAL CONTEXT: +- When an image is provided, use it to understand the page layout +- Bounding boxes with labels on their top right corner correspond to element indexes + +7. Form filling: +- If you fill an input field and your action sequence is interrupted, most often something changed e.g. suggestions popped up under the field. + +8. Long tasks: +- Keep track of the status and subresults in the memory. + +9. Extraction: +- If your task is to find information - call extract_content on the specific pages to get and store the information. +Your responses must be always JSON with the specified format. +""" + +NEXT_STEP_PROMPT = """ +What should I do next to achieve my goal? + +When you see [Current state starts here], focus on the following: +- Current URL and page title{url_placeholder} +- Available tabs{tabs_placeholder} +- Interactive elements and their indices +- Content above{content_above_placeholder} or below{content_below_placeholder} the viewport (if indicated) +- Any action results or errors{results_placeholder} + +For browser interactions: +- To navigate: browser_use with action="go_to_url", url="..." +- To click: browser_use with action="click_element", index=N +- To type: browser_use with action="input_text", index=N, text="..." +- To extract: browser_use with action="extract_content", goal="..." +- To scroll: browser_use with action="scroll_down" or "scroll_up" + +Consider both what's visible and what might be beyond the current viewport. +Be methodical - remember your progress and what you've learned so far. + +If you want to stop the interaction at any point, use the `terminate` tool/function call. +""" diff --git a/app/prompt/manus.py b/app/prompt/manus.py new file mode 100644 index 0000000..99e7e83 --- /dev/null +++ b/app/prompt/manus.py @@ -0,0 +1,10 @@ +SYSTEM_PROMPT = ( + "You are OpenManus, an all-capable AI assistant, aimed at solving any task presented by the user. You have various tools at your disposal that you can call upon to efficiently complete complex requests. Whether it's programming, information retrieval, file processing, web browsing, or human interaction (only for extreme cases), you can handle it all." + "The initial directory is: {directory}" +) + +NEXT_STEP_PROMPT = """ +Based on user needs, proactively select the most appropriate tool or combination of tools. For complex tasks, you can break down the problem and use different tools step by step to solve it. After using each tool, clearly explain the execution results and suggest the next steps. + +If you want to stop the interaction at any point, use the `terminate` tool/function call. +""" diff --git a/app/prompt/mcp.py b/app/prompt/mcp.py new file mode 100644 index 0000000..acf15b2 --- /dev/null +++ b/app/prompt/mcp.py @@ -0,0 +1,43 @@ +"""Prompts for the MCP Agent.""" + +SYSTEM_PROMPT = """You are an AI assistant with access to a Model Context Protocol (MCP) server. +You can use the tools provided by the MCP server to complete tasks. +The MCP server will dynamically expose tools that you can use - always check the available tools first. + +When using an MCP tool: +1. Choose the appropriate tool based on your task requirements +2. Provide properly formatted arguments as required by the tool +3. Observe the results and use them to determine next steps +4. Tools may change during operation - new tools might appear or existing ones might disappear + +Follow these guidelines: +- Call tools with valid parameters as documented in their schemas +- Handle errors gracefully by understanding what went wrong and trying again with corrected parameters +- For multimedia responses (like images), you'll receive a description of the content +- Complete user requests step by step, using the most appropriate tools +- If multiple tools need to be called in sequence, make one call at a time and wait for results + +Remember to clearly explain your reasoning and actions to the user. +""" + +NEXT_STEP_PROMPT = """Based on the current state and available tools, what should be done next? +Think step by step about the problem and identify which MCP tool would be most helpful for the current stage. +If you've already made progress, consider what additional information you need or what actions would move you closer to completing the task. +""" + +# Additional specialized prompts +TOOL_ERROR_PROMPT = """You encountered an error with the tool '{tool_name}'. +Try to understand what went wrong and correct your approach. +Common issues include: +- Missing or incorrect parameters +- Invalid parameter formats +- Using a tool that's no longer available +- Attempting an operation that's not supported + +Please check the tool specifications and try again with corrected parameters. +""" + +MULTIMEDIA_RESPONSE_PROMPT = """You've received a multimedia response (image, audio, etc.) from the tool '{tool_name}'. +This content has been processed and described for you. +Use this information to continue the task or provide insights to the user. +""" diff --git a/app/prompt/planning.py b/app/prompt/planning.py new file mode 100644 index 0000000..bd5f4ce --- /dev/null +++ b/app/prompt/planning.py @@ -0,0 +1,27 @@ +PLANNING_SYSTEM_PROMPT = """ +You are an expert Planning Agent tasked with solving problems efficiently through structured plans. +Your job is: +1. Analyze requests to understand the task scope +2. Create a clear, actionable plan that makes meaningful progress with the `planning` tool +3. Execute steps using available tools as needed +4. Track progress and adapt plans when necessary +5. Use `finish` to conclude immediately when the task is complete + + +Available tools will vary by task but may include: +- `planning`: Create, update, and track plans (commands: create, update, mark_step, etc.) +- `finish`: End the task when complete +Break tasks into logical steps with clear outcomes. Avoid excessive detail or sub-steps. +Think about dependencies and verification methods. +Know when to conclude - don't continue thinking once objectives are met. +""" + +NEXT_STEP_PROMPT = """ +Based on the current state, what's your next action? +Choose the most efficient path forward: +1. Is the plan sufficient, or does it need refinement? +2. Can you execute the next step immediately? +3. Is the task complete? If so, use `finish` right away. + +Be concise in your reasoning, then select the appropriate tool or action. +""" diff --git a/app/prompt/swe.py b/app/prompt/swe.py new file mode 100644 index 0000000..389e74b --- /dev/null +++ b/app/prompt/swe.py @@ -0,0 +1,22 @@ +SYSTEM_PROMPT = """SETTING: You are an autonomous programmer, and you're working directly in the command line with a special interface. + +The special interface consists of a file editor that shows you {{WINDOW}} lines of a file at a time. +In addition to typical bash commands, you can also use specific commands to help you navigate and edit files. +To call a command, you need to invoke it with a function call/tool call. + +Please note that THE EDIT COMMAND REQUIRES PROPER INDENTATION. +If you'd like to add the line ' print(x)' you must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run. + +RESPONSE FORMAT: +Your shell prompt is formatted as follows: +(Open file: ) +(Current directory: ) +bash-$ + +First, you should _always_ include a general thought about what you're going to do next. +Then, for every response, you must include exactly _ONE_ tool call/function call. + +Remember, you should always include a _SINGLE_ tool call/function call and then wait for a response from the shell before continuing with more discussion and commands. Everything you include in the DISCUSSION section will be saved for future reference. +If you'd like to issue two commands at once, PLEASE DO NOT DO THAT! Please instead first submit just the first tool call, and then after receiving a response you'll be able to issue the second tool call. +Note that the environment does NOT support interactive session commands (e.g. python, vim), so please do not invoke them. +""" diff --git a/app/prompt/toolcall.py b/app/prompt/toolcall.py new file mode 100644 index 0000000..e1a3be9 --- /dev/null +++ b/app/prompt/toolcall.py @@ -0,0 +1,5 @@ +SYSTEM_PROMPT = "You are an agent that can execute tool calls" + +NEXT_STEP_PROMPT = ( + "If you want to stop interaction, use `terminate` tool/function call." +) diff --git a/app/prompt/visualization.py b/app/prompt/visualization.py new file mode 100644 index 0000000..8e4fecc --- /dev/null +++ b/app/prompt/visualization.py @@ -0,0 +1,10 @@ +SYSTEM_PROMPT = """You are an AI agent designed to data analysis / visualization task. You have various tools at your disposal that you can call upon to efficiently complete complex requests. +# Note: +1. The workspace directory is: {directory}; Read / write file in workspace +2. Generate analysis conclusion report in the end""" + +NEXT_STEP_PROMPT = """Based on user needs, break down the problem and use different tools step by step to solve it. +# Note +1. Each step select the most appropriate tool proactively (ONLY ONE). +2. After using each tool, clearly explain the execution results and suggest the next steps. +3. When observation with Error, review and fix it.""" diff --git a/app/sandbox/__init__.py b/app/sandbox/__init__.py new file mode 100644 index 0000000..ccf0df6 --- /dev/null +++ b/app/sandbox/__init__.py @@ -0,0 +1,30 @@ +""" +Docker Sandbox Module + +Provides secure containerized execution environment with resource limits +and isolation for running untrusted code. +""" +from app.sandbox.client import ( + BaseSandboxClient, + LocalSandboxClient, + create_sandbox_client, +) +from app.sandbox.core.exceptions import ( + SandboxError, + SandboxResourceError, + SandboxTimeoutError, +) +from app.sandbox.core.manager import SandboxManager +from app.sandbox.core.sandbox import DockerSandbox + + +__all__ = [ + "DockerSandbox", + "SandboxManager", + "BaseSandboxClient", + "LocalSandboxClient", + "create_sandbox_client", + "SandboxError", + "SandboxTimeoutError", + "SandboxResourceError", +] diff --git a/app/sandbox/client.py b/app/sandbox/client.py new file mode 100644 index 0000000..09a8f2e --- /dev/null +++ b/app/sandbox/client.py @@ -0,0 +1,201 @@ +from abc import ABC, abstractmethod +from typing import Dict, Optional, Protocol + +from app.config import SandboxSettings +from app.sandbox.core.sandbox import DockerSandbox + + +class SandboxFileOperations(Protocol): + """Protocol for sandbox file operations.""" + + async def copy_from(self, container_path: str, local_path: str) -> None: + """Copies file from container to local. + + Args: + container_path: File path in container. + local_path: Local destination path. + """ + ... + + async def copy_to(self, local_path: str, container_path: str) -> None: + """Copies file from local to container. + + Args: + local_path: Local source file path. + container_path: Destination path in container. + """ + ... + + async def read_file(self, path: str) -> str: + """Reads file content from container. + + Args: + path: File path in container. + + Returns: + str: File content. + """ + ... + + async def write_file(self, path: str, content: str) -> None: + """Writes content to file in container. + + Args: + path: File path in container. + content: Content to write. + """ + ... + + +class BaseSandboxClient(ABC): + """Base sandbox client interface.""" + + @abstractmethod + async def create( + self, + config: Optional[SandboxSettings] = None, + volume_bindings: Optional[Dict[str, str]] = None, + ) -> None: + """Creates sandbox.""" + + @abstractmethod + async def run_command(self, command: str, timeout: Optional[int] = None) -> str: + """Executes command.""" + + @abstractmethod + async def copy_from(self, container_path: str, local_path: str) -> None: + """Copies file from container.""" + + @abstractmethod + async def copy_to(self, local_path: str, container_path: str) -> None: + """Copies file to container.""" + + @abstractmethod + async def read_file(self, path: str) -> str: + """Reads file.""" + + @abstractmethod + async def write_file(self, path: str, content: str) -> None: + """Writes file.""" + + @abstractmethod + async def cleanup(self) -> None: + """Cleans up resources.""" + + +class LocalSandboxClient(BaseSandboxClient): + """Local sandbox client implementation.""" + + def __init__(self): + """Initializes local sandbox client.""" + self.sandbox: Optional[DockerSandbox] = None + + async def create( + self, + config: Optional[SandboxSettings] = None, + volume_bindings: Optional[Dict[str, str]] = None, + ) -> None: + """Creates a sandbox. + + Args: + config: Sandbox configuration. + volume_bindings: Volume mappings. + + Raises: + RuntimeError: If sandbox creation fails. + """ + self.sandbox = DockerSandbox(config, volume_bindings) + await self.sandbox.create() + + async def run_command(self, command: str, timeout: Optional[int] = None) -> str: + """Runs command in sandbox. + + Args: + command: Command to execute. + timeout: Execution timeout in seconds. + + Returns: + Command output. + + Raises: + RuntimeError: If sandbox not initialized. + """ + if not self.sandbox: + raise RuntimeError("Sandbox not initialized") + return await self.sandbox.run_command(command, timeout) + + async def copy_from(self, container_path: str, local_path: str) -> None: + """Copies file from container to local. + + Args: + container_path: File path in container. + local_path: Local destination path. + + Raises: + RuntimeError: If sandbox not initialized. + """ + if not self.sandbox: + raise RuntimeError("Sandbox not initialized") + await self.sandbox.copy_from(container_path, local_path) + + async def copy_to(self, local_path: str, container_path: str) -> None: + """Copies file from local to container. + + Args: + local_path: Local source file path. + container_path: Destination path in container. + + Raises: + RuntimeError: If sandbox not initialized. + """ + if not self.sandbox: + raise RuntimeError("Sandbox not initialized") + await self.sandbox.copy_to(local_path, container_path) + + async def read_file(self, path: str) -> str: + """Reads file from container. + + Args: + path: File path in container. + + Returns: + File content. + + Raises: + RuntimeError: If sandbox not initialized. + """ + if not self.sandbox: + raise RuntimeError("Sandbox not initialized") + return await self.sandbox.read_file(path) + + async def write_file(self, path: str, content: str) -> None: + """Writes file to container. + + Args: + path: File path in container. + content: File content. + + Raises: + RuntimeError: If sandbox not initialized. + """ + if not self.sandbox: + raise RuntimeError("Sandbox not initialized") + await self.sandbox.write_file(path, content) + + async def cleanup(self) -> None: + """Cleans up resources.""" + if self.sandbox: + await self.sandbox.cleanup() + self.sandbox = None + + +def create_sandbox_client() -> LocalSandboxClient: + """Creates a sandbox client. + + Returns: + LocalSandboxClient: Sandbox client instance. + """ + return LocalSandboxClient() + + +SANDBOX_CLIENT = create_sandbox_client() diff --git a/app/sandbox/core/exceptions.py b/app/sandbox/core/exceptions.py new file mode 100644 index 0000000..5c1f0e8 --- /dev/null +++ b/app/sandbox/core/exceptions.py @@ -0,0 +1,17 @@ +"""Exception classes for the sandbox system. + +This module defines custom exceptions used throughout the sandbox system to +handle various error conditions in a structured way. +""" + + +class SandboxError(Exception): + """Base exception for sandbox-related errors.""" + + +class SandboxTimeoutError(SandboxError): + """Exception raised when a sandbox operation times out.""" + + +class SandboxResourceError(SandboxError): + """Exception raised for resource-related errors.""" diff --git a/app/sandbox/core/manager.py b/app/sandbox/core/manager.py new file mode 100644 index 0000000..5814f12 --- /dev/null +++ b/app/sandbox/core/manager.py @@ -0,0 +1,313 @@ +import asyncio +import uuid +from contextlib import asynccontextmanager +from typing import Dict, Optional, Set + +import docker +from docker.errors import APIError, ImageNotFound + +from app.config import SandboxSettings +from app.logger import logger +from app.sandbox.core.sandbox import DockerSandbox + + +class SandboxManager: + """Docker sandbox manager. + + Manages multiple DockerSandbox instances lifecycle including creation, + monitoring, and cleanup. Provides concurrent access control and automatic + cleanup mechanisms for sandbox resources. + + Attributes: + max_sandboxes: Maximum allowed number of sandboxes. + idle_timeout: Sandbox idle timeout in seconds. + cleanup_interval: Cleanup check interval in seconds. + _sandboxes: Active sandbox instance mapping. + _last_used: Last used time record for sandboxes. + """ + + def __init__( + self, + max_sandboxes: int = 100, + idle_timeout: int = 3600, + cleanup_interval: int = 300, + ): + """Initializes sandbox manager. + + Args: + max_sandboxes: Maximum sandbox count limit. + idle_timeout: Idle timeout in seconds. + cleanup_interval: Cleanup check interval in seconds. + """ + self.max_sandboxes = max_sandboxes + self.idle_timeout = idle_timeout + self.cleanup_interval = cleanup_interval + + # Docker client + self._client = docker.from_env() + + # Resource mappings + self._sandboxes: Dict[str, DockerSandbox] = {} + self._last_used: Dict[str, float] = {} + + # Concurrency control + self._locks: Dict[str, asyncio.Lock] = {} + self._global_lock = asyncio.Lock() + self._active_operations: Set[str] = set() + + # Cleanup task + self._cleanup_task: Optional[asyncio.Task] = None + self._is_shutting_down = False + + # Start automatic cleanup + self.start_cleanup_task() + + async def ensure_image(self, image: str) -> bool: + """Ensures Docker image is available. + + Args: + image: Image name. + + Returns: + bool: Whether image is available. + """ + try: + self._client.images.get(image) + return True + except ImageNotFound: + try: + logger.info(f"Pulling image {image}...") + await asyncio.get_event_loop().run_in_executor( + None, self._client.images.pull, image + ) + return True + except (APIError, Exception) as e: + logger.error(f"Failed to pull image {image}: {e}") + return False + + @asynccontextmanager + async def sandbox_operation(self, sandbox_id: str): + """Context manager for sandbox operations. + + Provides concurrency control and usage time updates. + + Args: + sandbox_id: Sandbox ID. + + Raises: + KeyError: If sandbox not found. + """ + if sandbox_id not in self._locks: + self._locks[sandbox_id] = asyncio.Lock() + + async with self._locks[sandbox_id]: + if sandbox_id not in self._sandboxes: + raise KeyError(f"Sandbox {sandbox_id} not found") + + self._active_operations.add(sandbox_id) + try: + self._last_used[sandbox_id] = asyncio.get_event_loop().time() + yield self._sandboxes[sandbox_id] + finally: + self._active_operations.remove(sandbox_id) + + async def create_sandbox( + self, + config: Optional[SandboxSettings] = None, + volume_bindings: Optional[Dict[str, str]] = None, + ) -> str: + """Creates a new sandbox instance. + + Args: + config: Sandbox configuration. + volume_bindings: Volume mapping configuration. + + Returns: + str: Sandbox ID. + + Raises: + RuntimeError: If max sandbox count reached or creation fails. + """ + async with self._global_lock: + if len(self._sandboxes) >= self.max_sandboxes: + raise RuntimeError( + f"Maximum number of sandboxes ({self.max_sandboxes}) reached" + ) + + config = config or SandboxSettings() + if not await self.ensure_image(config.image): + raise RuntimeError(f"Failed to ensure Docker image: {config.image}") + + sandbox_id = str(uuid.uuid4()) + try: + sandbox = DockerSandbox(config, volume_bindings) + await sandbox.create() + + self._sandboxes[sandbox_id] = sandbox + self._last_used[sandbox_id] = asyncio.get_event_loop().time() + self._locks[sandbox_id] = asyncio.Lock() + + logger.info(f"Created sandbox {sandbox_id}") + return sandbox_id + + except Exception as e: + logger.error(f"Failed to create sandbox: {e}") + if sandbox_id in self._sandboxes: + await self.delete_sandbox(sandbox_id) + raise RuntimeError(f"Failed to create sandbox: {e}") + + async def get_sandbox(self, sandbox_id: str) -> DockerSandbox: + """Gets a sandbox instance. + + Args: + sandbox_id: Sandbox ID. + + Returns: + DockerSandbox: Sandbox instance. + + Raises: + KeyError: If sandbox does not exist. + """ + async with self.sandbox_operation(sandbox_id) as sandbox: + return sandbox + + def start_cleanup_task(self) -> None: + """Starts automatic cleanup task.""" + + async def cleanup_loop(): + while not self._is_shutting_down: + try: + await self._cleanup_idle_sandboxes() + except Exception as e: + logger.error(f"Error in cleanup loop: {e}") + await asyncio.sleep(self.cleanup_interval) + + self._cleanup_task = asyncio.create_task(cleanup_loop()) + + async def _cleanup_idle_sandboxes(self) -> None: + """Cleans up idle sandboxes.""" + current_time = asyncio.get_event_loop().time() + to_cleanup = [] + + async with self._global_lock: + for sandbox_id, last_used in self._last_used.items(): + if ( + sandbox_id not in self._active_operations + and current_time - last_used > self.idle_timeout + ): + to_cleanup.append(sandbox_id) + + for sandbox_id in to_cleanup: + try: + await self.delete_sandbox(sandbox_id) + except Exception as e: + logger.error(f"Error cleaning up sandbox {sandbox_id}: {e}") + + async def cleanup(self) -> None: + """Cleans up all resources.""" + logger.info("Starting manager cleanup...") + self._is_shutting_down = True + + # Cancel cleanup task + if self._cleanup_task: + self._cleanup_task.cancel() + try: + await asyncio.wait_for(self._cleanup_task, timeout=1.0) + except (asyncio.CancelledError, asyncio.TimeoutError): + pass + + # Get all sandbox IDs to clean up + async with self._global_lock: + sandbox_ids = list(self._sandboxes.keys()) + + # Concurrently clean up all sandboxes + cleanup_tasks = [] + for sandbox_id in sandbox_ids: + task = asyncio.create_task(self._safe_delete_sandbox(sandbox_id)) + cleanup_tasks.append(task) + + if cleanup_tasks: + # Wait for all cleanup tasks to complete, with timeout to avoid infinite waiting + try: + await asyncio.wait(cleanup_tasks, timeout=30.0) + except asyncio.TimeoutError: + logger.error("Sandbox cleanup timed out") + + # Clean up remaining references + self._sandboxes.clear() + self._last_used.clear() + self._locks.clear() + self._active_operations.clear() + + logger.info("Manager cleanup completed") + + async def _safe_delete_sandbox(self, sandbox_id: str) -> None: + """Safely deletes a single sandbox. + + Args: + sandbox_id: Sandbox ID to delete. + """ + try: + if sandbox_id in self._active_operations: + logger.warning( + f"Sandbox {sandbox_id} has active operations, waiting for completion" + ) + for _ in range(10): # Wait at most 10 times + await asyncio.sleep(0.5) + if sandbox_id not in self._active_operations: + break + else: + logger.warning( + f"Timeout waiting for sandbox {sandbox_id} operations to complete" + ) + + # Get reference to sandbox object + sandbox = self._sandboxes.get(sandbox_id) + if sandbox: + await sandbox.cleanup() + + # Remove sandbox record from manager + async with self._global_lock: + self._sandboxes.pop(sandbox_id, None) + self._last_used.pop(sandbox_id, None) + self._locks.pop(sandbox_id, None) + logger.info(f"Deleted sandbox {sandbox_id}") + except Exception as e: + logger.error(f"Error during cleanup of sandbox {sandbox_id}: {e}") + + async def delete_sandbox(self, sandbox_id: str) -> None: + """Deletes specified sandbox. + + Args: + sandbox_id: Sandbox ID. + """ + if sandbox_id not in self._sandboxes: + return + + try: + await self._safe_delete_sandbox(sandbox_id) + except Exception as e: + logger.error(f"Failed to delete sandbox {sandbox_id}: {e}") + + async def __aenter__(self) -> "SandboxManager": + """Async context manager entry.""" + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: + """Async context manager exit.""" + await self.cleanup() + + def get_stats(self) -> Dict: + """Gets manager statistics. + + Returns: + Dict: Statistics information. + """ + return { + "total_sandboxes": len(self._sandboxes), + "active_operations": len(self._active_operations), + "max_sandboxes": self.max_sandboxes, + "idle_timeout": self.idle_timeout, + "cleanup_interval": self.cleanup_interval, + "is_shutting_down": self._is_shutting_down, + } diff --git a/app/sandbox/core/sandbox.py b/app/sandbox/core/sandbox.py new file mode 100644 index 0000000..c57b3f2 --- /dev/null +++ b/app/sandbox/core/sandbox.py @@ -0,0 +1,462 @@ +import asyncio +import io +import os +import tarfile +import tempfile +import uuid +from typing import Dict, Optional + +import docker +from docker.errors import NotFound +from docker.models.containers import Container + +from app.config import SandboxSettings +from app.sandbox.core.exceptions import SandboxTimeoutError +from app.sandbox.core.terminal import AsyncDockerizedTerminal + + +class DockerSandbox: + """Docker sandbox environment. + + Provides a containerized execution environment with resource limits, + file operations, and command execution capabilities. + + Attributes: + config: Sandbox configuration. + volume_bindings: Volume mapping configuration. + client: Docker client. + container: Docker container instance. + terminal: Container terminal interface. + """ + + def __init__( + self, + config: Optional[SandboxSettings] = None, + volume_bindings: Optional[Dict[str, str]] = None, + ): + """Initializes a sandbox instance. + + Args: + config: Sandbox configuration. Default configuration used if None. + volume_bindings: Volume mappings in {host_path: container_path} format. + """ + self.config = config or SandboxSettings() + self.volume_bindings = volume_bindings or {} + self.client = docker.from_env() + self.container: Optional[Container] = None + self.terminal: Optional[AsyncDockerizedTerminal] = None + + async def create(self) -> "DockerSandbox": + """Creates and starts the sandbox container. + + Returns: + Current sandbox instance. + + Raises: + docker.errors.APIError: If Docker API call fails. + RuntimeError: If container creation or startup fails. + """ + try: + # Prepare container config + host_config = self.client.api.create_host_config( + mem_limit=self.config.memory_limit, + cpu_period=100000, + cpu_quota=int(100000 * self.config.cpu_limit), + network_mode="none" if not self.config.network_enabled else "bridge", + binds=self._prepare_volume_bindings(), + ) + + # Generate unique container name with sandbox_ prefix + container_name = f"sandbox_{uuid.uuid4().hex[:8]}" + + # Create container + container = await asyncio.to_thread( + self.client.api.create_container, + image=self.config.image, + command="tail -f /dev/null", + hostname="sandbox", + working_dir=self.config.work_dir, + host_config=host_config, + name=container_name, + tty=True, + detach=True, + ) + + self.container = self.client.containers.get(container["Id"]) + + # Start container + await asyncio.to_thread(self.container.start) + + # Initialize terminal + self.terminal = AsyncDockerizedTerminal( + container["Id"], + self.config.work_dir, + env_vars={"PYTHONUNBUFFERED": "1"} + # Ensure Python output is not buffered + ) + await self.terminal.init() + + return self + + except Exception as e: + await self.cleanup() # Ensure resources are cleaned up + raise RuntimeError(f"Failed to create sandbox: {e}") from e + + def _prepare_volume_bindings(self) -> Dict[str, Dict[str, str]]: + """Prepares volume binding configuration. + + Returns: + Volume binding configuration dictionary. + """ + bindings = {} + + # Create and add working directory mapping + work_dir = self._ensure_host_dir(self.config.work_dir) + bindings[work_dir] = {"bind": self.config.work_dir, "mode": "rw"} + + # Add custom volume bindings + for host_path, container_path in self.volume_bindings.items(): + bindings[host_path] = {"bind": container_path, "mode": "rw"} + + return bindings + + @staticmethod + def _ensure_host_dir(path: str) -> str: + """Ensures directory exists on the host. + + Args: + path: Directory path. + + Returns: + Actual path on the host. + """ + host_path = os.path.join( + tempfile.gettempdir(), + f"sandbox_{os.path.basename(path)}_{os.urandom(4).hex()}", + ) + os.makedirs(host_path, exist_ok=True) + return host_path + + async def run_command(self, cmd: str, timeout: Optional[int] = None) -> str: + """Runs a command in the sandbox. + + Args: + cmd: Command to execute. + timeout: Timeout in seconds. + + Returns: + Command output as string. + + Raises: + RuntimeError: If sandbox not initialized or command execution fails. + TimeoutError: If command execution times out. + """ + if not self.terminal: + raise RuntimeError("Sandbox not initialized") + + try: + return await self.terminal.run_command( + cmd, timeout=timeout or self.config.timeout + ) + except TimeoutError: + raise SandboxTimeoutError( + f"Command execution timed out after {timeout or self.config.timeout} seconds" + ) + + async def read_file(self, path: str) -> str: + """Reads a file from the container. + + Args: + path: File path. + + Returns: + File contents as string. + + Raises: + FileNotFoundError: If file does not exist. + RuntimeError: If read operation fails. + """ + if not self.container: + raise RuntimeError("Sandbox not initialized") + + try: + # Get file archive + resolved_path = self._safe_resolve_path(path) + tar_stream, _ = await asyncio.to_thread( + self.container.get_archive, resolved_path + ) + + # Read file content from tar stream + content = await self._read_from_tar(tar_stream) + return content.decode("utf-8") + + except NotFound: + raise FileNotFoundError(f"File not found: {path}") + except Exception as e: + raise RuntimeError(f"Failed to read file: {e}") + + async def write_file(self, path: str, content: str) -> None: + """Writes content to a file in the container. + + Args: + path: Target path. + content: File content. + + Raises: + RuntimeError: If write operation fails. + """ + if not self.container: + raise RuntimeError("Sandbox not initialized") + + try: + resolved_path = self._safe_resolve_path(path) + parent_dir = os.path.dirname(resolved_path) + + # Create parent directory + if parent_dir: + await self.run_command(f"mkdir -p {parent_dir}") + + # Prepare file data + tar_stream = await self._create_tar_stream( + os.path.basename(path), content.encode("utf-8") + ) + + # Write file + await asyncio.to_thread( + self.container.put_archive, parent_dir or "/", tar_stream + ) + + except Exception as e: + raise RuntimeError(f"Failed to write file: {e}") + + def _safe_resolve_path(self, path: str) -> str: + """Safely resolves container path, preventing path traversal. + + Args: + path: Original path. + + Returns: + Resolved absolute path. + + Raises: + ValueError: If path contains potentially unsafe patterns. + """ + # Check for path traversal attempts + if ".." in path.split("/"): + raise ValueError("Path contains potentially unsafe patterns") + + resolved = ( + os.path.join(self.config.work_dir, path) + if not os.path.isabs(path) + else path + ) + return resolved + + async def copy_from(self, src_path: str, dst_path: str) -> None: + """Copies a file from the container. + + Args: + src_path: Source file path (container). + dst_path: Destination path (host). + + Raises: + FileNotFoundError: If source file does not exist. + RuntimeError: If copy operation fails. + """ + try: + # Ensure destination file's parent directory exists + parent_dir = os.path.dirname(dst_path) + if parent_dir: + os.makedirs(parent_dir, exist_ok=True) + + # Get file stream + resolved_src = self._safe_resolve_path(src_path) + stream, stat = await asyncio.to_thread( + self.container.get_archive, resolved_src + ) + + # Create temporary directory to extract file + with tempfile.TemporaryDirectory() as tmp_dir: + # Write stream to temporary file + tar_path = os.path.join(tmp_dir, "temp.tar") + with open(tar_path, "wb") as f: + for chunk in stream: + f.write(chunk) + + # Extract file + with tarfile.open(tar_path) as tar: + members = tar.getmembers() + if not members: + raise FileNotFoundError(f"Source file is empty: {src_path}") + + # If destination is a directory, we should preserve relative path structure + if os.path.isdir(dst_path): + tar.extractall(dst_path) + else: + # If destination is a file, we only extract the source file's content + if len(members) > 1: + raise RuntimeError( + f"Source path is a directory but destination is a file: {src_path}" + ) + + with open(dst_path, "wb") as dst: + src_file = tar.extractfile(members[0]) + if src_file is None: + raise RuntimeError( + f"Failed to extract file: {src_path}" + ) + dst.write(src_file.read()) + + except docker.errors.NotFound: + raise FileNotFoundError(f"Source file not found: {src_path}") + except Exception as e: + raise RuntimeError(f"Failed to copy file: {e}") + + async def copy_to(self, src_path: str, dst_path: str) -> None: + """Copies a file to the container. + + Args: + src_path: Source file path (host). + dst_path: Destination path (container). + + Raises: + FileNotFoundError: If source file does not exist. + RuntimeError: If copy operation fails. + """ + try: + if not os.path.exists(src_path): + raise FileNotFoundError(f"Source file not found: {src_path}") + + # Create destination directory in container + resolved_dst = self._safe_resolve_path(dst_path) + container_dir = os.path.dirname(resolved_dst) + if container_dir: + await self.run_command(f"mkdir -p {container_dir}") + + # Create tar file to upload + with tempfile.TemporaryDirectory() as tmp_dir: + tar_path = os.path.join(tmp_dir, "temp.tar") + with tarfile.open(tar_path, "w") as tar: + # Handle directory source path + if os.path.isdir(src_path): + os.path.basename(src_path.rstrip("/")) + for root, _, files in os.walk(src_path): + for file in files: + file_path = os.path.join(root, file) + arcname = os.path.join( + os.path.basename(dst_path), + os.path.relpath(file_path, src_path), + ) + tar.add(file_path, arcname=arcname) + else: + # Add single file to tar + tar.add(src_path, arcname=os.path.basename(dst_path)) + + # Read tar file content + with open(tar_path, "rb") as f: + data = f.read() + + # Upload to container + await asyncio.to_thread( + self.container.put_archive, + os.path.dirname(resolved_dst) or "/", + data, + ) + + # Verify file was created successfully + try: + await self.run_command(f"test -e {resolved_dst}") + except Exception: + raise RuntimeError(f"Failed to verify file creation: {dst_path}") + + except FileNotFoundError: + raise + except Exception as e: + raise RuntimeError(f"Failed to copy file: {e}") + + @staticmethod + async def _create_tar_stream(name: str, content: bytes) -> io.BytesIO: + """Creates a tar file stream. + + Args: + name: Filename. + content: File content. + + Returns: + Tar file stream. + """ + tar_stream = io.BytesIO() + with tarfile.open(fileobj=tar_stream, mode="w") as tar: + tarinfo = tarfile.TarInfo(name=name) + tarinfo.size = len(content) + tar.addfile(tarinfo, io.BytesIO(content)) + tar_stream.seek(0) + return tar_stream + + @staticmethod + async def _read_from_tar(tar_stream) -> bytes: + """Reads file content from a tar stream. + + Args: + tar_stream: Tar file stream. + + Returns: + File content. + + Raises: + RuntimeError: If read operation fails. + """ + with tempfile.NamedTemporaryFile() as tmp: + for chunk in tar_stream: + tmp.write(chunk) + tmp.seek(0) + + with tarfile.open(fileobj=tmp) as tar: + member = tar.next() + if not member: + raise RuntimeError("Empty tar archive") + + file_content = tar.extractfile(member) + if not file_content: + raise RuntimeError("Failed to extract file content") + + return file_content.read() + + async def cleanup(self) -> None: + """Cleans up sandbox resources.""" + errors = [] + try: + if self.terminal: + try: + await self.terminal.close() + except Exception as e: + errors.append(f"Terminal cleanup error: {e}") + finally: + self.terminal = None + + if self.container: + try: + await asyncio.to_thread(self.container.stop, timeout=5) + except Exception as e: + errors.append(f"Container stop error: {e}") + + try: + await asyncio.to_thread(self.container.remove, force=True) + except Exception as e: + errors.append(f"Container remove error: {e}") + finally: + self.container = None + + except Exception as e: + errors.append(f"General cleanup error: {e}") + + if errors: + print(f"Warning: Errors during cleanup: {', '.join(errors)}") + + async def __aenter__(self) -> "DockerSandbox": + """Async context manager entry.""" + return await self.create() + + async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: + """Async context manager exit.""" + await self.cleanup() diff --git a/app/sandbox/core/terminal.py b/app/sandbox/core/terminal.py new file mode 100644 index 0000000..aee5184 --- /dev/null +++ b/app/sandbox/core/terminal.py @@ -0,0 +1,346 @@ +""" +Asynchronous Docker Terminal + +This module provides asynchronous terminal functionality for Docker containers, +allowing interactive command execution with timeout control. +""" + +import asyncio +import re +import socket +from typing import Dict, Optional, Tuple, Union + +import docker +from docker import APIClient +from docker.errors import APIError +from docker.models.containers import Container + + +class DockerSession: + def __init__(self, container_id: str) -> None: + """Initializes a Docker session. + + Args: + container_id: ID of the Docker container. + """ + self.api = APIClient() + self.container_id = container_id + self.exec_id = None + self.socket = None + + async def create(self, working_dir: str, env_vars: Dict[str, str]) -> None: + """Creates an interactive session with the container. + + Args: + working_dir: Working directory inside the container. + env_vars: Environment variables to set. + + Raises: + RuntimeError: If socket connection fails. + """ + startup_command = [ + "bash", + "-c", + f"cd {working_dir} && " + "PROMPT_COMMAND='' " + "PS1='$ ' " + "exec bash --norc --noprofile", + ] + + exec_data = self.api.exec_create( + self.container_id, + startup_command, + stdin=True, + tty=True, + stdout=True, + stderr=True, + privileged=True, + user="root", + environment={**env_vars, "TERM": "dumb", "PS1": "$ ", "PROMPT_COMMAND": ""}, + ) + self.exec_id = exec_data["Id"] + + socket_data = self.api.exec_start( + self.exec_id, socket=True, tty=True, stream=True, demux=True + ) + + if hasattr(socket_data, "_sock"): + self.socket = socket_data._sock + self.socket.setblocking(False) + else: + raise RuntimeError("Failed to get socket connection") + + await self._read_until_prompt() + + async def close(self) -> None: + """Cleans up session resources. + + 1. Sends exit command + 2. Closes socket connection + 3. Checks and cleans up exec instance + """ + try: + if self.socket: + # Send exit command to close bash session + try: + self.socket.sendall(b"exit\n") + # Allow time for command execution + await asyncio.sleep(0.1) + except: + pass # Ignore sending errors, continue cleanup + + # Close socket connection + try: + self.socket.shutdown(socket.SHUT_RDWR) + except: + pass # Some platforms may not support shutdown + + self.socket.close() + self.socket = None + + if self.exec_id: + try: + # Check exec instance status + exec_inspect = self.api.exec_inspect(self.exec_id) + if exec_inspect.get("Running", False): + # If still running, wait for it to complete + await asyncio.sleep(0.5) + except: + pass # Ignore inspection errors, continue cleanup + + self.exec_id = None + + except Exception as e: + # Log error but don't raise, ensure cleanup continues + print(f"Warning: Error during session cleanup: {e}") + + async def _read_until_prompt(self) -> str: + """Reads output until prompt is found. + + Returns: + String containing output up to the prompt. + + Raises: + socket.error: If socket communication fails. + """ + buffer = b"" + while b"$ " not in buffer: + try: + chunk = self.socket.recv(4096) + if chunk: + buffer += chunk + except socket.error as e: + if e.errno == socket.EWOULDBLOCK: + await asyncio.sleep(0.1) + continue + raise + return buffer.decode("utf-8") + + async def execute(self, command: str, timeout: Optional[int] = None) -> str: + """Executes a command and returns cleaned output. + + Args: + command: Shell command to execute. + timeout: Maximum execution time in seconds. + + Returns: + Command output as string with prompt markers removed. + + Raises: + RuntimeError: If session not initialized or execution fails. + TimeoutError: If command execution exceeds timeout. + """ + if not self.socket: + raise RuntimeError("Session not initialized") + + try: + # Sanitize command to prevent shell injection + sanitized_command = self._sanitize_command(command) + full_command = f"{sanitized_command}\necho $?\n" + self.socket.sendall(full_command.encode()) + + async def read_output() -> str: + buffer = b"" + result_lines = [] + command_sent = False + + while True: + try: + chunk = self.socket.recv(4096) + if not chunk: + break + + buffer += chunk + lines = buffer.split(b"\n") + + buffer = lines[-1] + lines = lines[:-1] + + for line in lines: + line = line.rstrip(b"\r") + + if not command_sent: + command_sent = True + continue + + if line.strip() == b"echo $?" or line.strip().isdigit(): + continue + + if line.strip(): + result_lines.append(line) + + if buffer.endswith(b"$ "): + break + + except socket.error as e: + if e.errno == socket.EWOULDBLOCK: + await asyncio.sleep(0.1) + continue + raise + + output = b"\n".join(result_lines).decode("utf-8") + output = re.sub(r"\n\$ echo \$\$?.*$", "", output) + + return output + + if timeout: + result = await asyncio.wait_for(read_output(), timeout) + else: + result = await read_output() + + return result.strip() + + except asyncio.TimeoutError: + raise TimeoutError(f"Command execution timed out after {timeout} seconds") + except Exception as e: + raise RuntimeError(f"Failed to execute command: {e}") + + def _sanitize_command(self, command: str) -> str: + """Sanitizes the command string to prevent shell injection. + + Args: + command: Raw command string. + + Returns: + Sanitized command string. + + Raises: + ValueError: If command contains potentially dangerous patterns. + """ + + # Additional checks for specific risky commands + risky_commands = [ + "rm -rf /", + "rm -rf /*", + "mkfs", + "dd if=/dev/zero", + ":(){:|:&};:", + "chmod -R 777 /", + "chown -R", + ] + + for risky in risky_commands: + if risky in command.lower(): + raise ValueError( + f"Command contains potentially dangerous operation: {risky}" + ) + + return command + + +class AsyncDockerizedTerminal: + def __init__( + self, + container: Union[str, Container], + working_dir: str = "/workspace", + env_vars: Optional[Dict[str, str]] = None, + default_timeout: int = 60, + ) -> None: + """Initializes an asynchronous terminal for Docker containers. + + Args: + container: Docker container ID or Container object. + working_dir: Working directory inside the container. + env_vars: Environment variables to set. + default_timeout: Default command execution timeout in seconds. + """ + self.client = docker.from_env() + self.container = ( + container + if isinstance(container, Container) + else self.client.containers.get(container) + ) + self.working_dir = working_dir + self.env_vars = env_vars or {} + self.default_timeout = default_timeout + self.session = None + + async def init(self) -> None: + """Initializes the terminal environment. + + Ensures working directory exists and creates an interactive session. + + Raises: + RuntimeError: If initialization fails. + """ + await self._ensure_workdir() + + self.session = DockerSession(self.container.id) + await self.session.create(self.working_dir, self.env_vars) + + async def _ensure_workdir(self) -> None: + """Ensures working directory exists in container. + + Raises: + RuntimeError: If directory creation fails. + """ + try: + await self._exec_simple(f"mkdir -p {self.working_dir}") + except APIError as e: + raise RuntimeError(f"Failed to create working directory: {e}") + + async def _exec_simple(self, cmd: str) -> Tuple[int, str]: + """Executes a simple command using Docker's exec_run. + + Args: + cmd: Command to execute. + + Returns: + Tuple of (exit_code, output). + """ + result = await asyncio.to_thread( + self.container.exec_run, cmd, environment=self.env_vars + ) + return result.exit_code, result.output.decode("utf-8") + + async def run_command(self, cmd: str, timeout: Optional[int] = None) -> str: + """Runs a command in the container with timeout. + + Args: + cmd: Shell command to execute. + timeout: Maximum execution time in seconds. + + Returns: + Command output as string. + + Raises: + RuntimeError: If terminal not initialized. + """ + if not self.session: + raise RuntimeError("Terminal not initialized") + + return await self.session.execute(cmd, timeout=timeout or self.default_timeout) + + async def close(self) -> None: + """Closes the terminal session.""" + if self.session: + await self.session.close() + + async def __aenter__(self) -> "AsyncDockerizedTerminal": + """Async context manager entry.""" + await self.init() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: + """Async context manager exit.""" + await self.close() diff --git a/app/schema.py b/app/schema.py new file mode 100644 index 0000000..5f743f9 --- /dev/null +++ b/app/schema.py @@ -0,0 +1,187 @@ +from enum import Enum +from typing import Any, List, Literal, Optional, Union + +from pydantic import BaseModel, Field + + +class Role(str, Enum): + """Message role options""" + + SYSTEM = "system" + USER = "user" + ASSISTANT = "assistant" + TOOL = "tool" + + +ROLE_VALUES = tuple(role.value for role in Role) +ROLE_TYPE = Literal[ROLE_VALUES] # type: ignore + + +class ToolChoice(str, Enum): + """Tool choice options""" + + NONE = "none" + AUTO = "auto" + REQUIRED = "required" + + +TOOL_CHOICE_VALUES = tuple(choice.value for choice in ToolChoice) +TOOL_CHOICE_TYPE = Literal[TOOL_CHOICE_VALUES] # type: ignore + + +class AgentState(str, Enum): + """Agent execution states""" + + IDLE = "IDLE" + RUNNING = "RUNNING" + FINISHED = "FINISHED" + ERROR = "ERROR" + + +class Function(BaseModel): + name: str + arguments: str + + +class ToolCall(BaseModel): + """Represents a tool/function call in a message""" + + id: str + type: str = "function" + function: Function + + +class Message(BaseModel): + """Represents a chat message in the conversation""" + + role: ROLE_TYPE = Field(...) # type: ignore + content: Optional[str] = Field(default=None) + tool_calls: Optional[List[ToolCall]] = Field(default=None) + name: Optional[str] = Field(default=None) + tool_call_id: Optional[str] = Field(default=None) + base64_image: Optional[str] = Field(default=None) + + def __add__(self, other) -> List["Message"]: + """支持 Message + list 或 Message + Message 的操作""" + if isinstance(other, list): + return [self] + other + elif isinstance(other, Message): + return [self, other] + else: + raise TypeError( + f"unsupported operand type(s) for +: '{type(self).__name__}' and '{type(other).__name__}'" + ) + + def __radd__(self, other) -> List["Message"]: + """支持 list + Message 的操作""" + if isinstance(other, list): + return other + [self] + else: + raise TypeError( + f"unsupported operand type(s) for +: '{type(other).__name__}' and '{type(self).__name__}'" + ) + + def to_dict(self) -> dict: + """Convert message to dictionary format""" + message = {"role": self.role} + if self.content is not None: + message["content"] = self.content + if self.tool_calls is not None: + message["tool_calls"] = [tool_call.dict() for tool_call in self.tool_calls] + if self.name is not None: + message["name"] = self.name + if self.tool_call_id is not None: + message["tool_call_id"] = self.tool_call_id + if self.base64_image is not None: + message["base64_image"] = self.base64_image + return message + + @classmethod + def user_message( + cls, content: str, base64_image: Optional[str] = None + ) -> "Message": + """Create a user message""" + return cls(role=Role.USER, content=content, base64_image=base64_image) + + @classmethod + def system_message(cls, content: str) -> "Message": + """Create a system message""" + return cls(role=Role.SYSTEM, content=content) + + @classmethod + def assistant_message( + cls, content: Optional[str] = None, base64_image: Optional[str] = None + ) -> "Message": + """Create an assistant message""" + return cls(role=Role.ASSISTANT, content=content, base64_image=base64_image) + + @classmethod + def tool_message( + cls, content: str, name, tool_call_id: str, base64_image: Optional[str] = None + ) -> "Message": + """Create a tool message""" + return cls( + role=Role.TOOL, + content=content, + name=name, + tool_call_id=tool_call_id, + base64_image=base64_image, + ) + + @classmethod + def from_tool_calls( + cls, + tool_calls: List[Any], + content: Union[str, List[str]] = "", + base64_image: Optional[str] = None, + **kwargs, + ): + """Create ToolCallsMessage from raw tool calls. + + Args: + tool_calls: Raw tool calls from LLM + content: Optional message content + base64_image: Optional base64 encoded image + """ + formatted_calls = [ + {"id": call.id, "function": call.function.model_dump(), "type": "function"} + for call in tool_calls + ] + return cls( + role=Role.ASSISTANT, + content=content, + tool_calls=formatted_calls, + base64_image=base64_image, + **kwargs, + ) + + +class Memory(BaseModel): + messages: List[Message] = Field(default_factory=list) + max_messages: int = Field(default=100) + + def add_message(self, message: Message) -> None: + """Add a message to memory""" + self.messages.append(message) + # Optional: Implement message limit + if len(self.messages) > self.max_messages: + self.messages = self.messages[-self.max_messages :] + + def add_messages(self, messages: List[Message]) -> None: + """Add multiple messages to memory""" + self.messages.extend(messages) + # Optional: Implement message limit + if len(self.messages) > self.max_messages: + self.messages = self.messages[-self.max_messages :] + + def clear(self) -> None: + """Clear all messages""" + self.messages.clear() + + def get_recent_messages(self, n: int) -> List[Message]: + """Get n most recent messages""" + return self.messages[-n:] + + def to_dict_list(self) -> List[dict]: + """Convert messages to list of dicts""" + return [msg.to_dict() for msg in self.messages] diff --git a/app/tool/__init__.py b/app/tool/__init__.py new file mode 100644 index 0000000..636e9b8 --- /dev/null +++ b/app/tool/__init__.py @@ -0,0 +1,24 @@ +from app.tool.base import BaseTool +from app.tool.bash import Bash +from app.tool.browser_use_tool import BrowserUseTool +from app.tool.crawl4ai import Crawl4aiTool +from app.tool.create_chat_completion import CreateChatCompletion +from app.tool.planning import PlanningTool +from app.tool.str_replace_editor import StrReplaceEditor +from app.tool.terminate import Terminate +from app.tool.tool_collection import ToolCollection +from app.tool.web_search import WebSearch + + +__all__ = [ + "BaseTool", + "Bash", + "BrowserUseTool", + "Terminate", + "StrReplaceEditor", + "WebSearch", + "ToolCollection", + "CreateChatCompletion", + "PlanningTool", + "Crawl4aiTool", +] diff --git a/app/tool/ask_human.py b/app/tool/ask_human.py new file mode 100644 index 0000000..5fd4550 --- /dev/null +++ b/app/tool/ask_human.py @@ -0,0 +1,21 @@ +from app.tool import BaseTool + + +class AskHuman(BaseTool): + """Add a tool to ask human for help.""" + + name: str = "ask_human" + description: str = "Use this tool to ask human for help." + parameters: str = { + "type": "object", + "properties": { + "inquire": { + "type": "string", + "description": "The question you want to ask human.", + } + }, + "required": ["inquire"], + } + + async def execute(self, inquire: str) -> str: + return input(f"""Bot: {inquire}\n\nYou: """).strip() diff --git a/app/tool/base.py b/app/tool/base.py new file mode 100644 index 0000000..fdb8b7d --- /dev/null +++ b/app/tool/base.py @@ -0,0 +1,181 @@ +import json +from abc import ABC, abstractmethod +from typing import Any, Dict, Optional, Union + +from pydantic import BaseModel, Field + +from app.utils.logger import logger + + +# class BaseTool(ABC, BaseModel): +# name: str +# description: str +# parameters: Optional[dict] = None + +# class Config: +# arbitrary_types_allowed = True + +# async def __call__(self, **kwargs) -> Any: +# """Execute the tool with given parameters.""" +# return await self.execute(**kwargs) + +# @abstractmethod +# async def execute(self, **kwargs) -> Any: +# """Execute the tool with given parameters.""" + +# def to_param(self) -> Dict: +# """Convert tool to function call format.""" +# return { +# "type": "function", +# "function": { +# "name": self.name, +# "description": self.description, +# "parameters": self.parameters, +# }, +# } + + +class ToolResult(BaseModel): + """Represents the result of a tool execution.""" + + output: Any = Field(default=None) + error: Optional[str] = Field(default=None) + base64_image: Optional[str] = Field(default=None) + system: Optional[str] = Field(default=None) + + class Config: + arbitrary_types_allowed = True + + def __bool__(self): + return any(getattr(self, field) for field in self.__fields__) + + def __add__(self, other: "ToolResult"): + def combine_fields( + field: Optional[str], other_field: Optional[str], concatenate: bool = True + ): + if field and other_field: + if concatenate: + return field + other_field + raise ValueError("Cannot combine tool results") + return field or other_field + + return ToolResult( + output=combine_fields(self.output, other.output), + error=combine_fields(self.error, other.error), + base64_image=combine_fields(self.base64_image, other.base64_image, False), + system=combine_fields(self.system, other.system), + ) + + def __str__(self): + return f"Error: {self.error}" if self.error else self.output + + def replace(self, **kwargs): + """Returns a new ToolResult with the given fields replaced.""" + # return self.copy(update=kwargs) + return type(self)(**{**self.dict(), **kwargs}) + + +class BaseTool(ABC, BaseModel): + """Consolidated base class for all tools combining BaseModel and Tool functionality. + + Provides: + - Pydantic model validation + - Schema registration + - Standardized result handling + - Abstract execution interface + + Attributes: + name (str): Tool name + description (str): Tool description + parameters (dict): Tool parameters schema + _schemas (Dict[str, List[ToolSchema]]): Registered method schemas + """ + + name: str + description: str + parameters: Optional[dict] = None + # _schemas: Dict[str, List[ToolSchema]] = {} + + class Config: + arbitrary_types_allowed = True + underscore_attrs_are_private = False + + # def __init__(self, **data): + # """Initialize tool with model validation and schema registration.""" + # super().__init__(**data) + # logger.debug(f"Initializing tool class: {self.__class__.__name__}") + # self._register_schemas() + + # def _register_schemas(self): + # """Register schemas from all decorated methods.""" + # for name, method in inspect.getmembers(self, predicate=inspect.ismethod): + # if hasattr(method, 'tool_schemas'): + # self._schemas[name] = method.tool_schemas + # logger.debug(f"Registered schemas for method '{name}' in {self.__class__.__name__}") + + async def __call__(self, **kwargs) -> Any: + """Execute the tool with given parameters.""" + return await self.execute(**kwargs) + + @abstractmethod + async def execute(self, **kwargs) -> Any: + """Execute the tool with given parameters.""" + + def to_param(self) -> Dict: + """Convert tool to function call format. + + Returns: + Dictionary with tool metadata in OpenAI function calling format + """ + return { + "type": "function", + "function": { + "name": self.name, + "description": self.description, + "parameters": self.parameters, + }, + } + + # def get_schemas(self) -> Dict[str, List[ToolSchema]]: + # """Get all registered tool schemas. + + # Returns: + # Dict mapping method names to their schema definitions + # """ + # return self._schemas + + def success_response(self, data: Union[Dict[str, Any], str]) -> ToolResult: + """Create a successful tool result. + + Args: + data: Result data (dictionary or string) + + Returns: + ToolResult with success=True and formatted output + """ + if isinstance(data, str): + text = data + else: + text = json.dumps(data, indent=2) + logger.debug(f"Created success response for {self.__class__.__name__}") + return ToolResult(output=text) + + def fail_response(self, msg: str) -> ToolResult: + """Create a failed tool result. + + Args: + msg: Error message describing the failure + + Returns: + ToolResult with success=False and error message + """ + logger.debug(f"Tool {self.__class__.__name__} returned failed result: {msg}") + return ToolResult(error=msg) + + +class CLIResult(ToolResult): + """A ToolResult that can be rendered as a CLI output.""" + + +class ToolFailure(ToolResult): + """A ToolResult that represents a failure.""" diff --git a/app/tool/bash.py b/app/tool/bash.py new file mode 100644 index 0000000..c6b9072 --- /dev/null +++ b/app/tool/bash.py @@ -0,0 +1,158 @@ +import asyncio +import os +from typing import Optional + +from app.exceptions import ToolError +from app.tool.base import BaseTool, CLIResult + + +_BASH_DESCRIPTION = """Execute a bash command in the terminal. +* Long running commands: For commands that may run indefinitely, it should be run in the background and the output should be redirected to a file, e.g. command = `python3 app.py > server.log 2>&1 &`. +* Interactive: If a bash command returns exit code `-1`, this means the process is not yet finished. The assistant must then send a second call to terminal with an empty `command` (which will retrieve any additional logs), or it can send additional text (set `command` to the text) to STDIN of the running process, or it can send command=`ctrl+c` to interrupt the process. +* Timeout: If a command execution result says "Command timed out. Sending SIGINT to the process", the assistant should retry running the command in the background. +""" + + +class _BashSession: + """A session of a bash shell.""" + + _started: bool + _process: asyncio.subprocess.Process + + command: str = "/bin/bash" + _output_delay: float = 0.2 # seconds + _timeout: float = 120.0 # seconds + _sentinel: str = "<>" + + def __init__(self): + self._started = False + self._timed_out = False + + async def start(self): + if self._started: + return + + self._process = await asyncio.create_subprocess_shell( + self.command, + preexec_fn=os.setsid, + shell=True, + bufsize=0, + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + self._started = True + + def stop(self): + """Terminate the bash shell.""" + if not self._started: + raise ToolError("Session has not started.") + if self._process.returncode is not None: + return + self._process.terminate() + + async def run(self, command: str): + """Execute a command in the bash shell.""" + if not self._started: + raise ToolError("Session has not started.") + if self._process.returncode is not None: + return CLIResult( + system="tool must be restarted", + error=f"bash has exited with returncode {self._process.returncode}", + ) + if self._timed_out: + raise ToolError( + f"timed out: bash has not returned in {self._timeout} seconds and must be restarted", + ) + + # we know these are not None because we created the process with PIPEs + assert self._process.stdin + assert self._process.stdout + assert self._process.stderr + + # send command to the process + self._process.stdin.write( + command.encode() + f"; echo '{self._sentinel}'\n".encode() + ) + await self._process.stdin.drain() + + # read output from the process, until the sentinel is found + try: + async with asyncio.timeout(self._timeout): + while True: + await asyncio.sleep(self._output_delay) + # if we read directly from stdout/stderr, it will wait forever for + # EOF. use the StreamReader buffer directly instead. + output = ( + self._process.stdout._buffer.decode() + ) # pyright: ignore[reportAttributeAccessIssue] + if self._sentinel in output: + # strip the sentinel and break + output = output[: output.index(self._sentinel)] + break + except asyncio.TimeoutError: + self._timed_out = True + raise ToolError( + f"timed out: bash has not returned in {self._timeout} seconds and must be restarted", + ) from None + + if output.endswith("\n"): + output = output[:-1] + + error = ( + self._process.stderr._buffer.decode() + ) # pyright: ignore[reportAttributeAccessIssue] + if error.endswith("\n"): + error = error[:-1] + + # clear the buffers so that the next output can be read correctly + self._process.stdout._buffer.clear() # pyright: ignore[reportAttributeAccessIssue] + self._process.stderr._buffer.clear() # pyright: ignore[reportAttributeAccessIssue] + + return CLIResult(output=output, error=error) + + +class Bash(BaseTool): + """A tool for executing bash commands""" + + name: str = "bash" + description: str = _BASH_DESCRIPTION + parameters: dict = { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The bash command to execute. Can be empty to view additional logs when previous exit code is `-1`. Can be `ctrl+c` to interrupt the currently running process.", + }, + }, + "required": ["command"], + } + + _session: Optional[_BashSession] = None + + async def execute( + self, command: str | None = None, restart: bool = False, **kwargs + ) -> CLIResult: + if restart: + if self._session: + self._session.stop() + self._session = _BashSession() + await self._session.start() + + return CLIResult(system="tool has been restarted.") + + if self._session is None: + self._session = _BashSession() + await self._session.start() + + if command is not None: + return await self._session.run(command) + + raise ToolError("no command provided.") + + +if __name__ == "__main__": + bash = Bash() + rst = asyncio.run(bash.execute("ls -l")) + print(rst) diff --git a/app/tool/browser_use_tool.py b/app/tool/browser_use_tool.py new file mode 100644 index 0000000..449e8e5 --- /dev/null +++ b/app/tool/browser_use_tool.py @@ -0,0 +1,567 @@ +import asyncio +import base64 +import json +from typing import Generic, Optional, TypeVar + +from browser_use import Browser as BrowserUseBrowser +from browser_use import BrowserConfig +from browser_use.browser.context import BrowserContext, BrowserContextConfig +from browser_use.dom.service import DomService +from pydantic import Field, field_validator +from pydantic_core.core_schema import ValidationInfo + +from app.config import config +from app.llm import LLM +from app.tool.base import BaseTool, ToolResult +from app.tool.web_search import WebSearch + + +_BROWSER_DESCRIPTION = """\ +A powerful browser automation tool that allows interaction with web pages through various actions. +* This tool provides commands for controlling a browser session, navigating web pages, and extracting information +* It maintains state across calls, keeping the browser session alive until explicitly closed +* Use this when you need to browse websites, fill forms, click buttons, extract content, or perform web searches +* Each action requires specific parameters as defined in the tool's dependencies + +Key capabilities include: +* Navigation: Go to specific URLs, go back, search the web, or refresh pages +* Interaction: Click elements, input text, select from dropdowns, send keyboard commands +* Scrolling: Scroll up/down by pixel amount or scroll to specific text +* Content extraction: Extract and analyze content from web pages based on specific goals +* Tab management: Switch between tabs, open new tabs, or close tabs + +Note: When using element indices, refer to the numbered elements shown in the current browser state. +""" + +Context = TypeVar("Context") + + +class BrowserUseTool(BaseTool, Generic[Context]): + name: str = "browser_use" + description: str = _BROWSER_DESCRIPTION + parameters: dict = { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "go_to_url", + "click_element", + "input_text", + "scroll_down", + "scroll_up", + "scroll_to_text", + "send_keys", + "get_dropdown_options", + "select_dropdown_option", + "go_back", + "web_search", + "wait", + "extract_content", + "switch_tab", + "open_tab", + "close_tab", + ], + "description": "The browser action to perform", + }, + "url": { + "type": "string", + "description": "URL for 'go_to_url' or 'open_tab' actions", + }, + "index": { + "type": "integer", + "description": "Element index for 'click_element', 'input_text', 'get_dropdown_options', or 'select_dropdown_option' actions", + }, + "text": { + "type": "string", + "description": "Text for 'input_text', 'scroll_to_text', or 'select_dropdown_option' actions", + }, + "scroll_amount": { + "type": "integer", + "description": "Pixels to scroll (positive for down, negative for up) for 'scroll_down' or 'scroll_up' actions", + }, + "tab_id": { + "type": "integer", + "description": "Tab ID for 'switch_tab' action", + }, + "query": { + "type": "string", + "description": "Search query for 'web_search' action", + }, + "goal": { + "type": "string", + "description": "Extraction goal for 'extract_content' action", + }, + "keys": { + "type": "string", + "description": "Keys to send for 'send_keys' action", + }, + "seconds": { + "type": "integer", + "description": "Seconds to wait for 'wait' action", + }, + }, + "required": ["action"], + "dependencies": { + "go_to_url": ["url"], + "click_element": ["index"], + "input_text": ["index", "text"], + "switch_tab": ["tab_id"], + "open_tab": ["url"], + "scroll_down": ["scroll_amount"], + "scroll_up": ["scroll_amount"], + "scroll_to_text": ["text"], + "send_keys": ["keys"], + "get_dropdown_options": ["index"], + "select_dropdown_option": ["index", "text"], + "go_back": [], + "web_search": ["query"], + "wait": ["seconds"], + "extract_content": ["goal"], + }, + } + + lock: asyncio.Lock = Field(default_factory=asyncio.Lock) + browser: Optional[BrowserUseBrowser] = Field(default=None, exclude=True) + context: Optional[BrowserContext] = Field(default=None, exclude=True) + dom_service: Optional[DomService] = Field(default=None, exclude=True) + web_search_tool: WebSearch = Field(default_factory=WebSearch, exclude=True) + + # Context for generic functionality + tool_context: Optional[Context] = Field(default=None, exclude=True) + + llm: Optional[LLM] = Field(default_factory=LLM) + + @field_validator("parameters", mode="before") + def validate_parameters(cls, v: dict, info: ValidationInfo) -> dict: + if not v: + raise ValueError("Parameters cannot be empty") + return v + + async def _ensure_browser_initialized(self) -> BrowserContext: + """Ensure browser and context are initialized.""" + if self.browser is None: + browser_config_kwargs = {"headless": False, "disable_security": True} + + if config.browser_config: + from browser_use.browser.browser import ProxySettings + + # handle proxy settings. + if config.browser_config.proxy and config.browser_config.proxy.server: + browser_config_kwargs["proxy"] = ProxySettings( + server=config.browser_config.proxy.server, + username=config.browser_config.proxy.username, + password=config.browser_config.proxy.password, + ) + + browser_attrs = [ + "headless", + "disable_security", + "extra_chromium_args", + "chrome_instance_path", + "wss_url", + "cdp_url", + ] + + for attr in browser_attrs: + value = getattr(config.browser_config, attr, None) + if value is not None: + if not isinstance(value, list) or value: + browser_config_kwargs[attr] = value + + self.browser = BrowserUseBrowser(BrowserConfig(**browser_config_kwargs)) + + if self.context is None: + context_config = BrowserContextConfig() + + # if there is context config in the config, use it. + if ( + config.browser_config + and hasattr(config.browser_config, "new_context_config") + and config.browser_config.new_context_config + ): + context_config = config.browser_config.new_context_config + + self.context = await self.browser.new_context(context_config) + self.dom_service = DomService(await self.context.get_current_page()) + + return self.context + + async def execute( + self, + action: str, + url: Optional[str] = None, + index: Optional[int] = None, + text: Optional[str] = None, + scroll_amount: Optional[int] = None, + tab_id: Optional[int] = None, + query: Optional[str] = None, + goal: Optional[str] = None, + keys: Optional[str] = None, + seconds: Optional[int] = None, + **kwargs, + ) -> ToolResult: + """ + Execute a specified browser action. + + Args: + action: The browser action to perform + url: URL for navigation or new tab + index: Element index for click or input actions + text: Text for input action or search query + scroll_amount: Pixels to scroll for scroll action + tab_id: Tab ID for switch_tab action + query: Search query for Google search + goal: Extraction goal for content extraction + keys: Keys to send for keyboard actions + seconds: Seconds to wait + **kwargs: Additional arguments + + Returns: + ToolResult with the action's output or error + """ + async with self.lock: + try: + context = await self._ensure_browser_initialized() + + # Get max content length from config + max_content_length = getattr( + config.browser_config, "max_content_length", 2000 + ) + + # Navigation actions + if action == "go_to_url": + if not url: + return ToolResult( + error="URL is required for 'go_to_url' action" + ) + page = await context.get_current_page() + await page.goto(url) + await page.wait_for_load_state() + return ToolResult(output=f"Navigated to {url}") + + elif action == "go_back": + await context.go_back() + return ToolResult(output="Navigated back") + + elif action == "refresh": + await context.refresh_page() + return ToolResult(output="Refreshed current page") + + elif action == "web_search": + if not query: + return ToolResult( + error="Query is required for 'web_search' action" + ) + # Execute the web search and return results directly without browser navigation + search_response = await self.web_search_tool.execute( + query=query, fetch_content=True, num_results=1 + ) + # Navigate to the first search result + first_search_result = search_response.results[0] + url_to_navigate = first_search_result.url + + page = await context.get_current_page() + await page.goto(url_to_navigate) + await page.wait_for_load_state() + + return search_response + + # Element interaction actions + elif action == "click_element": + if index is None: + return ToolResult( + error="Index is required for 'click_element' action" + ) + element = await context.get_dom_element_by_index(index) + if not element: + return ToolResult(error=f"Element with index {index} not found") + download_path = await context._click_element_node(element) + output = f"Clicked element at index {index}" + if download_path: + output += f" - Downloaded file to {download_path}" + return ToolResult(output=output) + + elif action == "input_text": + if index is None or not text: + return ToolResult( + error="Index and text are required for 'input_text' action" + ) + element = await context.get_dom_element_by_index(index) + if not element: + return ToolResult(error=f"Element with index {index} not found") + await context._input_text_element_node(element, text) + return ToolResult( + output=f"Input '{text}' into element at index {index}" + ) + + elif action == "scroll_down" or action == "scroll_up": + direction = 1 if action == "scroll_down" else -1 + amount = ( + scroll_amount + if scroll_amount is not None + else context.config.browser_window_size["height"] + ) + await context.execute_javascript( + f"window.scrollBy(0, {direction * amount});" + ) + return ToolResult( + output=f"Scrolled {'down' if direction > 0 else 'up'} by {amount} pixels" + ) + + elif action == "scroll_to_text": + if not text: + return ToolResult( + error="Text is required for 'scroll_to_text' action" + ) + page = await context.get_current_page() + try: + locator = page.get_by_text(text, exact=False) + await locator.scroll_into_view_if_needed() + return ToolResult(output=f"Scrolled to text: '{text}'") + except Exception as e: + return ToolResult(error=f"Failed to scroll to text: {str(e)}") + + elif action == "send_keys": + if not keys: + return ToolResult( + error="Keys are required for 'send_keys' action" + ) + page = await context.get_current_page() + await page.keyboard.press(keys) + return ToolResult(output=f"Sent keys: {keys}") + + elif action == "get_dropdown_options": + if index is None: + return ToolResult( + error="Index is required for 'get_dropdown_options' action" + ) + element = await context.get_dom_element_by_index(index) + if not element: + return ToolResult(error=f"Element with index {index} not found") + page = await context.get_current_page() + options = await page.evaluate( + """ + (xpath) => { + const select = document.evaluate(xpath, document, null, + XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; + if (!select) return null; + return Array.from(select.options).map(opt => ({ + text: opt.text, + value: opt.value, + index: opt.index + })); + } + """, + element.xpath, + ) + return ToolResult(output=f"Dropdown options: {options}") + + elif action == "select_dropdown_option": + if index is None or not text: + return ToolResult( + error="Index and text are required for 'select_dropdown_option' action" + ) + element = await context.get_dom_element_by_index(index) + if not element: + return ToolResult(error=f"Element with index {index} not found") + page = await context.get_current_page() + await page.select_option(element.xpath, label=text) + return ToolResult( + output=f"Selected option '{text}' from dropdown at index {index}" + ) + + # Content extraction actions + elif action == "extract_content": + if not goal: + return ToolResult( + error="Goal is required for 'extract_content' action" + ) + + page = await context.get_current_page() + import markdownify + + content = markdownify.markdownify(await page.content()) + + prompt = f"""\ +Your task is to extract the content of the page. You will be given a page and a goal, and you should extract all relevant information around this goal from the page. If the goal is vague, summarize the page. Respond in json format. +Extraction goal: {goal} + +Page content: +{content[:max_content_length]} +""" + messages = [{"role": "system", "content": prompt}] + + # Define extraction function schema + extraction_function = { + "type": "function", + "function": { + "name": "extract_content", + "description": "Extract specific information from a webpage based on a goal", + "parameters": { + "type": "object", + "properties": { + "extracted_content": { + "type": "object", + "description": "The content extracted from the page according to the goal", + "properties": { + "text": { + "type": "string", + "description": "Text content extracted from the page", + }, + "metadata": { + "type": "object", + "description": "Additional metadata about the extracted content", + "properties": { + "source": { + "type": "string", + "description": "Source of the extracted content", + } + }, + }, + }, + } + }, + "required": ["extracted_content"], + }, + }, + } + + # Use LLM to extract content with required function calling + response = await self.llm.ask_tool( + messages, + tools=[extraction_function], + tool_choice="required", + ) + + if response and response.tool_calls: + args = json.loads(response.tool_calls[0].function.arguments) + extracted_content = args.get("extracted_content", {}) + return ToolResult( + output=f"Extracted from page:\n{extracted_content}\n" + ) + + return ToolResult(output="No content was extracted from the page.") + + # Tab management actions + elif action == "switch_tab": + if tab_id is None: + return ToolResult( + error="Tab ID is required for 'switch_tab' action" + ) + await context.switch_to_tab(tab_id) + page = await context.get_current_page() + await page.wait_for_load_state() + return ToolResult(output=f"Switched to tab {tab_id}") + + elif action == "open_tab": + if not url: + return ToolResult(error="URL is required for 'open_tab' action") + await context.create_new_tab(url) + return ToolResult(output=f"Opened new tab with {url}") + + elif action == "close_tab": + await context.close_current_tab() + return ToolResult(output="Closed current tab") + + # Utility actions + elif action == "wait": + seconds_to_wait = seconds if seconds is not None else 3 + await asyncio.sleep(seconds_to_wait) + return ToolResult(output=f"Waited for {seconds_to_wait} seconds") + + else: + return ToolResult(error=f"Unknown action: {action}") + + except Exception as e: + return ToolResult(error=f"Browser action '{action}' failed: {str(e)}") + + async def get_current_state( + self, context: Optional[BrowserContext] = None + ) -> ToolResult: + """ + Get the current browser state as a ToolResult. + If context is not provided, uses self.context. + """ + try: + # Use provided context or fall back to self.context + ctx = context or self.context + if not ctx: + return ToolResult(error="Browser context not initialized") + + state = await ctx.get_state() + + # Create a viewport_info dictionary if it doesn't exist + viewport_height = 0 + if hasattr(state, "viewport_info") and state.viewport_info: + viewport_height = state.viewport_info.height + elif hasattr(ctx, "config") and hasattr(ctx.config, "browser_window_size"): + viewport_height = ctx.config.browser_window_size.get("height", 0) + + # Take a screenshot for the state + page = await ctx.get_current_page() + + await page.bring_to_front() + await page.wait_for_load_state() + + screenshot = await page.screenshot( + full_page=True, animations="disabled", type="jpeg", quality=100 + ) + + screenshot = base64.b64encode(screenshot).decode("utf-8") + + # Build the state info with all required fields + state_info = { + "url": state.url, + "title": state.title, + "tabs": [tab.model_dump() for tab in state.tabs], + "help": "[0], [1], [2], etc., represent clickable indices corresponding to the elements listed. Clicking on these indices will navigate to or interact with the respective content behind them.", + "interactive_elements": ( + state.element_tree.clickable_elements_to_string() + if state.element_tree + else "" + ), + "scroll_info": { + "pixels_above": getattr(state, "pixels_above", 0), + "pixels_below": getattr(state, "pixels_below", 0), + "total_height": getattr(state, "pixels_above", 0) + + getattr(state, "pixels_below", 0) + + viewport_height, + }, + "viewport_height": viewport_height, + } + + return ToolResult( + output=json.dumps(state_info, indent=4, ensure_ascii=False), + base64_image=screenshot, + ) + except Exception as e: + return ToolResult(error=f"Failed to get browser state: {str(e)}") + + async def cleanup(self): + """Clean up browser resources.""" + async with self.lock: + if self.context is not None: + await self.context.close() + self.context = None + self.dom_service = None + if self.browser is not None: + await self.browser.close() + self.browser = None + + def __del__(self): + """Ensure cleanup when object is destroyed.""" + if self.browser is not None or self.context is not None: + try: + asyncio.run(self.cleanup()) + except RuntimeError: + loop = asyncio.new_event_loop() + loop.run_until_complete(self.cleanup()) + loop.close() + + @classmethod + def create_with_context(cls, context: Context) -> "BrowserUseTool[Context]": + """Factory method to create a BrowserUseTool with a specific context.""" + tool = cls() + tool.tool_context = context + return tool diff --git a/app/tool/chart_visualization/README.md b/app/tool/chart_visualization/README.md new file mode 100644 index 0000000..b5e0df5 --- /dev/null +++ b/app/tool/chart_visualization/README.md @@ -0,0 +1,146 @@ + + +# Chart Visualization Tool + +The chart visualization tool generates data processing code through Python and ultimately invokes [@visactor/vmind](https://github.com/VisActor/VMind) to obtain chart specifications. Chart rendering is implemented using [@visactor/vchart](https://github.com/VisActor/VChart). + +## Installation (Mac / Linux) + +1. Install node >= 18 + +```bash +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash +# Activate nvm, for example in Bash +source ~/.bashrc +# Then install the latest stable release of Node +nvm install node +# Activate usage, for example if the latest stable release is 22, then use 22 +nvm use 22 +``` + +2. Install dependencies + +```bash +# Navigate to the appropriate location in the current repository +cd app/tool/chart_visualization +npm install +``` + +## Installation (Windows) +1. Install nvm-windows + + Download the latest version `nvm-setup.exe` from the [official GitHub page](https://github.com/coreybutler/nvm-windows?tab=readme-ov-file#readme) and install it. + +2. Use nvm to install node + +```powershell +# Then install the latest stable release of Node +nvm install node +# Activate usage, for example if the latest stable release is 22, then use 22 +nvm use 22 +``` + +3. Install dependencies + +```bash +# Navigate to the appropriate location in the current repository +cd app/tool/chart_visualization +npm install +``` + +## Tool +### python_execute + +Execute the necessary parts of data analysis (excluding data visualization) using Python code, including data processing, data summary, report generation, and some general Python script code. + +#### Input +```typescript +{ + // Code type: data processing/data report/other general tasks + code_type: "process" | "report" | "others" + // Final execution code + code: string; +} +``` + +#### Output +Python execution results, including the saving of intermediate files and print output results. + +### visualization_preparation + +A pre-tool for data visualization with two purposes, + +#### Data -> Chart +Used to extract the data needed for analysis (.csv) and the corresponding visualization description from the data, ultimately outputting a JSON configuration file. + +#### Chart + Insight -> Chart +Select existing charts and corresponding data insights, choose data insights to add to the chart in the form of data annotations, and finally generate a JSON configuration file. + +#### Input +```typescript +{ + // Code type: data visualization or data insight addition + code_type: "visualization" | "insight" + // Python code used to produce the final JSON file + code: string; +} +``` + +#### Output +A configuration file for data visualization, used for the `data_visualization tool`. + +## data_visualization + +Generate specific data visualizations based on the content of `visualization_preparation`. + +### Input +```typescript +{ + // Configuration file path + json_path: string; + // Current purpose, data visualization or insight annotation addition + tool_type: "visualization" | "insight"; + // Final product png or html; html supports vchart rendering and interaction + output_type: 'png' | 'html' + // Language, currently supports Chinese and English + language: "zh" | "en" +} +``` + +## VMind Configuration + +### LLM + +VMind requires LLM invocation for intelligent chart generation. By default, it uses the `config.llm["default"]` configuration. + +### Generation Settings + +Main configurations include chart dimensions, theme, and generation method: +### Generation Method +Default: png. Currently supports automatic selection of `output_type` by LLM based on context. + +### Dimensions +Default dimensions are unspecified. For HTML output, charts fill the entire page by default. For PNG output, defaults to `1000*1000`. + +### Theme +Default theme: `'light'`. VChart supports multiple themes. See [Themes](https://www.visactor.io/vchart/guide/tutorial_docs/Theme/Theme_Extension). + +## Test + +Currently, three tasks of different difficulty levels are set for testing. + +### Simple Chart Generation Task + +Provide data and specific chart generation requirements, test results, execute the command: +```bash +python -m app.tool.chart_visualization.test.chart_demo +``` +The results should be located under `workspace\visualization`, involving 9 different chart results. + +### Simple Data Report Task + +Provide simple raw data analysis requirements, requiring simple processing of the data, execute the command: +```bash +python -m app.tool.chart_visualization.test.report_demo +``` +The results are also located under `workspace\visualization`. diff --git a/app/tool/chart_visualization/README_ja.md b/app/tool/chart_visualization/README_ja.md new file mode 100644 index 0000000..7c70278 --- /dev/null +++ b/app/tool/chart_visualization/README_ja.md @@ -0,0 +1,114 @@ +# グラフ可視化ツール + +グラフ可視化ツールは、Pythonを使用してデータ処理コードを生成し、最終的に[@visactor/vmind](https://github.com/VisActor/VMind)を呼び出してグラフのspec結果を得ます。グラフのレンダリングには[@visactor/vchart](https://github.com/VisActor/VChart)を使用します。 + +## インストール (Mac / Linux) + +1. Node >= 18をインストール + +```bash +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash +# nvmを有効化、例としてBashを使用 +source ~/.bashrc +# その後、最新の安定版Nodeをインストール +nvm install node +# 使用を有効化、例えば最新の安定版が22の場合、use 22 +nvm use 22 +``` + +2. 依存関係をインストール + +```bash +cd app/tool/chart_visualization +npm install +``` + +## インストール (Windows) +1. nvm-windowsをインストール + + [GitHub公式サイト](https://github.com/coreybutler/nvm-windows?tab=readme-ov-file#readme)から最新バージョンの`nvm-setup.exe`をダウンロードしてインストール + +2. nvmを使用してNodeをインストール + +```powershell +# その後、最新の安定版Nodeをインストール +nvm install node +# 使用を有効化、例えば最新の安定版が22の場合、use 22 +nvm use 22 +``` + +3. 依存関係をインストール + +```bash +# 現在のリポジトリで適切な位置に移動 +cd app/tool/chart_visualization +npm install +``` + +## ツール +### python_execute + +Pythonコードを使用してデータ分析(データ可視化を除く)に必要な部分を実行します。これにはデータ処理、データ要約、レポート生成、および一般的なPythonスクリプトコードが含まれます。 + +#### 入力 +```typescript +{ + // コードタイプ:データ処理/データレポート/その他の一般的なタスク + code_type: "process" | "report" | "others" + // 最終実行コード + code: string; +} +``` + +#### 出力 +Python実行結果、中間ファイルの保存とprint出力結果を含む + +### visualization_preparation + +データ可視化の準備ツールで、2つの用途があります。 + +#### Data -> Chart +データから分析に必要なデータ(.csv)と対応する可視化の説明を抽出し、最終的にJSON設定ファイルを出力します。 + +#### Chart + Insight -> Chart +既存のグラフと対応するデータインサイトを選択し、データインサイトをデータ注釈の形式でグラフに追加し、最終的にJSON設定ファイルを生成します。 + +#### 入力 +```typescript +{ + // コードタイプ:データ可視化またはデータインサイト追加 + code_type: "visualization" | "insight" + // 最終的なJSONファイルを生成するためのPythonコード + code: string; +} +``` + +#### 出力 +データ可視化の設定ファイル、`data_visualization tool`で使用 + +## data_visualization + +`visualization_preparation`の内容に基づいて具体的なデータ可視化を生成 + +### 入力 +```typescript +{ + // 設定ファイルのパス + json_path: string; + // 現在の用途、データ可視化またはインサイト注釈追加 + tool_type: "visualization" | "insight"; + // 最終成果物pngまたはhtml;htmlではvchartのレンダリングとインタラクションをサポート + output_type: 'png' | 'html' + // 言語、現在は中国語と英語をサポート + language: "zh" | "en" +} +``` + +## 出力 +最終的に'png'または'html'の形式でローカルに保存され、保存されたグラフのパスとグラフ内で発見されたデータインサイトを出力 + +## VMind設定 + +### LLM + +VMind自体 diff --git a/app/tool/chart_visualization/README_ko.md b/app/tool/chart_visualization/README_ko.md new file mode 100644 index 0000000..4f65f00 --- /dev/null +++ b/app/tool/chart_visualization/README_ko.md @@ -0,0 +1,128 @@ +# 차트 시각화 도구 + +차트 시각화 도구는 Python을 통해 데이터 처리 코드를 생성하고, 최종적으로 [@visactor/vmind](https://github.com/VisActor/VMind)를 호출하여 차트 사양을 얻습니다. 차트 렌더링은 [@visactor/vchart](https://github.com/VisActor/VChart)를 사용하여 구현됩니다. + +## 설치 (Mac / Linux) + +1. Node.js 18 이상 설치 + +```bash +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash +# nvm 활성화, 예를 들어 Bash +source ~/.bashrc +# 그런 다음 최신 안정 버전의 Node 설치 +nvm install node +# 사용 활성화, 예를 들어 최신 안정 버전이 22인 경우 use 22 +nvm use 22 +``` + +2. 의존성 설치 + +```bash +# 현재 저장소에서 해당 위치로 이동 +cd app/tool/chart_visualization +npm install +``` + +## 설치 (Windows) +1. nvm-windows 설치 + + [공식 GitHub 페이지](https://github.com/coreybutler/nvm-windows?tab=readme-ov-file#readme)에서 최신 버전의 `nvm-setup.exe`를 다운로드하고 설치합니다. + +2. nvm을 사용하여 Node.js 설치 + +```powershell +# 그런 다음 최신 안정 버전의 Node 설치 +nvm install node +# 사용 활성화, 예를 들어 최신 안정 버전이 22인 경우 use 22 +nvm use 22 +``` + +3. 의존성 설치 + +```bash +# 현재 저장소에서 해당 위치로 이동 +cd app/tool/chart_visualization +npm install +``` + +## 도구 +### python_execute + +Python 코드를 사용하여 데이터 분석의 필요한 부분(데이터 시각화 제외)을 실행합니다. 여기에는 데이터 처리, 데이터 요약, 보고서 생성 및 일부 일반적인 Python 스크립트 코드가 포함됩니다. + +#### 입력 +```typescript +{ + // 코드 유형: 데이터 처리/데이터 보고서/기타 일반 작업 + code_type: "process" | "report" | "others" + // 최종 실행 코드 + code: string; +} +``` + +#### 출력 +Python 실행 결과, 중간 파일 저장 및 출력 결과 포함. + +### visualization_preparation + +데이터 시각화를 위한 사전 도구로 두 가지 목적이 있습니다. + +#### 데이터 -> 차트 +분석에 필요한 데이터(.csv)와 해당 시각화 설명을 데이터에서 추출하여 최종적으로 JSON 구성 파일을 출력합니다. + +#### 차트 + 인사이트 -> 차트 +기존 차트와 해당 데이터 인사이트를 선택하고, 데이터 주석 형태로 차트에 추가할 데이터 인사이트를 선택하여 최종적으로 JSON 구성 파일을 생성합니다. + +#### 입력 +```typescript +{ + // 코드 유형: 데이터 시각화 또는 데이터 인사이트 추가 + code_type: "visualization" | "insight" + // 최종 JSON 파일을 생성하는 데 사용되는 Python 코드 + code: string; +} +``` + +#### 출력 +`data_visualization tool`에 사용되는 데이터 시각화를 위한 구성 파일. + +## data_visualization + +`visualization_preparation`의 내용을 기반으로 특정 데이터 시각화를 생성합니다. + +### 입력 +```typescript +{ + // 구성 파일 경로 + json_path: string; + // 현재 목적, 데이터 시각화 또는 인사이트 주석 추가 + tool_type: "visualization" | "insight"; + // 최종 제품 png 또는 html; html은 vchart 렌더링 및 상호작용 지원 + output_type: 'png' | 'html' + // 언어, 현재 중국어 및 영어 지원 + language: "zh" | "en" +} +``` + +## VMind 구성 + +### LLM + +VMind는 지능형 차트 생성을 위해 LLM 호출이 필요합니다. 기본적으로 `config.llm["default"]` 구성을 사용합니다. + +### 생성 설정 + +주요 구성에는 차트 크기, 테마 및 생성 방법이 포함됩니다. +### 생성 방법 +기본값: png. 현재 LLM이 컨텍스트에 따라 `output_type`을 자동으로 선택하는 것을 지원합니다. + +### 크기 +기본 크기는 지정되지 않았습니다. HTML 출력의 경우 차트는 기본적으로 전체 페이지를 채웁니다. PNG 출력의 경우 기본값은 `1000*1000`입니다. + +### 테마 +기본 테마: `'light'`. VChart는 여러 테마를 지원합니다. [테마](https://www.visactor.io/vchart/guide/tutorial_docs/Theme/Theme_Extension)를 참조하세요. + +## 테스트 + +현재, 서로 다른 난이도의 diff --git a/app/tool/chart_visualization/README_zh.md b/app/tool/chart_visualization/README_zh.md new file mode 100644 index 0000000..b1e414c --- /dev/null +++ b/app/tool/chart_visualization/README_zh.md @@ -0,0 +1,147 @@ +# 图表可视化工具 + +图表可视化工具,通过python生成数据处理代码,最终调用[@visactor/vmind](https://github.com/VisActor/VMind)得到图表的spec结果,图表渲染使用[@visactor/vchart](https://github.com/VisActor/VChart) + +## 安装(Mac / Linux) + +1. 安装node >= 18 + +```bash +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash +# 激活nvm,以Bash为例 +source ~/.bashrc +# 然后安装 Node 最近一个稳定颁布 +nvm install node +# 激活使用,例如最新一个稳定颁布为22,则use 22 +nvm use 22 +``` + +2. 安装依赖 + +```bash +cd app/tool/chart_visualization +npm install +``` + +## 安装(Windows) +1. 安装nvm-windows + + 从[github官网](https://github.com/coreybutler/nvm-windows?tab=readme-ov-file#readme)上下载最新版本`nvm-setup.exe`并且安装 + +2. 使用nvm安装node + +```powershell +# 然后安装 Node 最近一个稳定颁布 +nvm install node +# 激活使用,例如最新一个稳定颁布为22,则use 22 +nvm use 22 +``` + +3. 安装依赖 + +```bash +# 在当前仓库下定位到相应位置 +cd app/tool/chart_visualization +npm install +``` +## Tool +### python_execute + +用python代码执行数据分析(除数据可视化以外)中需要的部分,包括数据处理,数据总结摘要,报告生成以及一些通用python脚本代码 + +#### 输入 +```typescript +{ + // 代码类型:数据处理/数据报告/其他通用任务 + code_type: "process" | "report" | "others" + // 最终执行代码 + code: string; +} +``` + +#### 输出 +python执行结果,带有中间文件的保存和print输出结果 + +### visualization_preparation + +数据可视化前置工具,有两种用途, + +#### Data -〉 Chart +用于从数据中提取需要分析的数据(.csv)和对应可视化的描述,最终输出一份json配置文件。 + +#### Chart + Insight -> Chart +选取已有的图表和对应的数据洞察,挑选数据洞察以数据标注的形式增加到图表中,最终生成一份json配置文件。 + +#### 输入 +```typescript +{ + // 代码类型:数据可视化 或者 数据洞察添加 + code_type: "visualization" | "insight" + // 用于生产最终json文件的python代码 + code: string; +} +``` + +#### 输出 +数据可视化的配置文件,用于`data_visualization tool` + + +## data_visualization + +根据`visualization_preparation`的内容,生成具体的数据可视化 + +### 输入 +```typescript +{ + // 配置文件路径 + json_path: string; + // 当前用途,数据可视化或者洞察标注添加 + tool_type: "visualization" | "insight"; + // 最终产物png或者html;html下支持vchart渲染和交互 + output_type: 'png' | 'html' + // 语言,目前支持中文和英文 + language: "zh" | "en" +} +``` + +## 输出 +最终以'png'或者'html'的形式保存在本地,输出保存的图表路径以及图表中发现的数据洞察 + +## VMind配置 + +### LLM + +VMind本身也需要通过调用大模型得到智能图表生成结果,目前默认会使用`config.llm["default"]`配置 + +### 生成配置 + +主要生成配置包括图表的宽高、主题以及生成方式; +### 生成方式 +默认为png,目前支持大模型根据上下文自己选择`output_type` + +### 宽高 +目前默认不指定宽高,`html`下默认占满整个页面,'png'下默认为`1000 * 1000` + +### 主题 +目前默认主题为`'light'`,VChart图表支持多种主题,详见[主题](https://www.visactor.io/vchart/guide/tutorial_docs/Theme/Theme_Extension) + + +## 测试 + +当前设置了三种不同难度的任务用于测试 + +### 简单图表生成任务 + +给予数据和具体的图表生成需求,测试结果,执行命令: +```bash +python -m app.tool.chart_visualization.test.chart_demo +``` +结果应位于`worksapce\visualization`下,涉及到9种不同的图表结果 + +### 简单数据报表任务 + +给予简单原始数据可分析需求,需要对数据进行简单加工处理,执行命令: +```bash +python -m app.tool.chart_visualization.test.report_demo +``` +结果同样位于`worksapce\visualization`下 diff --git a/app/tool/chart_visualization/__init__.py b/app/tool/chart_visualization/__init__.py new file mode 100644 index 0000000..ea7d51a --- /dev/null +++ b/app/tool/chart_visualization/__init__.py @@ -0,0 +1,6 @@ +from app.tool.chart_visualization.chart_prepare import VisualizationPrepare +from app.tool.chart_visualization.data_visualization import DataVisualization +from app.tool.chart_visualization.python_execute import NormalPythonExecute + + +__all__ = ["DataVisualization", "VisualizationPrepare", "NormalPythonExecute"] diff --git a/app/tool/chart_visualization/chart_prepare.py b/app/tool/chart_visualization/chart_prepare.py new file mode 100644 index 0000000..1eed35e --- /dev/null +++ b/app/tool/chart_visualization/chart_prepare.py @@ -0,0 +1,38 @@ +from app.tool.chart_visualization.python_execute import NormalPythonExecute + + +class VisualizationPrepare(NormalPythonExecute): + """A tool for Chart Generation Preparation""" + + name: str = "visualization_preparation" + description: str = "Using Python code to generates metadata of data_visualization tool. Outputs: 1) JSON Information. 2) Cleaned CSV data files (Optional)." + parameters: dict = { + "type": "object", + "properties": { + "code_type": { + "description": "code type, visualization: csv -> chart; insight: choose insight into chart", + "type": "string", + "default": "visualization", + "enum": ["visualization", "insight"], + }, + "code": { + "type": "string", + "description": """Python code for data_visualization prepare. +## Visualization Type +1. Data loading logic +2. Csv Data and chart description generate +2.1 Csv data (The data you want to visulazation, cleaning / transform from origin data, saved in .csv) +2.2 Chart description of csv data (The chart title or description should be concise and clear. Examples: 'Product sales distribution', 'Monthly revenue trend'.) +3. Save information in json file.( format: {"csvFilePath": string, "chartTitle": string}[]) +## Insight Type +1. Select the insights from the data_visualization results that you want to add to the chart. +2. Save information in json file.( format: {"chartPath": string, "insights_id": number[]}[]) +# Note +1. You can generate one or multiple csv data with different visualization needs. +2. Make each chart data esay, clean and different. +3. Json file saving in utf-8 with path print: print(json_path) +""", + }, + }, + "required": ["code", "code_type"], + } diff --git a/app/tool/chart_visualization/data_visualization.py b/app/tool/chart_visualization/data_visualization.py new file mode 100644 index 0000000..26dfaa9 --- /dev/null +++ b/app/tool/chart_visualization/data_visualization.py @@ -0,0 +1,263 @@ +import asyncio +import json +import os +from typing import Any, Hashable + +import pandas as pd +from pydantic import Field, model_validator + +from app.config import config +from app.llm import LLM +from app.logger import logger +from app.tool.base import BaseTool + + +class DataVisualization(BaseTool): + name: str = "data_visualization" + description: str = """Visualize statistical chart or Add insights in chart with JSON info from visualization_preparation tool. You can do steps as follows: +1. Visualize statistical chart +2. Choose insights into chart based on step 1 (Optional) +Outputs: +1. Charts (png/html) +2. Charts Insights (.md)(Optional)""" + parameters: dict = { + "type": "object", + "properties": { + "json_path": { + "type": "string", + "description": """file path of json info with ".json" in the end""", + }, + "output_type": { + "description": "Rendering format (html=interactive)", + "type": "string", + "default": "html", + "enum": ["png", "html"], + }, + "tool_type": { + "description": "visualize chart or add insights", + "type": "string", + "default": "visualization", + "enum": ["visualization", "insight"], + }, + "language": { + "description": "english(en) / chinese(zh)", + "type": "string", + "default": "en", + "enum": ["zh", "en"], + }, + }, + "required": ["code"], + } + llm: LLM = Field(default_factory=LLM, description="Language model instance") + + @model_validator(mode="after") + def initialize_llm(self): + """Initialize llm with default settings if not provided.""" + if self.llm is None or not isinstance(self.llm, LLM): + self.llm = LLM(config_name=self.name.lower()) + return self + + def get_file_path( + self, + json_info: list[dict[str, str]], + path_str: str, + directory: str = None, + ) -> list[str]: + res = [] + for item in json_info: + if os.path.exists(item[path_str]): + res.append(item[path_str]) + elif os.path.exists( + os.path.join(f"{directory or config.workspace_root}", item[path_str]) + ): + res.append( + os.path.join( + f"{directory or config.workspace_root}", item[path_str] + ) + ) + else: + raise Exception(f"No such file or directory: {item[path_str]}") + return res + + def success_output_template(self, result: list[dict[str, str]]) -> str: + content = "" + if len(result) == 0: + return "Is EMPTY!" + for item in result: + content += f"""## {item['title']}\nChart saved in: {item['chart_path']}""" + if "insight_path" in item and item["insight_path"] and "insight_md" in item: + content += "\n" + item["insight_md"] + else: + content += "\n" + return f"Chart Generated Successful!\n{content}" + + async def data_visualization( + self, json_info: list[dict[str, str]], output_type: str, language: str + ) -> str: + data_list = [] + csv_file_path = self.get_file_path(json_info, "csvFilePath") + for index, item in enumerate(json_info): + df = pd.read_csv(csv_file_path[index], encoding="utf-8") + df = df.astype(object) + df = df.where(pd.notnull(df), None) + data_dict_list = df.to_json(orient="records", force_ascii=False) + + data_list.append( + { + "file_name": os.path.basename(csv_file_path[index]).replace( + ".csv", "" + ), + "dict_data": data_dict_list, + "chartTitle": item["chartTitle"], + } + ) + tasks = [ + self.invoke_vmind( + dict_data=item["dict_data"], + chart_description=item["chartTitle"], + file_name=item["file_name"], + output_type=output_type, + task_type="visualization", + language=language, + ) + for item in data_list + ] + + results = await asyncio.gather(*tasks) + error_list = [] + success_list = [] + for index, result in enumerate(results): + csv_path = csv_file_path[index] + if "error" in result and "chart_path" not in result: + error_list.append(f"Error in {csv_path}: {result['error']}") + else: + success_list.append( + { + **result, + "title": json_info[index]["chartTitle"], + } + ) + if len(error_list) > 0: + return { + "observation": f"# Error chart generated{'\n'.join(error_list)}\n{self.success_output_template(success_list)}", + "success": False, + } + else: + return {"observation": f"{self.success_output_template(success_list)}"} + + async def add_insighs( + self, json_info: list[dict[str, str]], output_type: str + ) -> str: + data_list = [] + chart_file_path = self.get_file_path( + json_info, "chartPath", os.path.join(config.workspace_root, "visualization") + ) + for index, item in enumerate(json_info): + if "insights_id" in item: + data_list.append( + { + "file_name": os.path.basename(chart_file_path[index]).replace( + f".{output_type}", "" + ), + "insights_id": item["insights_id"], + } + ) + tasks = [ + self.invoke_vmind( + insights_id=item["insights_id"], + file_name=item["file_name"], + output_type=output_type, + task_type="insight", + ) + for item in data_list + ] + results = await asyncio.gather(*tasks) + error_list = [] + success_list = [] + for index, result in enumerate(results): + chart_path = chart_file_path[index] + if "error" in result and "chart_path" not in result: + error_list.append(f"Error in {chart_path}: {result['error']}") + else: + success_list.append(chart_path) + success_template = ( + f"# Charts Update with Insights\n{','.join(success_list)}" + if len(success_list) > 0 + else "" + ) + if len(error_list) > 0: + return { + "observation": f"# Error in chart insights:{'\n'.join(error_list)}\n{success_template}", + "success": False, + } + else: + return {"observation": f"{success_template}"} + + async def execute( + self, + json_path: str, + output_type: str | None = "html", + tool_type: str | None = "visualization", + language: str | None = "en", + ) -> str: + try: + logger.info(f"📈 data_visualization with {json_path} in: {tool_type} ") + with open(json_path, "r", encoding="utf-8") as file: + json_info = json.load(file) + if tool_type == "visualization": + return await self.data_visualization(json_info, output_type, language) + else: + return await self.add_insighs(json_info, output_type) + except Exception as e: + return { + "observation": f"Error: {e}", + "success": False, + } + + async def invoke_vmind( + self, + file_name: str, + output_type: str, + task_type: str, + insights_id: list[str] = None, + dict_data: list[dict[Hashable, Any]] = None, + chart_description: str = None, + language: str = "en", + ): + llm_config = { + "base_url": self.llm.base_url, + "model": self.llm.model, + "api_key": self.llm.api_key, + } + vmind_params = { + "llm_config": llm_config, + "user_prompt": chart_description, + "dataset": dict_data, + "file_name": file_name, + "output_type": output_type, + "insights_id": insights_id, + "task_type": task_type, + "directory": str(config.workspace_root), + "language": language, + } + # build async sub process + process = await asyncio.create_subprocess_exec( + "npx", + "ts-node", + "src/chartVisualize.ts", + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=os.path.dirname(__file__), + ) + input_json = json.dumps(vmind_params, ensure_ascii=False).encode("utf-8") + try: + stdout, stderr = await process.communicate(input_json) + stdout_str = stdout.decode("utf-8") + stderr_str = stderr.decode("utf-8") + if process.returncode == 0: + return json.loads(stdout_str) + else: + return {"error": f"Node.js Error: {stderr_str}"} + except Exception as e: + return {"error": f"Subprocess Error: {str(e)}"} diff --git a/app/tool/chart_visualization/package-lock.json b/app/tool/chart_visualization/package-lock.json new file mode 100644 index 0000000..19dae01 --- /dev/null +++ b/app/tool/chart_visualization/package-lock.json @@ -0,0 +1,8739 @@ +{ + "name": "chart_visualization", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "chart_visualization", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@visactor/vchart": "^1.13.7", + "@visactor/vmind": "2.0.5", + "get-stdin": "^9.0.0", + "puppeteer": "^24.9.0" + }, + "devDependencies": { + "@types/node": "^22.10.1", + "ts-node": "^10.9.2", + "typescript": "^5.7.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz", + "integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==", + "dependencies": { + "debug": "^4.4.1", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.2", + "tar-fs": "^3.0.8", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@puppeteer/browsers/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/@puppeteer/browsers/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@puppeteer/browsers/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@resvg/resvg-js": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.4.1.tgz", + "integrity": "sha512-wTOf1zerZX8qYcMmLZw3czR4paI4hXqPjShNwJRh5DeHxvgffUS5KM7XwxtbIheUW6LVYT5fhT2AJiP6mU7U4A==", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@resvg/resvg-js-android-arm-eabi": "2.4.1", + "@resvg/resvg-js-android-arm64": "2.4.1", + "@resvg/resvg-js-darwin-arm64": "2.4.1", + "@resvg/resvg-js-darwin-x64": "2.4.1", + "@resvg/resvg-js-linux-arm-gnueabihf": "2.4.1", + "@resvg/resvg-js-linux-arm64-gnu": "2.4.1", + "@resvg/resvg-js-linux-arm64-musl": "2.4.1", + "@resvg/resvg-js-linux-x64-gnu": "2.4.1", + "@resvg/resvg-js-linux-x64-musl": "2.4.1", + "@resvg/resvg-js-win32-arm64-msvc": "2.4.1", + "@resvg/resvg-js-win32-ia32-msvc": "2.4.1", + "@resvg/resvg-js-win32-x64-msvc": "2.4.1" + } + }, + "node_modules/@resvg/resvg-js-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-U1oMNhea+kAXgiEXgzo7EbFGCD1Edq5aSlQoe6LMly6UjHzgx2W3N5kEXCwU/CgN5FiQhZr7PlSJSlcr7mdhfg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@stdlib/array-base-filled": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/array-base-filled/-/array-base-filled-0.2.2.tgz", + "integrity": "sha512-T7nB7dni5Y4/nsq6Gc1bAhYfzJbcOdqsmVZJUI698xpDbhCdVCIIaEbf0PnDMGN24psN+5mgAVmnNBom+uF0Xg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/array-base-zeros": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/array-base-zeros/-/array-base-zeros-0.2.2.tgz", + "integrity": "sha512-iwxqaEtpi4c2qpqabmhFdaQGkzgo5COwjHPn2T0S0wfJuM1VuVl5UBl15syr+MmZPJQOB1eBbh6F1uTh9597qw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/array-base-filled": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/array-float32": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/array-float32/-/array-float32-0.2.2.tgz", + "integrity": "sha512-pTcy1FNQrrJLL1LMxJjuVpcKJaibbGCFFTe41iCSXpSOC8SuTBuNohrO6K9+xR301Ruxxn4yrzjJJ6Fa3nQJ2g==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-has-float32array-support": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/array-float64": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/array-float64/-/array-float64-0.2.2.tgz", + "integrity": "sha512-ZmV5wcacGrhT0maw9dfLXNv4N3ZwFUV3D7ItFfZFGFnKIJbubrWzwtaYnxzIXigrDc8g3F6FVHRpsQLMxq0/lA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-has-float64array-support": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/array-uint16": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/array-uint16/-/array-uint16-0.2.2.tgz", + "integrity": "sha512-z5c/Izw43HkKfb1pTgEUMAS8GFvhtHkkHZSjX3XJN+17P0VjknxjlSvPiCBGqaDX9jXtlWH3mn1LSyDKtJQoeA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-has-uint16array-support": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/array-uint32": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/array-uint32/-/array-uint32-0.2.2.tgz", + "integrity": "sha512-3T894I9C2MqZJJmRCYFTuJp4Qw9RAt+GzYnVPyIXoK1h3TepUXe9VIVx50cUFIibdXycgu0IFGASeAb3YMyupw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-has-uint32array-support": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/array-uint8": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/array-uint8/-/array-uint8-0.2.2.tgz", + "integrity": "sha512-Ip9MUC8+10U9x0crMKWkpvfoUBBhWzc6k5SI4lxx38neFVmiJ3f+5MBADEagjpoKSBs71vlY2drnEZe+Gs2Ytg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-has-uint8array-support": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-has-float32array-support": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-has-float32array-support/-/assert-has-float32array-support-0.2.2.tgz", + "integrity": "sha512-pi2akQl8mVki43fF1GNQVLYW0bHIPp2HuRNThX9GjB3OFQTpvrV8/3zPSh4lOxQa5gRiabgf0+Rgeu3AOhEw9A==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-is-float32array": "^0.2.2", + "@stdlib/constants-float64-pinf": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-has-float64array-support": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-has-float64array-support/-/assert-has-float64array-support-0.2.2.tgz", + "integrity": "sha512-8L3GuKY1o0dJARCOsW9MXcugXapaMTpSG6dGxyNuUVEvFfY5UOzcj9/JIDal5FjqSgqVOGL5qZl2qtRwub34VA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-is-float64array": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-has-generator-support": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-has-generator-support/-/assert-has-generator-support-0.2.2.tgz", + "integrity": "sha512-TcE9BGV8i7B2OmxPlJ/2DUrAwG0W4fFS/DE7HmVk68PXVZsgyNQ/WP/IHBoazHDjhN5c3dU21c20kM/Bw007Rw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/utils-eval": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-has-own-property": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-has-own-property/-/assert-has-own-property-0.2.2.tgz", + "integrity": "sha512-m5rV4Z2/iNkwx2vRsNheM6sQZMzc8rQQOo90LieICXovXZy8wA5jNld4kRKjMNcRt/TjrNP7i2Rhh8hruRDlHg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-has-symbol-support": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-has-symbol-support/-/assert-has-symbol-support-0.2.2.tgz", + "integrity": "sha512-vCsGGmDZz5dikGgdF26rIL0y0nHvH7qaVf89HLLTybceuZijAqFSJEqcB3Gpl5uaeueLNAWExHi2EkoUVqKHGg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-has-tostringtag-support": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-has-tostringtag-support/-/assert-has-tostringtag-support-0.2.2.tgz", + "integrity": "sha512-bSHGqku11VH0swPEzO4Y2Dr+lTYEtjSWjamwqCTC8udOiOIOHKoxuU4uaMGKJjVfXG1L+XefLHqzuO5azxdRaA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-has-symbol-support": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-has-uint16array-support": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-has-uint16array-support/-/assert-has-uint16array-support-0.2.2.tgz", + "integrity": "sha512-aL188V7rOkkEH4wYjfpB+1waDO4ULxo5ppGEK6X0kG4YiXYBL2Zyum53bjEQvo0Nkn6ixe18dNzqqWWytBmDeg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-is-uint16array": "^0.2.1", + "@stdlib/constants-uint16-max": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-has-uint32array-support": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-has-uint32array-support/-/assert-has-uint32array-support-0.2.2.tgz", + "integrity": "sha512-+UHKP3mZOACkJ9CQjeKNfbXHm5HGQB862V5nV5q3UQlHPzhslnXKyG1SwAxTx+0g88C/2vlDLeqG8H4TH2UTFA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-is-uint32array": "^0.2.1", + "@stdlib/constants-uint32-max": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-has-uint8array-support": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-has-uint8array-support/-/assert-has-uint8array-support-0.2.2.tgz", + "integrity": "sha512-VfzrB0BMik9MvPyKcMDJL3waq4nM30RZUrr2EuuQ/RbUpromRWSDbzGTlRq5SfjtJrHDxILPV3rytDCc03dgWA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-is-uint8array": "^0.2.1", + "@stdlib/constants-uint8-max": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-is-array": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-is-array/-/assert-is-array-0.2.2.tgz", + "integrity": "sha512-aJyTX2U3JqAGCATgaAX9ygvDHc97GCIKkIhiZm/AZaLoFHPtMA1atQ4bKcefEC8Um9eefryxTHfFPfSr9CoNQQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/utils-native-class": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-is-big-endian": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-is-big-endian/-/assert-is-big-endian-0.2.2.tgz", + "integrity": "sha512-mPEl30/bqZh++UyQbxlyOuB7k0wC73y5J9nD2J6Ud6Fcl76R5IAGHRW0WT3W18is/6jG1jzMd8hrISFyD7N0sA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/array-uint16": "^0.2.1", + "@stdlib/array-uint8": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-is-boolean": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-is-boolean/-/assert-is-boolean-0.2.2.tgz", + "integrity": "sha512-3KFLRTYZpX6u95baZ6PubBvjehJs2xBU6+zrenR0jx8KToUYCnJPxqqj7JXRhSD+cOURmcjj9rocVaG9Nz18Pg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-has-tostringtag-support": "^0.2.2", + "@stdlib/boolean-ctor": "^0.2.2", + "@stdlib/utils-define-nonenumerable-read-only-property": "^0.2.2", + "@stdlib/utils-native-class": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-is-buffer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-is-buffer/-/assert-is-buffer-0.2.2.tgz", + "integrity": "sha512-4/WMFTEcDYlVbRhxY8Wlqag4S70QCnn6WmQ4wmfiLW92kqQHsLvTNvdt/qqh/SDyDV31R/cpd3QPsVN534dNEA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-is-object-like": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-is-float32array": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-is-float32array/-/assert-is-float32array-0.2.2.tgz", + "integrity": "sha512-hxEKz/Y4m1NYuOaiQKoqQA1HeAYwNXFqSk3FJ4hC71DuGNit2tuxucVyck3mcWLpLmqo0+Qlojgwo5P9/C/9MQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/utils-native-class": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-is-float64array": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-is-float64array/-/assert-is-float64array-0.2.2.tgz", + "integrity": "sha512-3R1wLi6u/IHXsXMtaLnvN9BSpqAJ8tWhwjOOr6kadDqCWsU7Odc7xKLeAXAInAxwnV8VDpO4ifym4A3wehazPQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/utils-native-class": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-is-function": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-is-function/-/assert-is-function-0.2.2.tgz", + "integrity": "sha512-whY69DUYWljCJ79Cvygp7VzWGOtGTsh3SQhzNuGt+ut6EsOW+8nwiRkyBXYKf/MOF+NRn15pxg8cJEoeRgsPcA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/utils-type-of": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-is-little-endian": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-is-little-endian/-/assert-is-little-endian-0.2.2.tgz", + "integrity": "sha512-KMzPndj85jDiE1+hYCpw12k2OQOVkfpCo7ojCmCl8366wtKGEaEdGbz1iH98zkxRvnZLSMXcYXI2z3gtdmB0Ag==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/array-uint16": "^0.2.1", + "@stdlib/array-uint8": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-is-number": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-is-number/-/assert-is-number-0.2.2.tgz", + "integrity": "sha512-sWpJ59GqGbmlcdYSUV/OYkmQW8k47w10+E0K0zPu1x1VKzhjgA5ZB2sJcpgI8Vt3ckRLjdhuc62ZHJkrJujG7A==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-has-tostringtag-support": "^0.2.2", + "@stdlib/number-ctor": "^0.2.2", + "@stdlib/utils-define-nonenumerable-read-only-property": "^0.2.2", + "@stdlib/utils-native-class": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-is-object": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-is-object/-/assert-is-object-0.2.2.tgz", + "integrity": "sha512-sNnphJuHyMDHHHaonlx6vaCKMe4sHOn0ag5Ck4iW3kJtM2OZB2J4h8qFcwKzlMk7fgFu7vYNGCZtpm1dYbbUfQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-is-array": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-is-object-like": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-is-object-like/-/assert-is-object-like-0.2.2.tgz", + "integrity": "sha512-MjQBpHdEebbJwLlxh/BKNH8IEHqY0YlcCMRKOQU0UOlILSJg0vG+GL4fDDqtx9FSXxcTqC+w3keHx8kAKvQhzg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-tools-array-function": "^0.2.1", + "@stdlib/utils-define-nonenumerable-read-only-property": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-is-plain-object": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-is-plain-object/-/assert-is-plain-object-0.2.2.tgz", + "integrity": "sha512-o4AFWgBsSNzZAOOfIrxoDFYTqnLuGiaHDFwIeZGUHdpQeav2Fll+sGeaqOcekF7yKawoswnwWdJqTsjapb4Yzw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-has-own-property": "^0.2.1", + "@stdlib/assert-is-function": "^0.2.1", + "@stdlib/assert-is-object": "^0.2.1", + "@stdlib/utils-get-prototype-of": "^0.2.1", + "@stdlib/utils-native-class": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-is-regexp": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-is-regexp/-/assert-is-regexp-0.2.2.tgz", + "integrity": "sha512-2JtiUtRJxPaVXL7dkWoV3n5jouI65DwYDXsDXg3xo23TXlTNGgU/HhKO4FWC1Yqju7YMZi0hcZSW6E9v8ISqeQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-has-tostringtag-support": "^0.2.2", + "@stdlib/utils-native-class": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-is-string": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-is-string/-/assert-is-string-0.2.2.tgz", + "integrity": "sha512-SOkFg4Hq443hkadM4tzcwTHWvTyKP9ULOZ8MSnnqmU0nBX1zLVFLFGY8jnF6Cary0dL0V7QQBCfuxqKFM6u2PQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-has-tostringtag-support": "^0.2.2", + "@stdlib/utils-define-nonenumerable-read-only-property": "^0.2.2", + "@stdlib/utils-native-class": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-is-uint16array": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-is-uint16array/-/assert-is-uint16array-0.2.2.tgz", + "integrity": "sha512-w3+HeTiXGLJGw5nCqr0WbvgArNMEj7ulED1Yd19xXbmmk2W1ZUB+g9hJDOQTiKsTU4AVyH4/As+aA8eDVmWtmg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/utils-native-class": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-is-uint32array": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-is-uint32array/-/assert-is-uint32array-0.2.2.tgz", + "integrity": "sha512-3F4nIHg1Qp0mMIsImWUC8DwQ3qBK5vdIJTjS2LufLbFBhHNmv5kK1yJiIXQDTLkENU0STZe05TByo01ZNLOmDQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/utils-native-class": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-is-uint8array": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-is-uint8array/-/assert-is-uint8array-0.2.2.tgz", + "integrity": "sha512-51WnDip6H2RrN0CbqWmfqySAjam8IZ0VjlfUDc3PtcgrZGrKKjVgyHAsT/L3ZDydwF+aB94uvYJu5QyrCPNaZw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/utils-native-class": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/assert-tools-array-function": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/assert-tools-array-function/-/assert-tools-array-function-0.2.2.tgz", + "integrity": "sha512-FYeT7X9x0C8Nh+MN6IJUDz+7i7yB6mio2/SDlrvyepjyPSU/cfHfwW0GEOnQhxZ+keLZC/YqDD930WjRODwMdA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-is-array": "^0.2.1", + "@stdlib/error-tools-fmtprodmsg": "^0.2.2", + "@stdlib/string-format": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/boolean-ctor": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/boolean-ctor/-/boolean-ctor-0.2.2.tgz", + "integrity": "sha512-qIkHzmfxDvGzQ3XI9R7sZG97QSaWG5TvWVlrvcysOGT1cs6HtQgnf4D//SRzZ52VLm8oICP+6OKtd8Hpm6G7Ww==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/complex-float32": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/complex-float32/-/complex-float32-0.2.1.tgz", + "integrity": "sha512-tp83HfJzcZLK7/6P6gZPcAa/8F/aHS7gBHgB6ft45d/n6oE+/VbnyOvsJKanRv8S96kBRj8xkvlWHz4IiBrT0Q==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-is-number": "^0.2.1", + "@stdlib/number-float64-base-to-float32": "^0.2.1", + "@stdlib/string-format": "^0.2.1", + "@stdlib/utils-define-nonenumerable-read-only-property": "^0.2.1", + "@stdlib/utils-define-property": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/complex-float32-ctor": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@stdlib/complex-float32-ctor/-/complex-float32-ctor-0.0.2.tgz", + "integrity": "sha512-QsTLynhTRmDT0mSkfdHj0FSqQSxh2nKx+vvrH3Y0/Cd/r0WoHFZwyibndDxshfkf9B7nist8QKyvV82I3IZciA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-is-number": "^0.2.2", + "@stdlib/error-tools-fmtprodmsg": "^0.2.2", + "@stdlib/number-float64-base-to-float32": "^0.2.1", + "@stdlib/string-format": "^0.2.2", + "@stdlib/utils-define-nonenumerable-read-only-property": "^0.2.2", + "@stdlib/utils-define-property": "^0.2.4" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/complex-float32-reim": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@stdlib/complex-float32-reim/-/complex-float32-reim-0.1.2.tgz", + "integrity": "sha512-24H+t1xwQF6vhOoMZdDA3TFB4M+jb5Swm/FwNaepovlzVIG2NlthUZs6mZg1T3oegqesIRQRwhpn4jIPjuGiTw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/array-float32": "^0.2.2", + "@stdlib/complex-float32-ctor": "^0.0.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/complex-float64": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/complex-float64/-/complex-float64-0.2.1.tgz", + "integrity": "sha512-vN9GqlSaonoREf8/RIN9tfNLnkfN4s7AI0DPsGnvc1491oOqq9UqMw8rYTrnxuum9/OaNAAUqDkb5GLu5uTveQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-is-number": "^0.2.1", + "@stdlib/complex-float32": "^0.2.1", + "@stdlib/string-format": "^0.2.1", + "@stdlib/utils-define-nonenumerable-read-only-property": "^0.2.1", + "@stdlib/utils-define-property": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/complex-float64-ctor": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@stdlib/complex-float64-ctor/-/complex-float64-ctor-0.0.3.tgz", + "integrity": "sha512-oixCtBif+Uab2rKtgedwQTbQTEC+wVSu4JQH935eJ8Jo0eL6vXUHHlVrkLgYKlCDLvq5px1QQn42Czg/ixh6Gw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-is-number": "^0.2.2", + "@stdlib/complex-float32-ctor": "^0.0.2", + "@stdlib/error-tools-fmtprodmsg": "^0.2.2", + "@stdlib/string-format": "^0.2.2", + "@stdlib/utils-define-nonenumerable-read-only-property": "^0.2.2", + "@stdlib/utils-define-property": "^0.2.4" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/complex-float64-reim": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@stdlib/complex-float64-reim/-/complex-float64-reim-0.1.2.tgz", + "integrity": "sha512-q6RnfgbUunApAYuGmkft1oOM3x3xVMVJwNRlRgfIXwKDb8pYt+S/CeIwi3Su5SF6ay3AqA1s+ze7m21osXAJyw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/array-float64": "^0.2.2", + "@stdlib/complex-float64-ctor": "^0.0.3" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/complex-reim": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/complex-reim/-/complex-reim-0.2.1.tgz", + "integrity": "sha512-67nakj+HwBRx/ha3j/sLbrMr2hwFVgEZtaczOgn1Jy/cU03lKvNbMkR7QI9s+sA+b+A3yJB3ob8ZQSqh3D1+dA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/array-float64": "^0.2.1", + "@stdlib/complex-float64": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/complex-reimf": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/complex-reimf/-/complex-reimf-0.2.1.tgz", + "integrity": "sha512-6HyPPmo0CEHoBjOg2w70mMFLcFEunM78ljnW6kf1OxjM/mqMaBM1NRpDrQoFwCIdh1RF1ojl3JR0YLllEf0qyQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/array-float32": "^0.2.1", + "@stdlib/complex-float32": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float32-max": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float32-max/-/constants-float32-max-0.2.2.tgz", + "integrity": "sha512-uxvIm/KmIeZP4vyfoqPd72l5/uidnCN9YJT3p7Z2LD8hYN3PPLu6pd/5b51HMFLwfkZ27byRJ9+YK6XnneJP0Q==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float32-smallest-normal": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float32-smallest-normal/-/constants-float32-smallest-normal-0.2.2.tgz", + "integrity": "sha512-2qkGjGML2/8P9YguHnac2AKXLbfycpYdCxKmuXQdAVzMMNCJWjHoIqZMFG29WBEDBOP057X+48S6WhIqoxRpWA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-e": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-e/-/constants-float64-e-0.2.2.tgz", + "integrity": "sha512-7fxHaABwosbUzpBsw6Z9Dd9MqUYne8x+44EjohVcWDr0p0mHB/DXVYEYTlwEP/U/XbRrKdO3jUG6IO/GsEjzWg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-eps": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-eps/-/constants-float64-eps-0.2.2.tgz", + "integrity": "sha512-61Pb2ip9aPhHXxiCn+VZ0UVw2rMYUp0xrX93FXyB3UTLacrofRKLMKtbV0SFac4VXx5igv2+0G+h6G/fwCgjyw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-eulergamma": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-eulergamma/-/constants-float64-eulergamma-0.2.2.tgz", + "integrity": "sha512-XsuVud0d1hLTQspFzgUSH2e3IawTXLlJi2k4Vg0Nn6juulxfNO9PnAGtHz+p1BynYF/YwN+qhKnISQxrN31rsQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-exponent-bias": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-exponent-bias/-/constants-float64-exponent-bias-0.2.2.tgz", + "integrity": "sha512-zLWkjzDYHSsBsXB/4mwHysOGl64JS3XBt/McjvjCLc/IZpfsUNFxLCl7oVCplXzYYHcQj/RfEBFy6cxQ6FvdpQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-fourth-pi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-fourth-pi/-/constants-float64-fourth-pi-0.2.2.tgz", + "integrity": "sha512-j0NOg45ouibms4ML8pfS/eDrurdtnhJTNPCGQM4mg3X+1ljsuO0pvkpVCvuz29t5J23KTcfGBXXr90ikoBmjlw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-gamma-lanczos-g": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-gamma-lanczos-g/-/constants-float64-gamma-lanczos-g-0.2.2.tgz", + "integrity": "sha512-hCaZbZ042htCy9mlGrfUEbz4d0xW/DLdr3vHs5KiBWU+G+WHVH33vubSnEoyT0ugWpAk2ZqWXe/V8sLGgOu0xg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-half-ln-two": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-half-ln-two/-/constants-float64-half-ln-two-0.2.2.tgz", + "integrity": "sha512-yv1XhzZR2AfJmnAGL0kdWlIUhc/vqdWol+1Gq2brXPVfgqbUmJu5XZuuK+jZA2k+fHyvRHNEwQRv9OPnOjchFg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-half-pi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-half-pi/-/constants-float64-half-pi-0.2.2.tgz", + "integrity": "sha512-lM3SiDsZCKiuF5lPThZFFqioIwh1bUiBUnnDMLB04/QkVRCAaXUo+dsq2hOB6iBhHoYhiKds6T+PsHSBlpqAaA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-high-word-abs-mask": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-high-word-abs-mask/-/constants-float64-high-word-abs-mask-0.2.2.tgz", + "integrity": "sha512-YtYngcHlw9qvOpmsSlkNHi6cy/7Y7QkyYh5kJbDvuOUXPDKa3rEwBln4mKjbWsXhmmN0bk7TLypH7Ryd/UAjUQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-high-word-exponent-mask": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-high-word-exponent-mask/-/constants-float64-high-word-exponent-mask-0.2.2.tgz", + "integrity": "sha512-LhYUXvpnLOFnWr8ucHA9N/H75VxcS2T9EoBDTmWBZoKj2Pg0icGVDmcNciRLIWbuPA9osgcKpxoU+ADIfaipVA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-high-word-sign-mask": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-high-word-sign-mask/-/constants-float64-high-word-sign-mask-0.2.1.tgz", + "integrity": "sha512-Fep/Ccgvz5i9d5k96zJsDjgXGno8HJfmH7wihLmziFmA2z9t7NSacH4/BH4rPJ5yXFHLkacNLDxaF1gO1XpcLA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-high-word-significand-mask": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-high-word-significand-mask/-/constants-float64-high-word-significand-mask-0.2.2.tgz", + "integrity": "sha512-eDDyiQ5PR1/qyklrW0Pus0ZopM7BYjkWTjqhSHhj0DibH6UMwSMlIl4ddCh3VX37p5eByuAavnaPgizk5c9mUw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-ln-sqrt-two-pi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-ln-sqrt-two-pi/-/constants-float64-ln-sqrt-two-pi-0.2.2.tgz", + "integrity": "sha512-C9YS9W/lvv54wUC7DojQSRH9faKw0sMAM09oMRVm8OOYNr01Rs1wXeSPStl9ns4qiV/G13vZzd1I3nGqgqihbw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-ln-two": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-ln-two/-/constants-float64-ln-two-0.2.2.tgz", + "integrity": "sha512-EQ8EJ6B1wPfuhva0aApKIsF7lTna++txV4AUzL2wTfwDHw6RzWpA44u+k54KnLF8ZXUNIYDNQHHvtzdfKrFzCA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-max": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-max/-/constants-float64-max-0.2.2.tgz", + "integrity": "sha512-S3kcIKTK65hPqirziof3KTYqfFKopgaTnaiDlDKdzaCzBZ5qkrAcRd4vl+W1KHoZruUyWC2/RYZUa/8+h075TQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-max-base2-exponent": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-max-base2-exponent/-/constants-float64-max-base2-exponent-0.2.2.tgz", + "integrity": "sha512-KmDe98pJ2HXz2SbqyFfSDhlSSVD7JssjbZ5K11HEK2avqMcoCbdHH20T+6/TpA01VqaK8dLbeyphOfALcDdMKA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-max-base2-exponent-subnormal": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-max-base2-exponent-subnormal/-/constants-float64-max-base2-exponent-subnormal-0.2.1.tgz", + "integrity": "sha512-D1wBNn54Hu2pK6P/yBz0FtPBI3/7HdgK8igYjWDKWUKzC92R/6PHZ9q5NzedcGxoBs8MUk1zNpP0tZyYj9Y4YQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-max-ln": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-max-ln/-/constants-float64-max-ln-0.2.2.tgz", + "integrity": "sha512-FPAEGjnoQMDPWJbCyyto7HWQ/SY2jjD8IkjyD8aOwENqbswjCbOINXRiK2ar27OOXG7Dv7CCpFpoorTxv0gmfA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-max-safe-integer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-max-safe-integer/-/constants-float64-max-safe-integer-0.2.2.tgz", + "integrity": "sha512-d+sxmxhkt980SDFhnnRDSpujPQTv4nEt5Ox3L86HgYZU4mQU/wbzYVkMuHIANW9x3ehww5blnGXTKYG9rQCXAw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-max-safe-nth-factorial": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-max-safe-nth-factorial/-/constants-float64-max-safe-nth-factorial-0.1.0.tgz", + "integrity": "sha512-sppIfkBbeyKNwfRbmNFi5obI7Q+IJCQzfWKYqvzmEJVOkmEg6hhtEeFc8zZJGCU7+Pndc3M2wdbTT5a3rhamHw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-min-base2-exponent": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-min-base2-exponent/-/constants-float64-min-base2-exponent-0.2.2.tgz", + "integrity": "sha512-YZmBiKik6LbWB4EOZ/ZUs/u6OIF742xNK8mhEqL0OEN4NuJe3OdErpOic6KjMmHjQuqCXdFoSqsWZaFHcIN7HA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-min-base2-exponent-subnormal": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-min-base2-exponent-subnormal/-/constants-float64-min-base2-exponent-subnormal-0.2.1.tgz", + "integrity": "sha512-fTXfvctXWj/48gK+gbRBrHuEHEKY4QOJoXSGp414Sz6vUxHusHJJ686p8ze3XqM7CY6fmL09ZgdGz/uhJl/7lw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-min-ln": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-min-ln/-/constants-float64-min-ln-0.2.2.tgz", + "integrity": "sha512-N1Sxjo3uTdEIpHeG2TzaX06UuvpcKHvjYKpIMhJSajbxvfVDURHlc9kIpfbP9C9/YYoCy0FvewA/kvbqNaYypA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-ninf": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-ninf/-/constants-float64-ninf-0.2.2.tgz", + "integrity": "sha512-Iu+wZs/vgudAKVg9FEcRY3FadkmvsWuq/wJ3jIHjhaP5xcnoF3XJUO4IneEndybHwehfJL65NShnDsJcg1gicw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/number-ctor": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-pi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-pi/-/constants-float64-pi-0.2.2.tgz", + "integrity": "sha512-ix34KmpUQ0LUM++L6avLhM9LFCcGTlsUDyWD/tYVGZBiIzDS3TMKShHRkZvC+v87fuyYNPoxolYtk5AlbacI6g==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-pinf": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-pinf/-/constants-float64-pinf-0.2.2.tgz", + "integrity": "sha512-UcwnWaSkUMD8QyKADwkXPlY7yOosCPZpE2EDXf/+WOzuWi5vpsec+JaasD5ggAN8Rv8OTVmexTFs1uZfrHgqVQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-smallest-normal": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-smallest-normal/-/constants-float64-smallest-normal-0.2.2.tgz", + "integrity": "sha512-GXNBkdqLT9X+dU59O1kmb7W5da/RhSXSvxx0xG5r7ipJPOtRLfTXGGvvTzWD4xA3Z5TKlrEL6ww5sph9BsPJnA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-smallest-subnormal": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-smallest-subnormal/-/constants-float64-smallest-subnormal-0.2.2.tgz", + "integrity": "sha512-KuF+scDOsP0okx8RLF+q3l1RheaYChf+u/HbhzFbz82GeCIdIVp86UMwoBgfn8AT8cnR5SrtvLtQw15MGfa/vg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-sqrt-eps": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-sqrt-eps/-/constants-float64-sqrt-eps-0.2.2.tgz", + "integrity": "sha512-X7LnGfnwNnhiwlY+zd3FX6zclsx61MaboGTNAAdaV78YjBDTdGdWMHk5MQo1U17ryPlhdGphOAejhDHeaSnTXQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-sqrt-two": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-sqrt-two/-/constants-float64-sqrt-two-0.2.2.tgz", + "integrity": "sha512-iqqouCuS9pUhjD91i5siScxLDtQTF1HsSZor6jaZRviMiOjCj/mjzxxTFHWUlU/rxHMBBhj/u7i12fv6a7dCAQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-sqrt-two-pi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-sqrt-two-pi/-/constants-float64-sqrt-two-pi-0.2.2.tgz", + "integrity": "sha512-I8Ylr64x8AFSQ2hFBT8szuIBAy2wqPx69taJMzfcmuM5SnSbS8SE/H19YnCimZErVFo4bz0Rh8Fp3edN4i6teQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-float64-two-pi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-float64-two-pi/-/constants-float64-two-pi-0.2.2.tgz", + "integrity": "sha512-cyXuwYOersVsA8tDSJ0ocMbtOc5KGxjlGvYC4vrpLQVkgNpxcGbA57n6JvaGmNk7+InXXbQ7qhTWGbTNgafcLQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-int32-max": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@stdlib/constants-int32-max/-/constants-int32-max-0.3.0.tgz", + "integrity": "sha512-jYN84QfG/yP2RYw98OR6UYehFFs0PsGAihV6pYU0ey+WF9IOXgSjRP56KMoZ7ctHwl4wsnj9I+qB2tGuEXr+pQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-uint16-max": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-uint16-max/-/constants-uint16-max-0.2.2.tgz", + "integrity": "sha512-qaFXbxgFnAkt73P5Ch7ODb0TsOTg0LEBM52hw6qt7+gTMZUdS0zBAiy5J2eEkTxA9rD9X3nIyUtLf2C7jafNdw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-uint32-max": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-uint32-max/-/constants-uint32-max-0.2.2.tgz", + "integrity": "sha512-2G44HQgIKDrh3tJUkmvtz+eM+uwDvOMF+2I3sONcTHacANb+zP7la4LDYiTp+HFkPJyfh/kPapXBiHpissAb1A==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/constants-uint8-max": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/constants-uint8-max/-/constants-uint8-max-0.2.2.tgz", + "integrity": "sha512-ZTBQq3fqS/Y4ll6cPY5SKaS266EfmKP9PW3YLJaTELmYIzVo9w2RFtfCqN05G3olTQ6Le9MUEE/C6VFgZNElDQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/error-tools-fmtprodmsg": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/error-tools-fmtprodmsg/-/error-tools-fmtprodmsg-0.2.2.tgz", + "integrity": "sha512-2IliQfTes4WV5odPidZFGD5eYDswZrPXob7oOu95Q69ERqImo8WzSwnG2EDbHPyOyYCewuMfM5Ha6Ggf+u944Q==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/fs-exists": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/fs-exists/-/fs-exists-0.2.2.tgz", + "integrity": "sha512-uGLqc7izCIam2aTyv0miyktl4l8awgRkCS39eIEvvvnKIaTBF6pxfac7FtFHeEQKE3XhtKsOmdQ/yJjUMChLuA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/utils-define-nonenumerable-read-only-property": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/fs-resolve-parent-path": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/fs-resolve-parent-path/-/fs-resolve-parent-path-0.2.2.tgz", + "integrity": "sha512-ZG78ouZc+pdPLtU+sSpYTvbKTiLUgn6NTtlVFYmcmkYRFn+fGOOakwVuhYMcYG6ti10cLD6WzB/YujxIt8f+nA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-has-own-property": "^0.2.2", + "@stdlib/assert-is-function": "^0.2.2", + "@stdlib/assert-is-plain-object": "^0.2.2", + "@stdlib/assert-is-string": "^0.2.2", + "@stdlib/error-tools-fmtprodmsg": "^0.2.2", + "@stdlib/fs-exists": "^0.2.2", + "@stdlib/process-cwd": "^0.2.2", + "@stdlib/string-format": "^0.2.2", + "@stdlib/utils-define-nonenumerable-read-only-property": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/function-ctor": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/function-ctor/-/function-ctor-0.2.2.tgz", + "integrity": "sha512-qSn1XQnnhgCSYBfFy4II0dY5eW4wdOprgDTHcOJ3PkPWuZHDC1fXZsok1OYAosHqIiIw44zBFcMS/JRex4ebdQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-assert-is-even": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-assert-is-even/-/math-base-assert-is-even-0.2.3.tgz", + "integrity": "sha512-cziGv8F/aNyfME7Wx2XJjnYBnf9vIeh8yTIzlLELd0OqGHqfsHU5OQxxcl9x5DbjZ1G/w0lphWxHFHYCuwFCHw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/math-base-assert-is-integer": "^0.2.4", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-assert-is-infinite": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-assert-is-infinite/-/math-base-assert-is-infinite-0.2.2.tgz", + "integrity": "sha512-4zDZuinC3vkXRdQepr0ZTwWX3KgM0VIWqYthOmCSgLLA87L9M9z9MgUZL1QeYeYa0+60epjDcQ8MS3ecT70Jxw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-ninf": "^0.2.2", + "@stdlib/constants-float64-pinf": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-assert-is-integer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-assert-is-integer/-/math-base-assert-is-integer-0.2.5.tgz", + "integrity": "sha512-Zi8N66GbWtSCR3OUsRdBknjNlX+aBN8w6CaVEP5+Jy/a7MgMYzevS52TNS5sm8jqzKBlFhZlPLex+Zl2GlPvSA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/math-base-special-floor": "^0.2.3", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-assert-is-nan": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-assert-is-nan/-/math-base-assert-is-nan-0.2.2.tgz", + "integrity": "sha512-QVS8rpWdkR9YmHqiYLDVLsCiM+dASt/2feuTl4T/GSdou3Y/PS/4j/tuDvCDoHDNfDkULUW+FCVjKYpbyoeqBQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/utils-library-manifest": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-assert-is-negative-zero": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-assert-is-negative-zero/-/math-base-assert-is-negative-zero-0.2.2.tgz", + "integrity": "sha512-WvKNuBZ6CDarOTzOuFLmO1jwZnFD+butIvfD2Ws6SsuqSCiWOaF4OhIckqPzo1XEdkqqhRNPqBxqc0D+hsEYVA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-ninf": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-assert-is-odd": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-assert-is-odd/-/math-base-assert-is-odd-0.3.0.tgz", + "integrity": "sha512-V44F3xdR5/bHXqqYvE/AldLnVmijLr/rgf7EjnJXXDQLfPCgemy0iHTFl19N68KG1YO9SMPdyOaNjh4K0O9Qqw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/math-base-assert-is-even": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-assert-is-positive-zero": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-assert-is-positive-zero/-/math-base-assert-is-positive-zero-0.2.2.tgz", + "integrity": "sha512-mMX5xsemKpHRAgjpVJCb3eVZ3WIkZh6KnHQH8i8n4vI44pcdpN5rcTdEAMlhLjxT/rT7H2wq85f7/FRsq9r9rw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-pinf": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-napi-binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-napi-binary/-/math-base-napi-binary-0.3.0.tgz", + "integrity": "sha512-bhwsmGMOMN1srcpNAFRjDMSXe9ue1s/XmaoBBlqcG6S2nqRQlIVnKKH4WZx4hmC1jDqoFXuNPJGE47VXpVV+mA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/complex-float32-ctor": "^0.0.2", + "@stdlib/complex-float32-reim": "^0.1.2", + "@stdlib/complex-float64-ctor": "^0.0.3", + "@stdlib/complex-float64-reim": "^0.1.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-napi-unary": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-napi-unary/-/math-base-napi-unary-0.2.3.tgz", + "integrity": "sha512-BCyJmpq2S8EFo2yMt1z+v1EL7nn8RHcM6jn7fa8n3BTP679K0MSlawIh3A0CFogfrTdjPM4G44VO1ddsdLExcg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/complex-float32-ctor": "^0.0.2", + "@stdlib/complex-float32-reim": "^0.1.1", + "@stdlib/complex-float64-ctor": "^0.0.3", + "@stdlib/complex-float64-reim": "^0.1.1", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-abs": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-abs/-/math-base-special-abs-0.2.2.tgz", + "integrity": "sha512-cw5CXj05c/L0COaD9J+paHXwmoN5IBYh+Spk0331f1pEMvGxSO1KmCREZaooUEEFKPhKDukEHKeitja2yAQh4Q==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-high-word-abs-mask": "^0.2.2", + "@stdlib/math-base-napi-unary": "^0.2.3", + "@stdlib/number-float64-base-to-words": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-acos": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-acos/-/math-base-special-acos-0.2.3.tgz", + "integrity": "sha512-f66Ikq0E3U5XQm6sTu4UHwP3TmcPrVgSK/mZTvg2JenswZ6qPtGO1A8KHZ5+/5bk1TSc9EW4zDGUqWG7mGzT4Q==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-fourth-pi": "^0.2.2", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-napi-unary": "^0.2.3", + "@stdlib/math-base-special-asin": "^0.2.2", + "@stdlib/math-base-special-sqrt": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-asin": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-asin/-/math-base-special-asin-0.2.3.tgz", + "integrity": "sha512-Ju1UFJspOOL630SqBtVmUh3lHv5JMu1szcAgx7kQupJwZiwWljoVQ5MmxlNY4l3nGM5oMokenlqTDNXOau43lw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-fourth-pi": "^0.2.2", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-napi-unary": "^0.2.3", + "@stdlib/math-base-special-sqrt": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-beta": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-beta/-/math-base-special-beta-0.2.1.tgz", + "integrity": "sha512-/crN/ptCu7ld7KodGkYUJIweUTHdxO5mw+rgkrMqNVqJ83QQPd1czB6hvNYFLfmhy3ckj7t/UYoYhhg/x/Wd7g==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-e": "^0.2.1", + "@stdlib/constants-float64-eps": "^0.2.1", + "@stdlib/math-base-assert-is-nan": "^0.2.1", + "@stdlib/math-base-special-abs": "^0.2.1", + "@stdlib/math-base-special-exp": "^0.2.1", + "@stdlib/math-base-special-log1p": "^0.2.1", + "@stdlib/math-base-special-pow": "^0.2.1", + "@stdlib/math-base-special-sqrt": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-betainc": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-betainc/-/math-base-special-betainc-0.2.2.tgz", + "integrity": "sha512-95tzDgn5d9RV9al4gxHwKfszd9M6AizlpnhAiwIi0JwqcO+OY3xgbABWal4/H09Tb8DaC9jDqiyGuyPuB0iDew==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/math-base-special-kernel-betainc": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-binomcoef": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-binomcoef/-/math-base-special-binomcoef-0.2.3.tgz", + "integrity": "sha512-RxnQ/QGgKUeqTvBL+7IH8rNKQYCfGs0I3PsFYfb0e9V1O2yIVvthURUpzjukurZM89JRapK1dN6aeZ5UM71Zgw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-max-safe-integer": "^0.2.2", + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/math-base-assert-is-integer": "^0.2.5", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-assert-is-odd": "^0.3.0", + "@stdlib/math-base-special-floor": "^0.2.3", + "@stdlib/math-base-special-gcd": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-ceil": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-ceil/-/math-base-special-ceil-0.2.2.tgz", + "integrity": "sha512-zGkDaMcPrxQ9Zo+fegf2MyI8UPIrVTK5sc/FgCN9qdwEFJTKGLsBd249T3xH7L2MDxx5JbIMGrr6L4U4uEm2Hw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/math-base-napi-unary": "^0.2.3", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-copysign": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-copysign/-/math-base-special-copysign-0.2.2.tgz", + "integrity": "sha512-m9nWIQhKsaNrZtS2vIPeToWDbzs/T0d0NWy7gSci38auQVufSbF6FYnCKl0f+uwiWlh5GYXs0uVbyCp7FFXN+A==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-high-word-abs-mask": "^0.2.2", + "@stdlib/constants-float64-high-word-sign-mask": "^0.2.1", + "@stdlib/math-base-napi-binary": "^0.3.0", + "@stdlib/number-float64-base-from-words": "^0.2.2", + "@stdlib/number-float64-base-get-high-word": "^0.2.2", + "@stdlib/number-float64-base-to-words": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-cos": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-cos/-/math-base-special-cos-0.2.1.tgz", + "integrity": "sha512-Yre+ASwsv4pQJk5dqY6488ZfmYDA6vtUTdapAVjCx28NluSFhXw1+S8EmsqnzYnqp/4x7Y1H7V2UPZfw+AdnbQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/math-base-special-kernel-cos": "^0.2.1", + "@stdlib/math-base-special-kernel-sin": "^0.2.1", + "@stdlib/math-base-special-rempio2": "^0.2.1", + "@stdlib/number-float64-base-get-high-word": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-erfc": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-erfc/-/math-base-special-erfc-0.2.4.tgz", + "integrity": "sha512-tVI+mMnW+oDfQXwoH86sZ8q4ximpUXX6wZFCYZB6KcO5GXeKuvK74DnU0YyIm+sTV+r9WJiTSBEW9iVQLZOkzg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-ninf": "^0.2.2", + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-napi-unary": "^0.2.3", + "@stdlib/math-base-special-exp": "^0.2.4", + "@stdlib/number-float64-base-set-low-word": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-erfcinv": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-erfcinv/-/math-base-special-erfcinv-0.2.3.tgz", + "integrity": "sha512-B8u7WZiIh0+rX8VWNOwvjPWpmeKBHIQoJtIigUseBgbch/rmgV43k63MCkjh2u+V2SmcFo38yD94qJg5bYyWeA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-ninf": "^0.2.2", + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-napi-unary": "^0.2.3", + "@stdlib/math-base-special-ln": "^0.2.4", + "@stdlib/math-base-special-sqrt": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-exp": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-exp/-/math-base-special-exp-0.2.4.tgz", + "integrity": "sha512-G6pZqu1wA4WwBj7DcnztA+/ro61wXJUTpKFLOwrIb2f/28pHGpA//Lub+3vAk6/ksAkhJ+qM/dfdM2ue7zLuEw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-ninf": "^0.2.2", + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-napi-unary": "^0.2.3", + "@stdlib/math-base-special-ldexp": "^0.2.3", + "@stdlib/math-base-special-trunc": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-expm1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-expm1/-/math-base-special-expm1-0.2.3.tgz", + "integrity": "sha512-uJlYZjPjG9X8owuwp1h1/iz9xf21v3dlyEAuutQ0NoacUDzZKVSCbQ3Of0i2Mujn+4N+kjCvEeph6cqhfYAl+A==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-exponent-bias": "^0.2.2", + "@stdlib/constants-float64-half-ln-two": "^0.2.2", + "@stdlib/constants-float64-ninf": "^0.2.2", + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-napi-unary": "^0.2.3", + "@stdlib/number-float64-base-from-words": "^0.2.2", + "@stdlib/number-float64-base-get-high-word": "^0.2.2", + "@stdlib/number-float64-base-set-high-word": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-factorial": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-factorial/-/math-base-special-factorial-0.2.1.tgz", + "integrity": "sha512-uqsANeW4gHFzhgDrV9X0INEwO74MPzQvDVXbxY9+b0E13Vq2HHCi0GqdtPOWXdhOCUk8RkLRs9GizU3X6Coy8A==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-pinf": "^0.2.1", + "@stdlib/math-base-assert-is-integer": "^0.2.1", + "@stdlib/math-base-assert-is-nan": "^0.2.1", + "@stdlib/math-base-special-gamma": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-floor": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-floor/-/math-base-special-floor-0.2.3.tgz", + "integrity": "sha512-zTkxVRawtWwJ4NmAT/1e+ZsIoBj1JqUquGOpiNVGNIKtyLOeCONZlZSbN7zuxPkshvmcSjpQ/VLKR8Tw/37E9A==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/math-base-napi-unary": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-fmod": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-fmod/-/math-base-special-fmod-0.1.0.tgz", + "integrity": "sha512-osHwmEOT5MPWOXRx8y3wKCp362eGHIcJRt8LARJJICr/qTZlu1HMnZnbwuhfy1NIQzpJ8aLOhEdl2PrProTt3A==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-exponent-bias": "^0.2.2", + "@stdlib/constants-float64-high-word-abs-mask": "^0.2.2", + "@stdlib/constants-float64-high-word-exponent-mask": "^0.2.2", + "@stdlib/constants-float64-high-word-sign-mask": "^0.2.1", + "@stdlib/constants-float64-high-word-significand-mask": "^0.2.2", + "@stdlib/constants-float64-min-base2-exponent": "^0.2.2", + "@stdlib/math-base-napi-binary": "^0.3.0", + "@stdlib/number-float64-base-from-words": "^0.2.2", + "@stdlib/number-float64-base-to-words": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-gamma": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-gamma/-/math-base-special-gamma-0.2.1.tgz", + "integrity": "sha512-Sfq1HnVoL4kN9EDHH3YparEAF0r7QD5jNFppUTOXmrqkofgImSl5tLttttnr2I7O9zsNhYkBAiTx9q0y25bAiA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-eulergamma": "^0.2.1", + "@stdlib/constants-float64-ninf": "^0.2.1", + "@stdlib/constants-float64-pi": "^0.2.1", + "@stdlib/constants-float64-pinf": "^0.2.1", + "@stdlib/constants-float64-sqrt-two-pi": "^0.2.1", + "@stdlib/math-base-assert-is-integer": "^0.2.1", + "@stdlib/math-base-assert-is-nan": "^0.2.1", + "@stdlib/math-base-assert-is-negative-zero": "^0.2.1", + "@stdlib/math-base-special-abs": "^0.2.1", + "@stdlib/math-base-special-exp": "^0.2.1", + "@stdlib/math-base-special-floor": "^0.2.1", + "@stdlib/math-base-special-pow": "^0.2.1", + "@stdlib/math-base-special-sin": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-gamma-delta-ratio": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-gamma-delta-ratio/-/math-base-special-gamma-delta-ratio-0.2.2.tgz", + "integrity": "sha512-lan+cfafH7aoyUxa88vLO+pYwLA+0uiyVFmCumxDemQUboCrTiNCYhBjONFGI/ljE3RukHoE3ZV4AccIcx526A==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-e": "^0.2.2", + "@stdlib/constants-float64-eps": "^0.2.2", + "@stdlib/constants-float64-gamma-lanczos-g": "^0.2.2", + "@stdlib/math-base-special-abs": "^0.2.2", + "@stdlib/math-base-special-exp": "^0.2.4", + "@stdlib/math-base-special-factorial": "^0.2.1", + "@stdlib/math-base-special-floor": "^0.2.3", + "@stdlib/math-base-special-gamma": "^0.2.1", + "@stdlib/math-base-special-gamma-lanczos-sum": "^0.3.0", + "@stdlib/math-base-special-log1p": "^0.2.3", + "@stdlib/math-base-special-pow": "^0.3.0" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-gamma-delta-ratio/node_modules/@stdlib/math-base-special-pow": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-pow/-/math-base-special-pow-0.3.0.tgz", + "integrity": "sha512-sMDYRUYGFyMXDHcCYy7hE07lV7jgI6rDspLMROKyESWcH4n8j54XE4/0w0i8OpdzR40H895MaPWU/tVnU1tP6w==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-exponent-bias": "^0.2.2", + "@stdlib/constants-float64-high-word-abs-mask": "^0.2.2", + "@stdlib/constants-float64-high-word-significand-mask": "^0.2.2", + "@stdlib/constants-float64-ln-two": "^0.2.2", + "@stdlib/constants-float64-ninf": "^0.2.2", + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/math-base-assert-is-infinite": "^0.2.2", + "@stdlib/math-base-assert-is-integer": "^0.2.5", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-assert-is-odd": "^0.3.0", + "@stdlib/math-base-napi-binary": "^0.3.0", + "@stdlib/math-base-special-abs": "^0.2.2", + "@stdlib/math-base-special-copysign": "^0.2.1", + "@stdlib/math-base-special-ldexp": "^0.2.2", + "@stdlib/math-base-special-sqrt": "^0.2.2", + "@stdlib/number-float64-base-get-high-word": "^0.2.2", + "@stdlib/number-float64-base-set-high-word": "^0.2.2", + "@stdlib/number-float64-base-set-low-word": "^0.2.2", + "@stdlib/number-float64-base-to-words": "^0.2.2", + "@stdlib/number-uint32-base-to-int32": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-gamma-lanczos-sum": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-gamma-lanczos-sum/-/math-base-special-gamma-lanczos-sum-0.3.0.tgz", + "integrity": "sha512-q13p6r7G0TmbD54cU8QgG8wGgdGGznV9dNKiNszw+hOqCQ+1DqziG8I6vN64R3EQLP7QN4yVprZcmuXSK+fgsg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/math-base-napi-unary": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-gamma-lanczos-sum-expg-scaled": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-gamma-lanczos-sum-expg-scaled/-/math-base-special-gamma-lanczos-sum-expg-scaled-0.3.0.tgz", + "integrity": "sha512-hScjKZvueOK5piX84ZLIV3ZiYvtvYtcixN8psxkPIxJlN7Bd5nAmSkEOBL+T+LeW2RjmdEMXFFJMF7FsK1js/Q==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/math-base-napi-unary": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-gamma1pm1": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-gamma1pm1/-/math-base-special-gamma1pm1-0.2.2.tgz", + "integrity": "sha512-lNT1lk0ifK2a/ta3GfR5V8KvfgkgheE44n5AQ/07BBfcVBMiAdqNuyjSMeWqsH/zVGzjU6G8+kLBzmaJXivPXQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-eps": "^0.2.2", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-special-expm1": "^0.2.3", + "@stdlib/math-base-special-gamma": "^0.2.1", + "@stdlib/math-base-special-ln": "^0.2.4", + "@stdlib/math-base-special-log1p": "^0.2.3" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-gammainc": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-gammainc/-/math-base-special-gammainc-0.2.2.tgz", + "integrity": "sha512-ffKZFiv/41SXs2Xms7IW3lPnICR898yfWAidq5uKjOLgRb3wrzNjq0sZ6EAVXvdBwyGULvSjyud28PpVhDLv3A==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-e": "^0.2.2", + "@stdlib/constants-float64-gamma-lanczos-g": "^0.2.2", + "@stdlib/constants-float64-max": "^0.2.2", + "@stdlib/constants-float64-max-ln": "^0.2.2", + "@stdlib/constants-float64-min-ln": "^0.2.2", + "@stdlib/constants-float64-pi": "^0.2.2", + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/constants-float64-sqrt-eps": "^0.2.2", + "@stdlib/constants-float64-sqrt-two-pi": "^0.2.2", + "@stdlib/constants-float64-two-pi": "^0.2.2", + "@stdlib/math-base-special-abs": "^0.2.2", + "@stdlib/math-base-special-erfc": "^0.2.4", + "@stdlib/math-base-special-exp": "^0.2.4", + "@stdlib/math-base-special-floor": "^0.2.3", + "@stdlib/math-base-special-gamma": "^0.3.0", + "@stdlib/math-base-special-gamma-lanczos-sum-expg-scaled": "^0.3.0", + "@stdlib/math-base-special-gamma1pm1": "^0.2.2", + "@stdlib/math-base-special-gammaln": "^0.2.2", + "@stdlib/math-base-special-ln": "^0.2.4", + "@stdlib/math-base-special-log1p": "^0.2.3", + "@stdlib/math-base-special-log1pmx": "^0.2.3", + "@stdlib/math-base-special-max": "^0.3.0", + "@stdlib/math-base-special-min": "^0.2.3", + "@stdlib/math-base-special-pow": "^0.3.0", + "@stdlib/math-base-special-powm1": "^0.3.0", + "@stdlib/math-base-special-sqrt": "^0.2.2", + "@stdlib/math-base-tools-continued-fraction": "^0.2.2", + "@stdlib/math-base-tools-evalpoly": "^0.2.2", + "@stdlib/math-base-tools-sum-series": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-gammainc/node_modules/@stdlib/math-base-special-gamma": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-gamma/-/math-base-special-gamma-0.3.0.tgz", + "integrity": "sha512-YfW+e5xuSDoUxgpquXPrFtAbdwOzE7Kqt7M0dcAkDNot8/yUn+QmrDGzURyBVzUyhRm9SaC9bACHxTShdJkcuA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-eulergamma": "^0.2.2", + "@stdlib/constants-float64-ninf": "^0.2.2", + "@stdlib/constants-float64-pi": "^0.2.2", + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/constants-float64-sqrt-two-pi": "^0.2.2", + "@stdlib/math-base-assert-is-integer": "^0.2.5", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-assert-is-negative-zero": "^0.2.2", + "@stdlib/math-base-napi-unary": "^0.2.3", + "@stdlib/math-base-special-abs": "^0.2.2", + "@stdlib/math-base-special-exp": "^0.2.4", + "@stdlib/math-base-special-floor": "^0.2.3", + "@stdlib/math-base-special-pow": "^0.3.0", + "@stdlib/math-base-special-sin": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-gammainc/node_modules/@stdlib/math-base-special-max": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-max/-/math-base-special-max-0.3.0.tgz", + "integrity": "sha512-yXsmdFCLHRB24l34Kn1kHZXHKoGqBxPY/5Mi+n5qLg+FwrX85ZG6KGVbO3DfcpG1NxDTcEKb1hxbUargI0P5fw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-assert-is-positive-zero": "^0.2.2", + "@stdlib/math-base-napi-binary": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-gammainc/node_modules/@stdlib/math-base-special-max/node_modules/@stdlib/math-base-napi-binary": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-napi-binary/-/math-base-napi-binary-0.2.1.tgz", + "integrity": "sha512-ewGarSRaz5gaLsE17yJ4me03e56ICgPAA0ru0SYFCeMK2E5Z4Z2Lbu7HAQTTg+8XhpoaZSw0h2GJopTV7PCKmw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/complex-float32": "^0.2.1", + "@stdlib/complex-float64": "^0.2.1", + "@stdlib/complex-reim": "^0.2.1", + "@stdlib/complex-reimf": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-gammainc/node_modules/@stdlib/math-base-special-pow": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-pow/-/math-base-special-pow-0.3.0.tgz", + "integrity": "sha512-sMDYRUYGFyMXDHcCYy7hE07lV7jgI6rDspLMROKyESWcH4n8j54XE4/0w0i8OpdzR40H895MaPWU/tVnU1tP6w==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-exponent-bias": "^0.2.2", + "@stdlib/constants-float64-high-word-abs-mask": "^0.2.2", + "@stdlib/constants-float64-high-word-significand-mask": "^0.2.2", + "@stdlib/constants-float64-ln-two": "^0.2.2", + "@stdlib/constants-float64-ninf": "^0.2.2", + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/math-base-assert-is-infinite": "^0.2.2", + "@stdlib/math-base-assert-is-integer": "^0.2.5", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-assert-is-odd": "^0.3.0", + "@stdlib/math-base-napi-binary": "^0.3.0", + "@stdlib/math-base-special-abs": "^0.2.2", + "@stdlib/math-base-special-copysign": "^0.2.1", + "@stdlib/math-base-special-ldexp": "^0.2.2", + "@stdlib/math-base-special-sqrt": "^0.2.2", + "@stdlib/number-float64-base-get-high-word": "^0.2.2", + "@stdlib/number-float64-base-set-high-word": "^0.2.2", + "@stdlib/number-float64-base-set-low-word": "^0.2.2", + "@stdlib/number-float64-base-to-words": "^0.2.2", + "@stdlib/number-uint32-base-to-int32": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-gammaincinv": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-gammaincinv/-/math-base-special-gammaincinv-0.2.2.tgz", + "integrity": "sha512-bIZ94ob1rY87seDWsvBTBRxp8Ja2Y46DLtQYuaylHUQuK+I2xKy8XKL2ZHPsOfuwhXRqm+q+91PDjPEAdH1dQw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float32-max": "^0.2.2", + "@stdlib/constants-float32-smallest-normal": "^0.2.2", + "@stdlib/constants-float64-ln-sqrt-two-pi": "^0.2.2", + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/constants-float64-sqrt-two-pi": "^0.2.2", + "@stdlib/constants-float64-two-pi": "^0.2.2", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-special-abs": "^0.2.2", + "@stdlib/math-base-special-erfcinv": "^0.2.3", + "@stdlib/math-base-special-exp": "^0.2.4", + "@stdlib/math-base-special-gamma": "^0.3.0", + "@stdlib/math-base-special-gammainc": "^0.2.2", + "@stdlib/math-base-special-gammaln": "^0.2.2", + "@stdlib/math-base-special-ln": "^0.2.4", + "@stdlib/math-base-special-min": "^0.2.3", + "@stdlib/math-base-special-pow": "^0.3.0", + "@stdlib/math-base-special-sqrt": "^0.2.2", + "@stdlib/math-base-tools-evalpoly": "^0.2.2", + "debug": "^2.6.9" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-gammaincinv/node_modules/@stdlib/math-base-special-gamma": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-gamma/-/math-base-special-gamma-0.3.0.tgz", + "integrity": "sha512-YfW+e5xuSDoUxgpquXPrFtAbdwOzE7Kqt7M0dcAkDNot8/yUn+QmrDGzURyBVzUyhRm9SaC9bACHxTShdJkcuA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-eulergamma": "^0.2.2", + "@stdlib/constants-float64-ninf": "^0.2.2", + "@stdlib/constants-float64-pi": "^0.2.2", + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/constants-float64-sqrt-two-pi": "^0.2.2", + "@stdlib/math-base-assert-is-integer": "^0.2.5", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-assert-is-negative-zero": "^0.2.2", + "@stdlib/math-base-napi-unary": "^0.2.3", + "@stdlib/math-base-special-abs": "^0.2.2", + "@stdlib/math-base-special-exp": "^0.2.4", + "@stdlib/math-base-special-floor": "^0.2.3", + "@stdlib/math-base-special-pow": "^0.3.0", + "@stdlib/math-base-special-sin": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-gammaincinv/node_modules/@stdlib/math-base-special-pow": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-pow/-/math-base-special-pow-0.3.0.tgz", + "integrity": "sha512-sMDYRUYGFyMXDHcCYy7hE07lV7jgI6rDspLMROKyESWcH4n8j54XE4/0w0i8OpdzR40H895MaPWU/tVnU1tP6w==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-exponent-bias": "^0.2.2", + "@stdlib/constants-float64-high-word-abs-mask": "^0.2.2", + "@stdlib/constants-float64-high-word-significand-mask": "^0.2.2", + "@stdlib/constants-float64-ln-two": "^0.2.2", + "@stdlib/constants-float64-ninf": "^0.2.2", + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/math-base-assert-is-infinite": "^0.2.2", + "@stdlib/math-base-assert-is-integer": "^0.2.5", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-assert-is-odd": "^0.3.0", + "@stdlib/math-base-napi-binary": "^0.3.0", + "@stdlib/math-base-special-abs": "^0.2.2", + "@stdlib/math-base-special-copysign": "^0.2.1", + "@stdlib/math-base-special-ldexp": "^0.2.2", + "@stdlib/math-base-special-sqrt": "^0.2.2", + "@stdlib/number-float64-base-get-high-word": "^0.2.2", + "@stdlib/number-float64-base-set-high-word": "^0.2.2", + "@stdlib/number-float64-base-set-low-word": "^0.2.2", + "@stdlib/number-float64-base-to-words": "^0.2.2", + "@stdlib/number-uint32-base-to-int32": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-gammaln": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-gammaln/-/math-base-special-gammaln-0.2.2.tgz", + "integrity": "sha512-opG6HUlspi/GLvQAr4pcwyAevm7BYuymlopgNZ1VulWUvksDpytalaX3zva0idlD2HvniKrDmzHngT1N9p0J1A==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-pi": "^0.2.2", + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/math-base-assert-is-infinite": "^0.2.2", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-special-abs": "^0.2.2", + "@stdlib/math-base-special-ln": "^0.2.4", + "@stdlib/math-base-special-sinpi": "^0.2.1", + "@stdlib/math-base-special-trunc": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-gcd": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-gcd/-/math-base-special-gcd-0.2.1.tgz", + "integrity": "sha512-w10k9W176lDkbiDIwnmVr1nkTyypTQLwA3/CN9qEUmXh/u8NlxkSnDYBpArcWnxE0oFaIggw8sLJ58TuMvxMaw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-ninf": "^0.2.1", + "@stdlib/constants-float64-pinf": "^0.2.1", + "@stdlib/constants-int32-max": "^0.2.1", + "@stdlib/math-base-assert-is-integer": "^0.2.1", + "@stdlib/math-base-assert-is-nan": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-gcd/node_modules/@stdlib/constants-int32-max": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/constants-int32-max/-/constants-int32-max-0.2.1.tgz", + "integrity": "sha512-vKtp3q/HdAeGG8BJBZdNzFrYpVQeleODgvOxh9Pn/TX1Ktjc50I9TVl7nTVWsT2QnacruOorILk2zNsdgBHPUQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-kernel-betainc": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-kernel-betainc/-/math-base-special-kernel-betainc-0.2.2.tgz", + "integrity": "sha512-DQwQUWQkmZtjRgdvZ1yZOEdAYLQoEUEndbr47Z69Oe6AgwKwxxpZUh09h9imKheFCFHLVnwVUz20azIM5KifQw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-e": "^0.2.2", + "@stdlib/constants-float64-eps": "^0.2.2", + "@stdlib/constants-float64-gamma-lanczos-g": "^0.2.2", + "@stdlib/constants-float64-half-pi": "^0.2.2", + "@stdlib/constants-float64-max": "^0.2.2", + "@stdlib/constants-float64-max-ln": "^0.2.2", + "@stdlib/constants-float64-min-ln": "^0.2.2", + "@stdlib/constants-float64-pi": "^0.2.2", + "@stdlib/constants-float64-smallest-normal": "^0.2.2", + "@stdlib/constants-int32-max": "^0.3.0", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-special-abs": "^0.2.2", + "@stdlib/math-base-special-asin": "^0.2.3", + "@stdlib/math-base-special-beta": "^0.3.0", + "@stdlib/math-base-special-binomcoef": "^0.2.3", + "@stdlib/math-base-special-exp": "^0.2.4", + "@stdlib/math-base-special-expm1": "^0.2.3", + "@stdlib/math-base-special-factorial": "^0.3.0", + "@stdlib/math-base-special-floor": "^0.2.3", + "@stdlib/math-base-special-gamma": "^0.3.0", + "@stdlib/math-base-special-gamma-delta-ratio": "^0.2.2", + "@stdlib/math-base-special-gamma-lanczos-sum-expg-scaled": "^0.3.0", + "@stdlib/math-base-special-gammainc": "^0.2.1", + "@stdlib/math-base-special-gammaln": "^0.2.2", + "@stdlib/math-base-special-ln": "^0.2.4", + "@stdlib/math-base-special-log1p": "^0.2.3", + "@stdlib/math-base-special-max": "^0.3.0", + "@stdlib/math-base-special-maxabs": "^0.3.0", + "@stdlib/math-base-special-min": "^0.2.3", + "@stdlib/math-base-special-minabs": "^0.2.3", + "@stdlib/math-base-special-pow": "^0.3.0", + "@stdlib/math-base-special-sqrt": "^0.2.2", + "@stdlib/math-base-tools-continued-fraction": "^0.2.2", + "@stdlib/math-base-tools-sum-series": "^0.2.2", + "@stdlib/utils-define-nonenumerable-read-only-property": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-kernel-betainc/node_modules/@stdlib/math-base-special-beta": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-beta/-/math-base-special-beta-0.3.0.tgz", + "integrity": "sha512-SWUF1AZLqaEJ8g1Lj0/UOfj955AsIS3QPYH/ZMijELVxCwmp7VRgalI0AxMM09IraJt1cH5WrSwSnouH1WC3ZQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-e": "^0.2.2", + "@stdlib/constants-float64-eps": "^0.2.2", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-napi-binary": "^0.3.0", + "@stdlib/math-base-special-abs": "^0.2.2", + "@stdlib/math-base-special-exp": "^0.2.4", + "@stdlib/math-base-special-log1p": "^0.2.3", + "@stdlib/math-base-special-pow": "^0.3.0", + "@stdlib/math-base-special-sqrt": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-kernel-betainc/node_modules/@stdlib/math-base-special-factorial": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-factorial/-/math-base-special-factorial-0.3.0.tgz", + "integrity": "sha512-tXdXqstF4gmy4HpzALo3Bhkj2UQSlyk+PU3alWXZH5XtKUozHuXhQDnak+2c4w0JqnKxHq4mnaR2qgjfkDNZcA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-max-safe-nth-factorial": "^0.1.0", + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/math-base-assert-is-integer": "^0.2.5", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-napi-unary": "^0.2.3", + "@stdlib/math-base-special-gamma": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-kernel-betainc/node_modules/@stdlib/math-base-special-factorial/node_modules/@stdlib/math-base-assert-is-odd": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-assert-is-odd/-/math-base-assert-is-odd-0.2.1.tgz", + "integrity": "sha512-V4qQuCO6/AA5udqlNatMRZ8R/MgpqD8mPIkFrpSZJdpLcGYSz815uAAf3NBOuWXkE2Izw0/Tg/hTQ+YcOW2g5g==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/math-base-assert-is-even": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-kernel-betainc/node_modules/@stdlib/math-base-special-factorial/node_modules/@stdlib/math-base-special-gamma": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-gamma/-/math-base-special-gamma-0.2.1.tgz", + "integrity": "sha512-Sfq1HnVoL4kN9EDHH3YparEAF0r7QD5jNFppUTOXmrqkofgImSl5tLttttnr2I7O9zsNhYkBAiTx9q0y25bAiA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-eulergamma": "^0.2.1", + "@stdlib/constants-float64-ninf": "^0.2.1", + "@stdlib/constants-float64-pi": "^0.2.1", + "@stdlib/constants-float64-pinf": "^0.2.1", + "@stdlib/constants-float64-sqrt-two-pi": "^0.2.1", + "@stdlib/math-base-assert-is-integer": "^0.2.1", + "@stdlib/math-base-assert-is-nan": "^0.2.1", + "@stdlib/math-base-assert-is-negative-zero": "^0.2.1", + "@stdlib/math-base-special-abs": "^0.2.1", + "@stdlib/math-base-special-exp": "^0.2.1", + "@stdlib/math-base-special-floor": "^0.2.1", + "@stdlib/math-base-special-pow": "^0.2.1", + "@stdlib/math-base-special-sin": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-kernel-betainc/node_modules/@stdlib/math-base-special-factorial/node_modules/@stdlib/math-base-special-pow": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-pow/-/math-base-special-pow-0.2.1.tgz", + "integrity": "sha512-7SvgVzDkuilZKrHh4tPiXx9fypF/V7PSvAcUVjvcRj5kVEwv/15RpzlmCJlT9B20VPSx4gJ1S0UIA6xgmYFuAg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-exponent-bias": "^0.2.1", + "@stdlib/constants-float64-high-word-abs-mask": "^0.2.1", + "@stdlib/constants-float64-high-word-significand-mask": "^0.2.1", + "@stdlib/constants-float64-ln-two": "^0.2.1", + "@stdlib/constants-float64-ninf": "^0.2.1", + "@stdlib/constants-float64-pinf": "^0.2.1", + "@stdlib/math-base-assert-is-infinite": "^0.2.1", + "@stdlib/math-base-assert-is-integer": "^0.2.1", + "@stdlib/math-base-assert-is-nan": "^0.2.1", + "@stdlib/math-base-assert-is-odd": "^0.2.1", + "@stdlib/math-base-special-abs": "^0.2.1", + "@stdlib/math-base-special-copysign": "^0.2.1", + "@stdlib/math-base-special-ldexp": "^0.2.1", + "@stdlib/math-base-special-sqrt": "^0.2.1", + "@stdlib/number-float64-base-get-high-word": "^0.2.1", + "@stdlib/number-float64-base-set-high-word": "^0.2.1", + "@stdlib/number-float64-base-set-low-word": "^0.2.1", + "@stdlib/number-float64-base-to-words": "^0.2.1", + "@stdlib/number-uint32-base-to-int32": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-kernel-betainc/node_modules/@stdlib/math-base-special-gamma": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-gamma/-/math-base-special-gamma-0.3.0.tgz", + "integrity": "sha512-YfW+e5xuSDoUxgpquXPrFtAbdwOzE7Kqt7M0dcAkDNot8/yUn+QmrDGzURyBVzUyhRm9SaC9bACHxTShdJkcuA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-eulergamma": "^0.2.2", + "@stdlib/constants-float64-ninf": "^0.2.2", + "@stdlib/constants-float64-pi": "^0.2.2", + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/constants-float64-sqrt-two-pi": "^0.2.2", + "@stdlib/math-base-assert-is-integer": "^0.2.5", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-assert-is-negative-zero": "^0.2.2", + "@stdlib/math-base-napi-unary": "^0.2.3", + "@stdlib/math-base-special-abs": "^0.2.2", + "@stdlib/math-base-special-exp": "^0.2.4", + "@stdlib/math-base-special-floor": "^0.2.3", + "@stdlib/math-base-special-pow": "^0.3.0", + "@stdlib/math-base-special-sin": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-kernel-betainc/node_modules/@stdlib/math-base-special-max": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-max/-/math-base-special-max-0.3.0.tgz", + "integrity": "sha512-yXsmdFCLHRB24l34Kn1kHZXHKoGqBxPY/5Mi+n5qLg+FwrX85ZG6KGVbO3DfcpG1NxDTcEKb1hxbUargI0P5fw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-assert-is-positive-zero": "^0.2.2", + "@stdlib/math-base-napi-binary": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-kernel-betainc/node_modules/@stdlib/math-base-special-max/node_modules/@stdlib/math-base-napi-binary": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-napi-binary/-/math-base-napi-binary-0.2.1.tgz", + "integrity": "sha512-ewGarSRaz5gaLsE17yJ4me03e56ICgPAA0ru0SYFCeMK2E5Z4Z2Lbu7HAQTTg+8XhpoaZSw0h2GJopTV7PCKmw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/complex-float32": "^0.2.1", + "@stdlib/complex-float64": "^0.2.1", + "@stdlib/complex-reim": "^0.2.1", + "@stdlib/complex-reimf": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-kernel-betainc/node_modules/@stdlib/math-base-special-pow": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-pow/-/math-base-special-pow-0.3.0.tgz", + "integrity": "sha512-sMDYRUYGFyMXDHcCYy7hE07lV7jgI6rDspLMROKyESWcH4n8j54XE4/0w0i8OpdzR40H895MaPWU/tVnU1tP6w==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-exponent-bias": "^0.2.2", + "@stdlib/constants-float64-high-word-abs-mask": "^0.2.2", + "@stdlib/constants-float64-high-word-significand-mask": "^0.2.2", + "@stdlib/constants-float64-ln-two": "^0.2.2", + "@stdlib/constants-float64-ninf": "^0.2.2", + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/math-base-assert-is-infinite": "^0.2.2", + "@stdlib/math-base-assert-is-integer": "^0.2.5", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-assert-is-odd": "^0.3.0", + "@stdlib/math-base-napi-binary": "^0.3.0", + "@stdlib/math-base-special-abs": "^0.2.2", + "@stdlib/math-base-special-copysign": "^0.2.1", + "@stdlib/math-base-special-ldexp": "^0.2.2", + "@stdlib/math-base-special-sqrt": "^0.2.2", + "@stdlib/number-float64-base-get-high-word": "^0.2.2", + "@stdlib/number-float64-base-set-high-word": "^0.2.2", + "@stdlib/number-float64-base-set-low-word": "^0.2.2", + "@stdlib/number-float64-base-to-words": "^0.2.2", + "@stdlib/number-uint32-base-to-int32": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-kernel-betaincinv": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-kernel-betaincinv/-/math-base-special-kernel-betaincinv-0.1.1.tgz", + "integrity": "sha512-DZLALmQj0m3Wx8L8/na8Jj9vluNj4Z5DxmAPvnA1AWGYy7KsotmP4HXwgSTlsfbXeF3iGcrmworPOo4HJUSxIQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-eps": "^0.2.1", + "@stdlib/constants-float64-half-pi": "^0.2.1", + "@stdlib/constants-float64-max": "^0.2.1", + "@stdlib/constants-float64-pi": "^0.2.1", + "@stdlib/constants-float64-smallest-normal": "^0.2.1", + "@stdlib/constants-float64-smallest-subnormal": "^0.2.1", + "@stdlib/constants-float64-sqrt-two": "^0.2.1", + "@stdlib/math-base-special-abs": "^0.2.1", + "@stdlib/math-base-special-acos": "^0.2.1", + "@stdlib/math-base-special-asin": "^0.2.1", + "@stdlib/math-base-special-beta": "^0.2.1", + "@stdlib/math-base-special-betainc": "^0.2.1", + "@stdlib/math-base-special-cos": "^0.2.1", + "@stdlib/math-base-special-erfcinv": "^0.2.1", + "@stdlib/math-base-special-exp": "^0.2.1", + "@stdlib/math-base-special-expm1": "^0.2.1", + "@stdlib/math-base-special-floor": "^0.2.1", + "@stdlib/math-base-special-gamma-delta-ratio": "^0.2.1", + "@stdlib/math-base-special-gammaincinv": "^0.2.1", + "@stdlib/math-base-special-kernel-betainc": "^0.2.1", + "@stdlib/math-base-special-ldexp": "^0.2.1", + "@stdlib/math-base-special-ln": "^0.2.1", + "@stdlib/math-base-special-log1p": "^0.2.1", + "@stdlib/math-base-special-max": "^0.2.1", + "@stdlib/math-base-special-min": "^0.2.1", + "@stdlib/math-base-special-pow": "^0.2.1", + "@stdlib/math-base-special-round": "^0.2.1", + "@stdlib/math-base-special-signum": "^0.2.1", + "@stdlib/math-base-special-sin": "^0.2.1", + "@stdlib/math-base-special-sqrt": "^0.2.1", + "@stdlib/math-base-tools-evalpoly": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-kernel-cos": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-kernel-cos/-/math-base-special-kernel-cos-0.2.3.tgz", + "integrity": "sha512-K5FbN25SmEc5Z89GejUkrZpqCv05ZX6D7g9SUFcKWFJ1fwiZNgxrF8q4aJtGDQhuV3q66C1gaKJyQeLq/OI8lQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/math-base-napi-binary": "^0.3.0", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-kernel-sin": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-kernel-sin/-/math-base-special-kernel-sin-0.2.3.tgz", + "integrity": "sha512-PFnlGdapUaCaMXqZr+tG5Ioq+l4TCyGE5e8XEYlsyhNDILf0XE2ghHzlROA/wW365Arl4sPLWUoo4oH98DUPqw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/math-base-napi-binary": "^0.3.0", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-ldexp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-ldexp/-/math-base-special-ldexp-0.2.3.tgz", + "integrity": "sha512-yD4YisQGVTJmTJUshuzpaoq34sxJtrU+Aw4Ih39mzgXiQi6sh3E3nijB8WXDNKr2v05acUWJ1PRMkkJSfu16Kg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-exponent-bias": "^0.2.2", + "@stdlib/constants-float64-max-base2-exponent": "^0.2.2", + "@stdlib/constants-float64-max-base2-exponent-subnormal": "^0.2.1", + "@stdlib/constants-float64-min-base2-exponent-subnormal": "^0.2.1", + "@stdlib/constants-float64-ninf": "^0.2.2", + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/math-base-assert-is-infinite": "^0.2.2", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-special-abs": "^0.2.2", + "@stdlib/math-base-special-copysign": "^0.2.1", + "@stdlib/number-float32-base-to-word": "^0.2.2", + "@stdlib/number-float64-base-exponent": "^0.2.2", + "@stdlib/number-float64-base-from-words": "^0.2.2", + "@stdlib/number-float64-base-normalize": "^0.2.3", + "@stdlib/number-float64-base-to-words": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-ln": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-ln/-/math-base-special-ln-0.2.4.tgz", + "integrity": "sha512-lSB47USaixrEmxwadT0/yByvTtxNhaRwN0FIXt5oj38bsgMXGW4V8xrANOy1N+hrn3KGfHJNDyFPYbXWVdMTIw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-exponent-bias": "^0.2.2", + "@stdlib/constants-float64-ninf": "^0.2.2", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-napi-unary": "^0.2.1", + "@stdlib/number-float64-base-get-high-word": "^0.2.2", + "@stdlib/number-float64-base-set-high-word": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-log1p": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-log1p/-/math-base-special-log1p-0.2.3.tgz", + "integrity": "sha512-1Pu3attNR+DcskIvhvyls+2KRZ0UCHQ/jP2tvgFI9bWDCgb4oEimXPzjFteGNg9Mj6WlAW2b9wU9tHt3bp8R3g==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-exponent-bias": "^0.2.2", + "@stdlib/constants-float64-ninf": "^0.2.2", + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-napi-unary": "^0.2.1", + "@stdlib/number-float64-base-get-high-word": "^0.2.2", + "@stdlib/number-float64-base-set-high-word": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-log1pmx": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-log1pmx/-/math-base-special-log1pmx-0.2.3.tgz", + "integrity": "sha512-HfjDXcbFztm/GQRrn7a9FMYS0rm/4VPXWa50sYQzBHSYaEwYv5Y1awaZz+cA/ncuqAq1Mw0dfcwEMNRmZtnxEQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-eps": "^0.2.2", + "@stdlib/math-base-napi-unary": "^0.2.3", + "@stdlib/math-base-special-abs": "^0.2.2", + "@stdlib/math-base-special-ln": "^0.2.4", + "@stdlib/math-base-special-log1p": "^0.2.3", + "@stdlib/math-base-tools-sum-series": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-max": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-max/-/math-base-special-max-0.2.1.tgz", + "integrity": "sha512-jsA3x5azfclbULDFwvHjNlB2nciUDHwrw7qHP/QlSdJi47E1iBDNYdzhlOa3JKzblbrITpzgZEsGBcpCinEInQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-pinf": "^0.2.1", + "@stdlib/math-base-assert-is-nan": "^0.2.1", + "@stdlib/math-base-assert-is-positive-zero": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-maxabs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-maxabs/-/math-base-special-maxabs-0.3.0.tgz", + "integrity": "sha512-SDj+rGD9itZ/YG2hKzhLX4Tf13SNJdOyNsMy1ezjec6Az3xJXKzv2wJAJIteo0KF6jQnEDkI/F6OIF65MY+o0g==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/math-base-napi-binary": "^0.2.1", + "@stdlib/math-base-special-abs": "^0.2.2", + "@stdlib/math-base-special-max": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-maxabs/node_modules/@stdlib/math-base-napi-binary": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-napi-binary/-/math-base-napi-binary-0.2.1.tgz", + "integrity": "sha512-ewGarSRaz5gaLsE17yJ4me03e56ICgPAA0ru0SYFCeMK2E5Z4Z2Lbu7HAQTTg+8XhpoaZSw0h2GJopTV7PCKmw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/complex-float32": "^0.2.1", + "@stdlib/complex-float64": "^0.2.1", + "@stdlib/complex-reim": "^0.2.1", + "@stdlib/complex-reimf": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-min": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-min/-/math-base-special-min-0.2.3.tgz", + "integrity": "sha512-tNrKnkcHCRVWzteZJpZ/xql9B6N6EzecnUVizDYqG9y66bOVtI+TADcQ5I/bijEwAIi2BjrIVeq/TBEgQEQBkw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-ninf": "^0.2.2", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-assert-is-negative-zero": "^0.2.2", + "@stdlib/math-base-napi-binary": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-min/node_modules/@stdlib/math-base-napi-binary": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-napi-binary/-/math-base-napi-binary-0.2.1.tgz", + "integrity": "sha512-ewGarSRaz5gaLsE17yJ4me03e56ICgPAA0ru0SYFCeMK2E5Z4Z2Lbu7HAQTTg+8XhpoaZSw0h2GJopTV7PCKmw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/complex-float32": "^0.2.1", + "@stdlib/complex-float64": "^0.2.1", + "@stdlib/complex-reim": "^0.2.1", + "@stdlib/complex-reimf": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-minabs": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-minabs/-/math-base-special-minabs-0.2.3.tgz", + "integrity": "sha512-IV7PSL09S2GHmsxxtFgebPEwLm/wHnC1e1ulP8Uiuo2zinOiv4NXy2tpf9T+nq95d0ICFMnr9IGxFs6Nd74hRw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/math-base-napi-binary": "^0.2.1", + "@stdlib/math-base-special-abs": "^0.2.2", + "@stdlib/math-base-special-min": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-minabs/node_modules/@stdlib/math-base-napi-binary": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-napi-binary/-/math-base-napi-binary-0.2.1.tgz", + "integrity": "sha512-ewGarSRaz5gaLsE17yJ4me03e56ICgPAA0ru0SYFCeMK2E5Z4Z2Lbu7HAQTTg+8XhpoaZSw0h2GJopTV7PCKmw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/complex-float32": "^0.2.1", + "@stdlib/complex-float64": "^0.2.1", + "@stdlib/complex-reim": "^0.2.1", + "@stdlib/complex-reimf": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-pow": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-pow/-/math-base-special-pow-0.2.1.tgz", + "integrity": "sha512-7SvgVzDkuilZKrHh4tPiXx9fypF/V7PSvAcUVjvcRj5kVEwv/15RpzlmCJlT9B20VPSx4gJ1S0UIA6xgmYFuAg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-exponent-bias": "^0.2.1", + "@stdlib/constants-float64-high-word-abs-mask": "^0.2.1", + "@stdlib/constants-float64-high-word-significand-mask": "^0.2.1", + "@stdlib/constants-float64-ln-two": "^0.2.1", + "@stdlib/constants-float64-ninf": "^0.2.1", + "@stdlib/constants-float64-pinf": "^0.2.1", + "@stdlib/math-base-assert-is-infinite": "^0.2.1", + "@stdlib/math-base-assert-is-integer": "^0.2.1", + "@stdlib/math-base-assert-is-nan": "^0.2.1", + "@stdlib/math-base-assert-is-odd": "^0.2.1", + "@stdlib/math-base-special-abs": "^0.2.1", + "@stdlib/math-base-special-copysign": "^0.2.1", + "@stdlib/math-base-special-ldexp": "^0.2.1", + "@stdlib/math-base-special-sqrt": "^0.2.1", + "@stdlib/number-float64-base-get-high-word": "^0.2.1", + "@stdlib/number-float64-base-set-high-word": "^0.2.1", + "@stdlib/number-float64-base-set-low-word": "^0.2.1", + "@stdlib/number-float64-base-to-words": "^0.2.1", + "@stdlib/number-uint32-base-to-int32": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-pow/node_modules/@stdlib/math-base-assert-is-odd": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-assert-is-odd/-/math-base-assert-is-odd-0.2.1.tgz", + "integrity": "sha512-V4qQuCO6/AA5udqlNatMRZ8R/MgpqD8mPIkFrpSZJdpLcGYSz815uAAf3NBOuWXkE2Izw0/Tg/hTQ+YcOW2g5g==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/math-base-assert-is-even": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-powm1": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-powm1/-/math-base-special-powm1-0.3.1.tgz", + "integrity": "sha512-Pz7e2JlZH9EktJCDuyFPoT9IxMUSiZiJquyh2xB92NQQi9CAIdyaPUryNo36LxG65bne5GZF47MeiWCE8oWgiA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/math-base-assert-is-infinite": "^0.2.2", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-napi-binary": "^0.3.0", + "@stdlib/math-base-special-abs": "^0.2.2", + "@stdlib/math-base-special-expm1": "^0.2.3", + "@stdlib/math-base-special-fmod": "^0.1.0", + "@stdlib/math-base-special-ln": "^0.2.4", + "@stdlib/math-base-special-pow": "^0.3.0", + "@stdlib/math-base-special-trunc": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-powm1/node_modules/@stdlib/math-base-special-pow": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-pow/-/math-base-special-pow-0.3.0.tgz", + "integrity": "sha512-sMDYRUYGFyMXDHcCYy7hE07lV7jgI6rDspLMROKyESWcH4n8j54XE4/0w0i8OpdzR40H895MaPWU/tVnU1tP6w==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-exponent-bias": "^0.2.2", + "@stdlib/constants-float64-high-word-abs-mask": "^0.2.2", + "@stdlib/constants-float64-high-word-significand-mask": "^0.2.2", + "@stdlib/constants-float64-ln-two": "^0.2.2", + "@stdlib/constants-float64-ninf": "^0.2.2", + "@stdlib/constants-float64-pinf": "^0.2.2", + "@stdlib/math-base-assert-is-infinite": "^0.2.2", + "@stdlib/math-base-assert-is-integer": "^0.2.5", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-assert-is-odd": "^0.3.0", + "@stdlib/math-base-napi-binary": "^0.3.0", + "@stdlib/math-base-special-abs": "^0.2.2", + "@stdlib/math-base-special-copysign": "^0.2.1", + "@stdlib/math-base-special-ldexp": "^0.2.2", + "@stdlib/math-base-special-sqrt": "^0.2.2", + "@stdlib/number-float64-base-get-high-word": "^0.2.2", + "@stdlib/number-float64-base-set-high-word": "^0.2.2", + "@stdlib/number-float64-base-set-low-word": "^0.2.2", + "@stdlib/number-float64-base-to-words": "^0.2.2", + "@stdlib/number-uint32-base-to-int32": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-rempio2": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-rempio2/-/math-base-special-rempio2-0.2.1.tgz", + "integrity": "sha512-ErV5EAe3SQCSijg4Pi4Z0sRPOGrODF3jkyCeiLM+iYj2TMOwDaOWQ0xCTME0p9G45TDrbZCLM5arxN83TfzgXQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/array-base-zeros": "^0.2.1", + "@stdlib/constants-float64-high-word-abs-mask": "^0.2.1", + "@stdlib/constants-float64-high-word-exponent-mask": "^0.2.1", + "@stdlib/constants-float64-high-word-significand-mask": "^0.2.1", + "@stdlib/math-base-special-floor": "^0.2.1", + "@stdlib/math-base-special-ldexp": "^0.2.1", + "@stdlib/math-base-special-round": "^0.2.1", + "@stdlib/number-float64-base-from-words": "^0.2.1", + "@stdlib/number-float64-base-get-high-word": "^0.2.1", + "@stdlib/number-float64-base-get-low-word": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-round": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-round/-/math-base-special-round-0.2.1.tgz", + "integrity": "sha512-ibeKiN9z//6wS4H4uaa+vGnh/t1vJtZYXz+NqRVtwoP+nnE/mtL+fIrBlAnkIWVIH+smQPNNo8qsohjyGLBvUQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-signum": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-signum/-/math-base-special-signum-0.2.2.tgz", + "integrity": "sha512-cszwgkfeMTnUiORRWdWv6Q/tpoXkXkMYNMoAFO5qzHTuahnDP37Lkn8fTmCEtgHEasg3Cm69xLbqP0UDuNPHyA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-napi-unary": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-sin": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-sin/-/math-base-special-sin-0.2.1.tgz", + "integrity": "sha512-IQ6+bzfiZ6/VUn5DIe6iwCsYERE1pwtAOsAWkgNZ1Ih3FzXUxdEOyHtv1zraPrVUb8mR+V5q7OfAGy8TCTnkUg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-high-word-abs-mask": "^0.2.1", + "@stdlib/constants-float64-high-word-exponent-mask": "^0.2.1", + "@stdlib/math-base-special-kernel-cos": "^0.2.1", + "@stdlib/math-base-special-kernel-sin": "^0.2.1", + "@stdlib/math-base-special-rempio2": "^0.2.1", + "@stdlib/number-float64-base-get-high-word": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-sinpi": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-sinpi/-/math-base-special-sinpi-0.2.1.tgz", + "integrity": "sha512-Q3yCp1CoD7gemIILO28bU7iBn8OFiCgXm9vP/9q0tRBxmjtiUnjqbFd+3jRXdAmiCc/B/bPjwGBtVnCnrEMY9g==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-pi": "^0.2.1", + "@stdlib/math-base-assert-is-infinite": "^0.2.1", + "@stdlib/math-base-assert-is-nan": "^0.2.1", + "@stdlib/math-base-special-abs": "^0.2.1", + "@stdlib/math-base-special-copysign": "^0.2.1", + "@stdlib/math-base-special-cos": "^0.2.1", + "@stdlib/math-base-special-sin": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-sqrt": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-sqrt/-/math-base-special-sqrt-0.2.2.tgz", + "integrity": "sha512-YWxe9vVE5blDbRPDAdZfU03vfGTBHy/8pLDa/qLz7SiJj5n5sVWKObdbMR2oPHF4c6DaZh4IYkrcHFleiY8YkQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/math-base-napi-unary": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-special-trunc": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-special-trunc/-/math-base-special-trunc-0.2.2.tgz", + "integrity": "sha512-cvizbo6oFEbdiv7BrtEMODGW+cJcBgyAIleJnIpCf75C722Y/IZgWikWhACSjv4stxGywFubx85B7uvm3vLgwA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/math-base-napi-unary": "^0.2.3", + "@stdlib/math-base-special-ceil": "^0.2.1", + "@stdlib/math-base-special-floor": "^0.2.3", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-tools-continued-fraction": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-tools-continued-fraction/-/math-base-tools-continued-fraction-0.2.2.tgz", + "integrity": "sha512-5dm72lTXwSVOsBsOLF57RZqqHehRd9X3HKdQ3WhOoHx7fNc0lxJJEDjtK8gMdV3NvfoER1MBiGbs2h23oaK5qw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-has-generator-support": "^0.2.2", + "@stdlib/constants-float32-smallest-normal": "^0.2.2", + "@stdlib/constants-float64-eps": "^0.2.2", + "@stdlib/math-base-special-abs": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-tools-evalpoly": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-tools-evalpoly/-/math-base-tools-evalpoly-0.2.2.tgz", + "integrity": "sha512-vLvfkMkccXZGFiyI3GPf8Ayi6vPEZeHgENnoBDGC+eMIDIoVWmOpVWsjpUz8xtc5xGNsa1hKalSI40IrouHsYA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/function-ctor": "^0.2.1", + "@stdlib/utils-define-nonenumerable-read-only-property": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/math-base-tools-sum-series": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/math-base-tools-sum-series/-/math-base-tools-sum-series-0.2.2.tgz", + "integrity": "sha512-P3X+jMONClp93ucJi1Up/x26uwL0kH20CMV9bLzcQyRY8Mceh7jPZuEwzGQR0jq/tJ/4J7AnHg4kdrx4Pd+BNA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-has-generator-support": "^0.2.2", + "@stdlib/constants-float64-eps": "^0.2.2", + "@stdlib/math-base-special-abs": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/number-ctor": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/number-ctor/-/number-ctor-0.2.2.tgz", + "integrity": "sha512-98pL4f1uiXVIw9uRV6t4xecMFUYRRTUoctsqDDV8MSRtKEYDzqkWCNz/auupJFJ135L1ejzkejh73fASsgcwKQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/number-float32-base-to-word": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/number-float32-base-to-word/-/number-float32-base-to-word-0.2.2.tgz", + "integrity": "sha512-/I866ocLExPpAjgZnHAjeaBw3ZHg5tVPcRdJoTPEiBG2hwD/OonHdCsfB9lu6FxO6sbp7I9BR1JolCoEyrhmYg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/array-float32": "^0.2.2", + "@stdlib/array-uint32": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/number-float64-base-exponent": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/number-float64-base-exponent/-/number-float64-base-exponent-0.2.2.tgz", + "integrity": "sha512-mYivBQKCuu54ulorf5A5rIhFaGPjGvmtkxhvK14q7gzRA80si83dk8buUsLpeeYsakg7yLn10RCVjBEP9/gm7Q==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-exponent-bias": "^0.2.2", + "@stdlib/constants-float64-high-word-exponent-mask": "^0.2.2", + "@stdlib/number-float64-base-get-high-word": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/number-float64-base-from-words": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/number-float64-base-from-words/-/number-float64-base-from-words-0.2.2.tgz", + "integrity": "sha512-SzMDXSnIDZ8l3PDmtN9TPKTf0mUmh83kKCtj4FisKTcTbcmUmT/ovmrpMTiqdposymjHBieNvGiCz/K03NmlAA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/array-float64": "^0.2.1", + "@stdlib/array-uint32": "^0.2.2", + "@stdlib/assert-is-little-endian": "^0.2.1", + "@stdlib/number-float64-base-to-words": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/number-float64-base-get-high-word": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/number-float64-base-get-high-word/-/number-float64-base-get-high-word-0.2.2.tgz", + "integrity": "sha512-LMNQAHdLZepKOFMRXAXLuq30GInmEdTtR0rO7Ka4F3m7KpYvw84JMyvZByMQHBu+daR6JNr2a/o9aFjmVIe51g==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/array-float64": "^0.2.1", + "@stdlib/array-uint32": "^0.2.2", + "@stdlib/assert-is-little-endian": "^0.2.1", + "@stdlib/number-float64-base-to-words": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/number-float64-base-get-low-word": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/number-float64-base-get-low-word/-/number-float64-base-get-low-word-0.2.2.tgz", + "integrity": "sha512-VZjflvoQ9//rZwwuhl7uSLUnnscdIIYmBrHofnBHRjHwdLGUzSd9PM0iagtvI82OHw5QnydBYI4hohBeAAg+aQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/array-float64": "^0.2.1", + "@stdlib/array-uint32": "^0.2.2", + "@stdlib/assert-is-little-endian": "^0.2.1", + "@stdlib/number-float64-base-to-words": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/number-float64-base-normalize": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@stdlib/number-float64-base-normalize/-/number-float64-base-normalize-0.2.3.tgz", + "integrity": "sha512-HT+3fhYZOEg2JgHBWS/ysc9ZveQZV10weKbtxhLHOsvceQVp1GbThsLik62mU2/3f96S9MgiVfPfSDI3jnBoYw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/constants-float64-smallest-normal": "^0.2.2", + "@stdlib/math-base-assert-is-infinite": "^0.2.2", + "@stdlib/math-base-assert-is-nan": "^0.2.2", + "@stdlib/math-base-special-abs": "^0.2.1", + "@stdlib/utils-define-nonenumerable-read-only-property": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/number-float64-base-set-high-word": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/number-float64-base-set-high-word/-/number-float64-base-set-high-word-0.2.2.tgz", + "integrity": "sha512-bLvH15GJgX5URMaOOJAQgO8/dCJPYUQoXPZH7ecSC3XnnVMfWEf43knkjEGYCnWp4nro5hPRElbtdV4mKEjpUg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/array-float64": "^0.2.1", + "@stdlib/array-uint32": "^0.2.2", + "@stdlib/assert-is-little-endian": "^0.2.1", + "@stdlib/number-float64-base-to-words": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/number-float64-base-set-low-word": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/number-float64-base-set-low-word/-/number-float64-base-set-low-word-0.2.2.tgz", + "integrity": "sha512-E1pGjTwacJ+Tkt5rKQNdwitKnM1iDgMlulYosNdn6CtvU0Pkq359bNhscMscxehdY3MifwuJpuGzDWD2EGUXzQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/array-float64": "^0.2.1", + "@stdlib/array-uint32": "^0.2.2", + "@stdlib/assert-is-little-endian": "^0.2.1", + "@stdlib/number-float64-base-to-words": "^0.2.1", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/number-float64-base-to-float32": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/number-float64-base-to-float32/-/number-float64-base-to-float32-0.2.2.tgz", + "integrity": "sha512-T5snDkVNZY6pomrSW/qLWQfZ9JHgqCFLi8jaaarfNj2o+5NMUuvvRifLUIacTm8/uC96xB0j3+wKTh1zbIV5ig==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/array-float32": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/number-float64-base-to-words": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/number-float64-base-to-words/-/number-float64-base-to-words-0.2.2.tgz", + "integrity": "sha512-nkFHHXoMhb3vcfl7ZvzgiNdqBdBfbKxHtgvDXRxrNQoVmyYbnjljjYD489d2/TAhe+Zvn7qph6QLgTod3zaeww==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/array-float64": "^0.2.1", + "@stdlib/array-uint32": "^0.2.2", + "@stdlib/assert-is-little-endian": "^0.2.1", + "@stdlib/os-byte-order": "^0.2.1", + "@stdlib/os-float-word-order": "^0.2.2", + "@stdlib/utils-define-nonenumerable-read-only-property": "^0.2.2", + "@stdlib/utils-library-manifest": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/number-uint32-base-to-int32": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/number-uint32-base-to-int32/-/number-uint32-base-to-int32-0.2.2.tgz", + "integrity": "sha512-NPADfdHE/3VEifKDttXM24dRj5YQqxwh2wTRD8fQrpHeaWiMIUo8yDqWrrFNIdLVAcqjL2SwWpo4VJ7oKTYaIA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/object-ctor": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/object-ctor/-/object-ctor-0.2.1.tgz", + "integrity": "sha512-HEIBBpfdQS9Nh5mmIqMk9fzedx6E0tayJrVa2FD7No86rVuq/Ikxq1QP7qNXm+i6z9iNUUS/lZq7BmJESWO/Zg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/os-byte-order": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/os-byte-order/-/os-byte-order-0.2.2.tgz", + "integrity": "sha512-2y6rHAvZo43YmZu9u/E/7cnqZa0hNTLoIiMpV1IxQ/7iv03xZ45Z3xyvWMk0b7bAWwWL7iUknOAAmEchK/kHBA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-is-big-endian": "^0.2.1", + "@stdlib/assert-is-little-endian": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/os-float-word-order": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/os-float-word-order/-/os-float-word-order-0.2.2.tgz", + "integrity": "sha512-5xpcEuxv/CudKctHS5czKdM7Bj/gC+sm/5R5bRPYyqxANM67t365j3v2v8rmmOxkEp9t0fa8Dggx8VmOkpJXaA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/os-byte-order": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/process-cwd": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/process-cwd/-/process-cwd-0.2.2.tgz", + "integrity": "sha512-8Q/nA/ud5d5PEzzG6ZtKzcOw+RMLm5CWR8Wd+zVO5vcPj+JD7IV7M2lBhbzfUzr63Torrf/vEhT3cob8vUHV/A==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/regexp-extended-length-path": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/regexp-extended-length-path/-/regexp-extended-length-path-0.2.2.tgz", + "integrity": "sha512-z3jqauEsaxpsQU3rj1A1QnOgu17pyW5kt+Az8QkoTk7wqNE8HhPikI6k4o7XBHV689rSFWZCl4c4W+7JAiNObQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/utils-define-nonenumerable-read-only-property": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/regexp-function-name": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/regexp-function-name/-/regexp-function-name-0.2.2.tgz", + "integrity": "sha512-0z/KRsgHJJ3UQkmBeLH+Nin0hXIeA+Fw1T+mnG2V5CHnTA6FKlpxJxWrvwLEsRX7mR/DNtDp06zGyzMFE/4kig==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/utils-define-nonenumerable-read-only-property": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/stats-base-dists-t-quantile": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stdlib/stats-base-dists-t-quantile/-/stats-base-dists-t-quantile-0.2.1.tgz", + "integrity": "sha512-59sdJjHsOMd9JlZ/Kdz4Jc/QHESejDRATw/G/zHafMrO6vIhaus9Y5O2PYUzPQx7nR6i5hsXnT8OQwqZ+RoVNQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/math-base-assert-is-nan": "^0.2.1", + "@stdlib/math-base-special-kernel-betaincinv": "^0.1.1", + "@stdlib/math-base-special-signum": "^0.2.1", + "@stdlib/math-base-special-sqrt": "^0.2.1", + "@stdlib/utils-constant-function": "^0.2.1", + "@stdlib/utils-define-nonenumerable-read-only-property": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/string-base-format-interpolate": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/string-base-format-interpolate/-/string-base-format-interpolate-0.2.2.tgz", + "integrity": "sha512-i9nU9rAB2+o/RR66TS9iQ8x+YzeUDL1SGiAo6GY3hP6Umz5Dx9Qp/v8T69gWVsb4a1YSclz5+YeCWaFgwvPjKA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/string-base-format-tokenize": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/string-base-format-tokenize/-/string-base-format-tokenize-0.2.2.tgz", + "integrity": "sha512-kXq2015i+LJjqth5dN+hYnvJXBSzRm8w0ABWB5tYAsIuQTpQK+mSo2muM8JBEFEnqUHAwpUsu2qNTK/9o8lsJg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/string-base-lowercase": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@stdlib/string-base-lowercase/-/string-base-lowercase-0.4.0.tgz", + "integrity": "sha512-IH35Z5e4T+S3b3SfYY39mUhrD2qvJVp4VS7Rn3+jgj4+C3syocuAPsJ8C4OQXWGfblX/N9ymizbpFBCiVvMW8w==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/string-base-replace": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/string-base-replace/-/string-base-replace-0.2.2.tgz", + "integrity": "sha512-Y4jZwRV4Uertw7AlA/lwaYl1HjTefSriN5+ztRcQQyDYmoVN3gzoVKLJ123HPiggZ89vROfC+sk/6AKvly+0CA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/string-format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/string-format/-/string-format-0.2.2.tgz", + "integrity": "sha512-GUa50uxgMAtoItsxTbMmwkyhIwrCxCrsjzk3nAbLnt/1Kt1EWOWMwsALqZdD6K4V/xSJ4ns6PZur3W6w+vKk9g==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/string-base-format-interpolate": "^0.2.1", + "@stdlib/string-base-format-tokenize": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/string-replace": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/string-replace/-/string-replace-0.2.2.tgz", + "integrity": "sha512-czNS5IU7sBuHjac45Y3VWUTsUoi82yc8JsMZrOMcjgSrEuDrVmA6sNJg7HC1DuSpdPjm/v9uUk102s1gIfk3Nw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-is-function": "^0.2.2", + "@stdlib/assert-is-regexp": "^0.2.2", + "@stdlib/assert-is-string": "^0.2.2", + "@stdlib/error-tools-fmtprodmsg": "^0.2.2", + "@stdlib/string-base-replace": "^0.2.2", + "@stdlib/string-format": "^0.2.2", + "@stdlib/utils-escape-regexp-string": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/symbol-ctor": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/symbol-ctor/-/symbol-ctor-0.2.2.tgz", + "integrity": "sha512-XsmiTfHnTb9jSPf2SoK3O0wrNOXMxqzukvDvtzVur1XBKfim9+seaAS4akmV1H3+AroAXQWVtde885e1B6jz1w==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/utils-constant-function": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/utils-constant-function/-/utils-constant-function-0.2.2.tgz", + "integrity": "sha512-ezRenGy5zU4R0JTfha/bpF8U+Hx0b52AZV++ca/pcaQVvPBRkgCsJacXX0eDbexoBB4+ZZ1vcyIi4RKJ0RRlbQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/utils-constructor-name": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/utils-constructor-name/-/utils-constructor-name-0.2.2.tgz", + "integrity": "sha512-TBtO3MKDAf05ij5ajmyBCbpKKt0Lfahn5tu18gqds4PkFltgcw5tVZfSHY5DZ2HySJQ2GMMYjPW2Kbg6yPCSVg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-is-buffer": "^0.2.1", + "@stdlib/regexp-function-name": "^0.2.2", + "@stdlib/utils-native-class": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/utils-convert-path": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/utils-convert-path/-/utils-convert-path-0.2.2.tgz", + "integrity": "sha512-8nNuAgt23Np9NssjShUrPK42c6gRTweGuoQw+yTpTfBR9VQv8WFyt048n8gRGUlAHizrdMNpEY9VAb7IBzpVYw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-is-string": "^0.2.2", + "@stdlib/error-tools-fmtprodmsg": "^0.2.2", + "@stdlib/regexp-extended-length-path": "^0.2.2", + "@stdlib/string-base-lowercase": "^0.4.0", + "@stdlib/string-format": "^0.2.2", + "@stdlib/string-replace": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/utils-define-nonenumerable-read-only-property": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/utils-define-nonenumerable-read-only-property/-/utils-define-nonenumerable-read-only-property-0.2.2.tgz", + "integrity": "sha512-V3mpAesJemLYDKG376CsmoczWPE/4LKsp8xBvUxCt5CLNAx3J/1W39iZQyA5q6nY1RStGinGn1/dYZwa8ig0Uw==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/utils-define-property": "^0.2.3" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/utils-define-property": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@stdlib/utils-define-property/-/utils-define-property-0.2.4.tgz", + "integrity": "sha512-XlMdz7xwuw/sqXc9LbsV8XunCzZXjbZPC+OAdf4t4PBw4ZRwGzlTI6WED+f4PYR5Tp9F1cHgLPyMYCIBfA2zRg==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/error-tools-fmtprodmsg": "^0.2.1", + "@stdlib/string-format": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/utils-escape-regexp-string": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/utils-escape-regexp-string/-/utils-escape-regexp-string-0.2.2.tgz", + "integrity": "sha512-areCibzgpmvm6pGKBg+mXkSDJW4NxtS5jcAT7RtunGMdAYhA/I5whISMPaeJkIT2XhjjFkjKBaIs5pF6aPr4fQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-is-string": "^0.2.1", + "@stdlib/error-tools-fmtprodmsg": "^0.2.2", + "@stdlib/string-format": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/utils-eval": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/utils-eval/-/utils-eval-0.2.2.tgz", + "integrity": "sha512-MaFpWZh3fGcTjUeozju5faXqH8w4MRVfpO/M5pon3osTM0by8zrKiI5D9oWqNVygb9JBd+etE+4tj2L1nr5j2A==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/utils-get-prototype-of": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/utils-get-prototype-of/-/utils-get-prototype-of-0.2.2.tgz", + "integrity": "sha512-eDb1BAvt7GW/jduBkfuQrUsA9p09mV8RW20g0DWPaxci6ORYg/UB0tdbAA23aZz2QUoxdYY5s/UJxlq/GHwoKQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-is-function": "^0.2.1", + "@stdlib/object-ctor": "^0.2.1", + "@stdlib/utils-native-class": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/utils-global": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/utils-global/-/utils-global-0.2.2.tgz", + "integrity": "sha512-A4E8VFHn+1bpfJ4PA8H2b62CMQpjv2A+H3QDEBrouLFWne0wrx0TNq8vH6VYHxx9ZRxhgWQjfHiSAxtUJobrbQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-is-boolean": "^0.2.1", + "@stdlib/error-tools-fmtprodmsg": "^0.2.2", + "@stdlib/string-format": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/utils-library-manifest": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/utils-library-manifest/-/utils-library-manifest-0.2.2.tgz", + "integrity": "sha512-YqzVLuBsB4wTqzdUtRArAjBJoT3x61iop2jFChXexhl6ejV3vDpHcukEEkqIOcJKut+1cG5TLJdexgHNt1C0NA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/fs-resolve-parent-path": "^0.2.1", + "@stdlib/utils-convert-path": "^0.2.1", + "debug": "^2.6.9", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/utils-native-class": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/utils-native-class/-/utils-native-class-0.2.2.tgz", + "integrity": "sha512-cSn/FozbEpfR/FlJAoceQtZ8yUJFhZ8RFkbEsxW/7+H4o09yln3lBS0HSfUJISYNtpTNN/2/Fup88vmvwspvwA==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/assert-has-own-property": "^0.2.1", + "@stdlib/assert-has-tostringtag-support": "^0.2.2", + "@stdlib/symbol-ctor": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@stdlib/utils-type-of": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@stdlib/utils-type-of/-/utils-type-of-0.2.2.tgz", + "integrity": "sha512-RLGFxPNgY9AtVVrFGdKO6Y3pOd/Ov2WA4O2/czZN/AbgYzbPdoF0KkfvHRIney6k+TtvoyYB8YqZXJ4G88f9BQ==", + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], + "dependencies": { + "@stdlib/utils-constructor-name": "^0.2.1", + "@stdlib/utils-global": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stdlib" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@turf/boolean-clockwise": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-clockwise/-/boolean-clockwise-6.5.0.tgz", + "integrity": "sha512-45+C7LC5RMbRWrxh3Z0Eihsc8db1VGBO5d9BLTOAwU4jR6SgsunTfRWR16X7JUwIDYlCVEmnjcXJNi/kIU3VIw==", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/clone": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/clone/-/clone-6.5.0.tgz", + "integrity": "sha512-mzVtTFj/QycXOn6ig+annKrM6ZlimreKYz6f/GSERytOpgzodbQyOgkfwru100O1KQhhjSudKK4DsQ0oyi9cTw==", + "dependencies": { + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/flatten": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/flatten/-/flatten-6.5.0.tgz", + "integrity": "sha512-IBZVwoNLVNT6U/bcUUllubgElzpMsNoCw8tLqBw6dfYg9ObGmpEjf9BIYLr7a2Yn5ZR4l7YIj2T7kD5uJjZADQ==", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/helpers": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz", + "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==", + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/invariant": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.5.0.tgz", + "integrity": "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==", + "dependencies": { + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/meta": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-6.5.0.tgz", + "integrity": "sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA==", + "dependencies": { + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/rewind": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/rewind/-/rewind-6.5.0.tgz", + "integrity": "sha512-IoUAMcHWotBWYwSYuYypw/LlqZmO+wcBpn8ysrBNbazkFNkLf3btSDZMkKJO/bvOzl55imr/Xj4fi3DdsLsbzQ==", + "dependencies": { + "@turf/boolean-clockwise": "^6.5.0", + "@turf/clone": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@types/node": { + "version": "22.13.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.17.tgz", + "integrity": "sha512-nAJuQXoyPj04uLgu+obZcSmsfOenUg6DxPKogeUy6yNCFwWaj5sBF8/G/pNo8EtBJjAfSVgfIlugR/BCOleO+g==", + "devOptional": true, + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@visactor/calculator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@visactor/calculator/-/calculator-2.0.5.tgz", + "integrity": "sha512-/NBDB/wBQLeQuSspDBuiEAbbyfJS/xPX6mubVsLGhfy65UwUBojAQgmX25FcRJnUsRXooK5heshni19DBBf8xA==", + "dependencies": { + "@visactor/vutils": "~0.19.3", + "node-sql-parser": "~4.17.0", + "ts-pattern": "~4.1.4" + } + }, + "node_modules/@visactor/chart-advisor": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@visactor/chart-advisor/-/chart-advisor-2.0.5.tgz", + "integrity": "sha512-pvHceRlworB7kDSmbWXUtherLLXh5nMj0aEGuxtzKQyHmeO0sjuu9gGXBFIgscGliSZM4tmeNrFU9eBLGJ8dxw==", + "dependencies": { + "@visactor/vutils": "~0.19.3" + } + }, + "node_modules/@visactor/vchart": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/@visactor/vchart/-/vchart-1.13.8.tgz", + "integrity": "sha512-g8GacKxDvxUiuT4kW83u5vrAoAvpJ0+yca4IUYvdSTxdYXzipJEgiNmSnwd46GP8eBKJj36ZmqzEhLVzPJ+/Pw==", + "dependencies": { + "@visactor/vdataset": "~0.19.4", + "@visactor/vgrammar-core": "0.16.3", + "@visactor/vgrammar-hierarchy": "0.16.3", + "@visactor/vgrammar-projection": "0.16.3", + "@visactor/vgrammar-sankey": "0.16.3", + "@visactor/vgrammar-util": "0.16.3", + "@visactor/vgrammar-venn": "0.16.3", + "@visactor/vgrammar-wordcloud": "0.16.3", + "@visactor/vgrammar-wordcloud-shape": "0.16.3", + "@visactor/vrender-components": "0.22.6", + "@visactor/vrender-core": "0.22.6", + "@visactor/vrender-kits": "0.22.6", + "@visactor/vscale": "~0.19.4", + "@visactor/vutils": "~0.19.4", + "@visactor/vutils-extension": "1.13.8" + } + }, + "node_modules/@visactor/vchart-theme": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@visactor/vchart-theme/-/vchart-theme-1.12.2.tgz", + "integrity": "sha512-r298TUdK+CKbHGVYWgQnNSEB5uqpFvF2/aMNZ/2POQnd2CovAPJOx2nTE6hAcOn8rra2FwJ2xF8AyP1O5OhrTw==", + "peerDependencies": { + "@visactor/vchart": ">=1.10.4" + } + }, + "node_modules/@visactor/vdataset": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@visactor/vdataset/-/vdataset-0.19.4.tgz", + "integrity": "sha512-xxglcFtvho5jWiQPKwTolKXbNOG8f77CrK7TJhfiqNlzoe27qO8B+A6lUKlLMt1kZaCH7ZNrFFkHyPjnnZ/gng==", + "dependencies": { + "@turf/flatten": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/rewind": "^6.5.0", + "@visactor/vutils": "0.19.4", + "d3-dsv": "^2.0.0", + "d3-geo": "^1.12.1", + "d3-hexbin": "^0.2.2", + "d3-hierarchy": "^3.1.1", + "eventemitter3": "^4.0.7", + "geobuf": "^3.0.1", + "geojson-dissolve": "^3.1.0", + "path-browserify": "^1.0.1", + "pbf": "^3.2.1", + "point-at-length": "^1.1.0", + "simple-statistics": "^7.7.3", + "simplify-geojson": "^1.0.4", + "topojson-client": "^3.1.0" + } + }, + "node_modules/@visactor/vgrammar-coordinate": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@visactor/vgrammar-coordinate/-/vgrammar-coordinate-0.16.3.tgz", + "integrity": "sha512-tfDSi3WgY/GWDvbf67eus4a7jR74y7OMod3JrTqyDVzSNZUOgUtS3ieEM71f9yipxjY8gxo53GPDpH/advxUZw==", + "dependencies": { + "@visactor/vgrammar-util": "0.16.3", + "@visactor/vutils": "~0.19.4" + } + }, + "node_modules/@visactor/vgrammar-core": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@visactor/vgrammar-core/-/vgrammar-core-0.16.3.tgz", + "integrity": "sha512-cd7hmh9JobbCDUJPOshmQB5V0KVM0GLTPBe/ZySJDi1cUSWpukAgRrLozEk/M5XgDbVIT+4pjqe6siacCad8dg==", + "dependencies": { + "@visactor/vdataset": "~0.19.4", + "@visactor/vgrammar-coordinate": "0.16.3", + "@visactor/vgrammar-util": "0.16.3", + "@visactor/vrender-components": "0.22.6", + "@visactor/vrender-core": "0.22.6", + "@visactor/vrender-kits": "0.22.6", + "@visactor/vscale": "~0.19.4", + "@visactor/vutils": "~0.19.4" + } + }, + "node_modules/@visactor/vgrammar-hierarchy": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@visactor/vgrammar-hierarchy/-/vgrammar-hierarchy-0.16.3.tgz", + "integrity": "sha512-qnfSWRt1PErkVPTtet8DVc4MY+WwmgJoNNW2FALFht1qUfPdglTqT96drPbkurwiZMzSk+Xfr7+IPUA8ZQwWag==", + "dependencies": { + "@visactor/vgrammar-core": "0.16.3", + "@visactor/vgrammar-util": "0.16.3", + "@visactor/vrender-core": "0.22.6", + "@visactor/vrender-kits": "0.22.6", + "@visactor/vutils": "~0.19.4" + } + }, + "node_modules/@visactor/vgrammar-projection": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@visactor/vgrammar-projection/-/vgrammar-projection-0.16.3.tgz", + "integrity": "sha512-c+MJ3qgtsNQHwZCDBVT7fNahNxe0g827IiytQWvrtMxavLIrtJqeul5H+6BYrGvYk8d81ByxNZdoVNn/mfNtDw==", + "dependencies": { + "@visactor/vgrammar-core": "0.16.3", + "@visactor/vgrammar-util": "0.16.3", + "@visactor/vutils": "~0.19.4", + "d3-geo": "^1.12.1" + } + }, + "node_modules/@visactor/vgrammar-sankey": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@visactor/vgrammar-sankey/-/vgrammar-sankey-0.16.3.tgz", + "integrity": "sha512-7j0xx77Yn2KzY4EcZ27qFF6R1KTcmy3BtQQewOHA1uoUX8ZRsfe57eziYRiBhyVrzdFWoa0IJqzH7Yk/zITvuQ==", + "dependencies": { + "@visactor/vgrammar-core": "0.16.3", + "@visactor/vgrammar-util": "0.16.3", + "@visactor/vrender-core": "0.22.6", + "@visactor/vrender-kits": "0.22.6", + "@visactor/vutils": "~0.19.4" + } + }, + "node_modules/@visactor/vgrammar-util": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@visactor/vgrammar-util/-/vgrammar-util-0.16.3.tgz", + "integrity": "sha512-aF9MqjTR7YvBAVDtp1A/CDVcXFGlO+TxkHVPEQVrn7cVu2DGRXCZnu/iQ+AUhttVYaWlSRflZj4cnQrKS4zy4g==", + "dependencies": { + "@visactor/vrender-core": "0.22.6", + "@visactor/vutils": "~0.19.4" + } + }, + "node_modules/@visactor/vgrammar-venn": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@visactor/vgrammar-venn/-/vgrammar-venn-0.16.3.tgz", + "integrity": "sha512-M6mtCrpOcPrD6nkQFZ3Fl0Z2zPaKFTyRIPeO235vDwB/ZzefN5BObh85UGsv0swK46L5yu3daBxW0VtrGMBZRA==", + "dependencies": { + "@visactor/vgrammar-core": "0.16.3", + "@visactor/vgrammar-util": "0.16.3", + "@visactor/vrender-core": "0.22.6", + "@visactor/vrender-kits": "0.22.6", + "@visactor/vutils": "~0.19.4" + } + }, + "node_modules/@visactor/vgrammar-wordcloud": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@visactor/vgrammar-wordcloud/-/vgrammar-wordcloud-0.16.3.tgz", + "integrity": "sha512-uIHUJ3CGir+IjDjv4SpJR5SZvWSIYU2VoBdoCvFdhP9j8t15wadGYfe0/br9d6xOM3laiSCFYvPdhy0Ke5sP4w==", + "dependencies": { + "@visactor/vgrammar-core": "0.16.3", + "@visactor/vgrammar-util": "0.16.3", + "@visactor/vrender-core": "0.22.6", + "@visactor/vrender-kits": "0.22.6", + "@visactor/vutils": "~0.19.4" + } + }, + "node_modules/@visactor/vgrammar-wordcloud-shape": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@visactor/vgrammar-wordcloud-shape/-/vgrammar-wordcloud-shape-0.16.3.tgz", + "integrity": "sha512-ZWRHbec4WM2W3v2t57gRaX1IUGy+nDRjumcctgzSvmCpmR3nORgLKmMhxXYEA0VwcpY+umM0lVcd42iqPH8c7g==", + "dependencies": { + "@visactor/vgrammar-core": "0.16.3", + "@visactor/vgrammar-util": "0.16.3", + "@visactor/vrender-core": "0.22.6", + "@visactor/vrender-kits": "0.22.6", + "@visactor/vscale": "~0.19.4", + "@visactor/vutils": "~0.19.4" + } + }, + "node_modules/@visactor/vmind": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@visactor/vmind/-/vmind-2.0.5.tgz", + "integrity": "sha512-QztQaeSkdeRZYOUlB4qaBpx3/swyO3JzFH8eYvSgvptS/rf8aQDZiufAUasafDLkcME5N6RpBGkcGYIDkmt74Q==", + "dependencies": { + "@stdlib/stats-base-dists-t-quantile": "0.2.1", + "@visactor/calculator": "2.0.5", + "@visactor/chart-advisor": "2.0.5", + "@visactor/vchart-theme": "^1.11.2", + "@visactor/vdataset": "~0.19.3", + "@visactor/vutils": "~0.19.3", + "alasql": "~4.3.2", + "array-normalize": "~2.0.0", + "axios": "^1.4.0", + "bayesian-changepoint": "~1.0.1", + "dayjs": "~1.11.10", + "density-clustering": "~1.3.0", + "euclidean-distance": "~1.0.0", + "js-yaml": "~4.1.0", + "json5": "~2.2.3", + "jsonrepair": "~3.8.1", + "jstat": "~1.9.6", + "string-similarity-js": "~2.1.4" + } + }, + "node_modules/@visactor/vrender-components": { + "version": "0.22.6", + "resolved": "https://registry.npmjs.org/@visactor/vrender-components/-/vrender-components-0.22.6.tgz", + "integrity": "sha512-YHLjA2GzP5LQxAAgzo2iniBxDldy9GtEzjm/sCXrrOGzzwMFlyhAeXCbUVEIMhTTfpRdK5LocAP1PSJsv4BObA==", + "dependencies": { + "@visactor/vrender-core": "0.22.6", + "@visactor/vrender-kits": "0.22.6", + "@visactor/vscale": "~0.19.4", + "@visactor/vutils": "~0.19.4" + } + }, + "node_modules/@visactor/vrender-core": { + "version": "0.22.6", + "resolved": "https://registry.npmjs.org/@visactor/vrender-core/-/vrender-core-0.22.6.tgz", + "integrity": "sha512-R/MPjAuF9vT5atn7tAqhA5K1FMqYzv2SOhREsJpgP6QbJSnGR2uMTrNENRFvrM81ikR6yeh7WeTx6Fh2av+M4A==", + "dependencies": { + "@visactor/vutils": "~0.19.4", + "color-convert": "2.0.1" + } + }, + "node_modules/@visactor/vrender-kits": { + "version": "0.22.6", + "resolved": "https://registry.npmjs.org/@visactor/vrender-kits/-/vrender-kits-0.22.6.tgz", + "integrity": "sha512-0yRvhMhnT3JeFKCOi8riubkuKjNMIlzcW1FQV+kIyOGGV6nCSjvFL4+XDuEGalHlHt76BSlM1/cmxnmRNTHCRQ==", + "dependencies": { + "@resvg/resvg-js": "2.4.1", + "@visactor/vrender-core": "0.22.6", + "@visactor/vutils": "~0.19.4", + "gifuct-js": "2.1.2", + "lottie-web": "^5.12.2", + "roughjs": "4.5.2" + } + }, + "node_modules/@visactor/vscale": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@visactor/vscale/-/vscale-0.19.4.tgz", + "integrity": "sha512-kp69hPMof3GBKRuUiXSR9+9K+Z8ZXsTlOAwcnknXmiiZDhdcDkPlv27/d+Xx1Wi/iqw+BS2S7YIjHmfzdiVQ/Q==", + "dependencies": { + "@visactor/vutils": "0.19.4" + } + }, + "node_modules/@visactor/vutils": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@visactor/vutils/-/vutils-0.19.4.tgz", + "integrity": "sha512-kLbcsTe1/3HSSvEJvJikzGD0plY0gdHbpxt98oo7W6OrianfYd97nm/w7rFXcq/S49e6C5d1SdU4MZk/PYxhEQ==", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "eventemitter3": "^4.0.7" + } + }, + "node_modules/@visactor/vutils-extension": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/@visactor/vutils-extension/-/vutils-extension-1.13.8.tgz", + "integrity": "sha512-mOtUJjUEthQTHyYnynWJs8wbbW+UoW0z18lH++TqGoDbsJLcr4Mlpxhe8IDP/bda7kRVTI/FHbzVHhKWKLBvxw==", + "dependencies": { + "@visactor/vdataset": "~0.19.4", + "@visactor/vutils": "~0.19.4" + } + }, + "node_modules/abs-svg-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", + "integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==" + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/alasql": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/alasql/-/alasql-4.3.3.tgz", + "integrity": "sha512-IP64TOG+zBTPA41OB2NJVkM3urEIhvZtYwtPFC/1QSH7nCzwShIwWfxwyOhTK7yzF/ZaNGEpc3Eexyzb2nUbFg==", + "dependencies": { + "cross-fetch": "4", + "yargs": "16" + }, + "bin": { + "alasql": "bin/alasql-cli.js" + }, + "engines": { + "node": ">=15" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/array-bounds": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-bounds/-/array-bounds-1.0.1.tgz", + "integrity": "sha512-8wdW3ZGk6UjMPJx/glyEt0sLzzwAE1bhToPsO1W2pbpR2gULyxe3BjSiuJFheP50T/GgODVPz2fuMUmIywt8cQ==" + }, + "node_modules/array-normalize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/array-normalize/-/array-normalize-2.0.0.tgz", + "integrity": "sha512-WofPolGg9OqpmfYh2qqOJ0yeJ9Idjn+EcQ+Nyy3eQbqtuz0MRyqTEHB0PH/Ypp2PpsOAfjsqTMzu1fHOaPzO1Q==", + "dependencies": { + "array-bounds": "^1.0.0" + } + }, + "node_modules/array-source": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/array-source/-/array-source-0.0.4.tgz", + "integrity": "sha512-frNdc+zBn80vipY+GdcJkLEbMWj3xmzArYApmUGxoiV8uAu/ygcs9icPdsGdA26h0MkHUMW6EN2piIvVx+M5Mw==" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==" + }, + "node_modules/bare-events": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "optional": true + }, + "node_modules/bare-fs": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.5.tgz", + "integrity": "sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA==", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", + "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", + "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bayesian-changepoint": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bayesian-changepoint/-/bayesian-changepoint-1.0.1.tgz", + "integrity": "sha512-OhSHWfGiEcBtI46b5guJGmj6pJEjvyaXsRPCAQy5MPoVaDZ38poXmzVZLSIuw6VLQmZs58+uf5F9iFA4NVmTTA==" + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/chromium-bidi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz", + "integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-fetch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", + "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + }, + "node_modules/d3-dsv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-2.0.0.tgz", + "integrity": "sha512-E+Pn8UJYx9mViuIUkoc93gJGGYut6mSDKy2+XaPwccwkRGlR+LO97L2VCCRjQivTwLHkSnAJG7yo00BWY6QM+w==", + "dependencies": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json", + "csv2tsv": "bin/dsv2dsv", + "dsv2dsv": "bin/dsv2dsv", + "dsv2json": "bin/dsv2json", + "json2csv": "bin/json2dsv", + "json2dsv": "bin/json2dsv", + "json2tsv": "bin/json2dsv", + "tsv2csv": "bin/dsv2dsv", + "tsv2json": "bin/dsv2json" + } + }, + "node_modules/d3-geo": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", + "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", + "dependencies": { + "d3-array": "1" + } + }, + "node_modules/d3-hexbin": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/d3-hexbin/-/d3-hexbin-0.2.2.tgz", + "integrity": "sha512-KS3fUT2ReD4RlGCjvCEm1RgMtp2NFZumdMu4DBzQK8AZv3fXRM6Xm8I4fSU07UXvH4xxg03NwWKWdvxfS/yc4w==" + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/density-clustering": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/density-clustering/-/density-clustering-1.3.0.tgz", + "integrity": "sha512-icpmBubVTwLnsaor9qH/4tG5+7+f61VcqMN3V3pm9sxxSCt2Jcs0zWOgwZW9ARJYaKD3FumIgHiMOcIMRRAzFQ==" + }, + "node_modules/devtools-protocol": { + "version": "0.0.1439962", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1439962.tgz", + "integrity": "sha512-jJF48UdryzKiWhJ1bLKr7BFWUQCEIT5uCNbDLqkQJBtkFxYzILJH44WN0PDKMIlGDN7Utb8vyUY85C3w4R/t2g==" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/euclidean-distance": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/euclidean-distance/-/euclidean-distance-1.0.0.tgz", + "integrity": "sha512-3+1fOi9GKT2PhSX+uKZ/cX4F98wLY2gTibZPPZeToEPvHZNLnnoymcJgQzWeeIMvqciQRIhn9KEKY7QVplC7hQ==" + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/extract-zip/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-source": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-source/-/file-source-0.6.1.tgz", + "integrity": "sha512-1R1KneL7eTXmXfKxC10V/9NeGOdbsAXJ+lQ//fvvcHUgtaZcZDWNJNblxAoVOyV1cj45pOtUrR3vZTBwqcW8XA==", + "dependencies": { + "stream-source": "0.3" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/geobuf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/geobuf/-/geobuf-3.0.2.tgz", + "integrity": "sha512-ASgKwEAQQRnyNFHNvpd5uAwstbVYmiTW0Caw3fBb509tNTqXyAAPMyFs5NNihsLZhLxU1j/kjFhkhLWA9djuVg==", + "dependencies": { + "concat-stream": "^2.0.0", + "pbf": "^3.2.1", + "shapefile": "~0.6.6" + }, + "bin": { + "geobuf2json": "bin/geobuf2json", + "json2geobuf": "bin/json2geobuf", + "shp2geobuf": "bin/shp2geobuf" + } + }, + "node_modules/geojson-dissolve": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/geojson-dissolve/-/geojson-dissolve-3.1.0.tgz", + "integrity": "sha512-JXHfn+A3tU392HA703gJbjmuHaQOAE/C1KzbELCczFRFux+GdY6zt1nKb1VMBHp4LWeE7gUY2ql+g06vJqhiwQ==", + "dependencies": { + "@turf/meta": "^3.7.5", + "geojson-flatten": "^0.2.1", + "geojson-linestring-dissolve": "0.0.1", + "topojson-client": "^3.0.0", + "topojson-server": "^3.0.0" + } + }, + "node_modules/geojson-dissolve/node_modules/@turf/meta": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-3.14.0.tgz", + "integrity": "sha512-OtXqLQuR9hlQ/HkAF/OdzRea7E0eZK1ay8y8CBXkoO2R6v34CsDrWYLMSo0ZzMsaQDpKo76NPP2GGo+PyG1cSg==" + }, + "node_modules/geojson-flatten": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/geojson-flatten/-/geojson-flatten-0.2.4.tgz", + "integrity": "sha512-LiX6Jmot8adiIdZ/fthbcKKPOfWjTQchX/ggHnwMZ2e4b0I243N1ANUos0LvnzepTEsj0+D4fIJ5bKhBrWnAHA==", + "dependencies": { + "get-stdin": "^6.0.0", + "minimist": "1.2.0" + }, + "bin": { + "geojson-flatten": "geojson-flatten" + } + }, + "node_modules/geojson-flatten/node_modules/get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/geojson-linestring-dissolve": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/geojson-linestring-dissolve/-/geojson-linestring-dissolve-0.0.1.tgz", + "integrity": "sha512-Y8I2/Ea28R/Xeki7msBcpMvJL2TaPfaPKP8xqueJfQ9/jEhps+iOJxOR2XCBGgVb12Z6XnDb1CMbaPfLepsLaw==" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stdin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", + "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", + "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/get-uri/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/gifuct-js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/gifuct-js/-/gifuct-js-2.1.2.tgz", + "integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==", + "dependencies": { + "js-binary-schema-parser": "^2.0.3" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "node_modules/js-binary-schema-parser": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz", + "integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonrepair": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.8.1.tgz", + "integrity": "sha512-5wnjaO53EJOhfLFY92nvBz2B9gqF9ql/D4HKUb1WOSBaqtVcAifFfmurblnhCJn/ySqKFA8U3n7nhGMAu/hEjQ==", + "bin": { + "jsonrepair": "bin/cli.js" + } + }, + "node_modules/jstat": { + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/jstat/-/jstat-1.9.6.tgz", + "integrity": "sha512-rPBkJbK2TnA8pzs93QcDDPlKcrtZWuuCo2dVR0TFLOJSxhqfWOVCSp8aV3/oSbn+4uY4yw1URtLpHQedtmXfug==" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/lottie-web": { + "version": "5.12.2", + "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.12.2.tgz", + "integrity": "sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha512-7Wl+Jz+IGWuSdgsQEJ4JunV0si/iMhg42MnQQG6h1R6TNeVenp4U9x5CC5v/gYqz/fENLQITAWXidNtVL0NNbw==" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-sql-parser": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/node-sql-parser/-/node-sql-parser-4.17.0.tgz", + "integrity": "sha512-3IhovpmUBpcETnoKK/KBdkz2mz53kVG5E1dnqz1QuYvtzdxYZW5xaGGEvW9u6Yyy2ivwR3eUZrn9inmEVef02w==", + "dependencies": { + "big-integer": "^1.6.48" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-svg-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", + "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==" + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-source": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/path-source/-/path-source-0.1.3.tgz", + "integrity": "sha512-dWRHm5mIw5kw0cs3QZLNmpUWty48f5+5v9nWD2dw3Y0Hf+s01Ag8iJEWV0Sm0kocE8kK27DrIowha03e1YR+Qw==", + "dependencies": { + "array-source": "0.0", + "file-source": "0.6" + } + }, + "node_modules/pbf": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", + "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", + "dependencies": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/point-at-length": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/point-at-length/-/point-at-length-1.1.0.tgz", + "integrity": "sha512-nNHDk9rNEh/91o2Y8kHLzBLNpLf80RYd2gCun9ss+V0ytRSf6XhryBTx071fesktjbachRmGuUbId+JQmzhRXw==", + "dependencies": { + "abs-svg-path": "~0.1.1", + "isarray": "~0.0.1", + "parse-svg-path": "~0.1.1" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.9.0.tgz", + "integrity": "sha512-L0pOtALIx8rgDt24Y+COm8X52v78gNtBOW6EmUcEPci0TYD72SAuaXKqasRIx4JXxmg2Tkw5ySKcpPOwN8xXnQ==", + "hasInstallScript": true, + "dependencies": { + "@puppeteer/browsers": "2.10.5", + "chromium-bidi": "5.1.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1439962", + "puppeteer-core": "24.9.0", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.9.0.tgz", + "integrity": "sha512-HFdCeH/wx6QPz8EncafbCqJBqaCG1ENW75xg3cLFMRUoqZDgByT6HSueiumetT2uClZxwqj0qS4qMVZwLHRHHw==", + "dependencies": { + "@puppeteer/browsers": "2.10.5", + "chromium-bidi": "5.1.0", + "debug": "^4.4.1", + "devtools-protocol": "0.0.1439962", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/puppeteer-core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, + "node_modules/roughjs": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.5.2.tgz", + "integrity": "sha512-2xSlLDKdsWyFxrveYWk9YQ/Y9UfK38EAMRNkYkMqYBJvPX8abCa9PN0x3w02H8Oa6/0bcZICJU+U95VumPqseg==", + "dependencies": { + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shapefile": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/shapefile/-/shapefile-0.6.6.tgz", + "integrity": "sha512-rLGSWeK2ufzCVx05wYd+xrWnOOdSV7xNUW5/XFgx3Bc02hBkpMlrd2F1dDII7/jhWzv0MSyBFh5uJIy9hLdfuw==", + "dependencies": { + "array-source": "0.0", + "commander": "2", + "path-source": "0.1", + "slice-source": "0.4", + "stream-source": "0.3", + "text-encoding": "^0.6.4" + }, + "bin": { + "dbf2json": "bin/dbf2json", + "shp2json": "bin/shp2json" + } + }, + "node_modules/simple-statistics": { + "version": "7.8.8", + "resolved": "https://registry.npmjs.org/simple-statistics/-/simple-statistics-7.8.8.tgz", + "integrity": "sha512-CUtP0+uZbcbsFpqEyvNDYjJCl+612fNgjT8GaVuvMG7tBuJg8gXGpsP5M7X658zy0IcepWOZ6nPBu1Qb9ezA1w==", + "engines": { + "node": "*" + } + }, + "node_modules/simplify-geojson": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/simplify-geojson/-/simplify-geojson-1.0.5.tgz", + "integrity": "sha512-02l1W4UipP5ivNVq6kX15mAzCRIV1oI3tz0FUEyOsNiv1ltuFDjbNhO+nbv/xhbDEtKqWLYuzpWhUsJrjR/ypA==", + "dependencies": { + "concat-stream": "~1.4.1", + "minimist": "1.2.6", + "simplify-geometry": "0.0.2" + }, + "bin": { + "simplify-geojson": "cli.js" + } + }, + "node_modules/simplify-geojson/node_modules/concat-stream": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.4.11.tgz", + "integrity": "sha512-X3JMh8+4je3U1cQpG87+f9lXHDrqcb2MVLg9L7o8b1UZ0DzhRrUpdn65ttzu10PpJPPI3MQNkis+oha6TSA9Mw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "inherits": "~2.0.1", + "readable-stream": "~1.1.9", + "typedarray": "~0.0.5" + } + }, + "node_modules/simplify-geojson/node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "node_modules/simplify-geojson/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/simplify-geojson/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + }, + "node_modules/simplify-geometry": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/simplify-geometry/-/simplify-geometry-0.0.2.tgz", + "integrity": "sha512-ZEyrplkqgCqDlL7V8GbbYgTLlcnNF+MWWUdy8s8ZeJru50bnI71rDew/I+HG36QS2mPOYAq1ZjwNXxHJ8XOVBw==" + }, + "node_modules/slice-source": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/slice-source/-/slice-source-0.4.1.tgz", + "integrity": "sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg==" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, + "node_modules/stream-source": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/stream-source/-/stream-source-0.3.5.tgz", + "integrity": "sha512-ZuEDP9sgjiAwUVoDModftG0JtYiLUV8K4ljYD1VyUMRWtbVf92474o4kuuul43iZ8t/hRuiDAx1dIJSvirrK/g==" + }, + "node_modules/streamx": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", + "integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-similarity-js": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/string-similarity-js/-/string-similarity-js-2.1.4.tgz", + "integrity": "sha512-uApODZNjCHGYROzDSAdCmAHf60L/pMDHnP/yk6TAbvGg7JSPZlSto/ceCI7hZEqzc53/juU2aOJFkM2yUVTMTA==" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar-fs": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz", + "integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-encoding": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "integrity": "sha512-hJnc6Qg3dWoOMkqP53F0dzRIgtmsAge09kxUIqGrEUS4qr5rWLckGYaQAVr+opBrIMRErGgy6f5aPnyPpyGRfg==", + "deprecated": "no longer maintained" + }, + "node_modules/topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "dependencies": { + "commander": "2" + }, + "bin": { + "topo2geo": "bin/topo2geo", + "topomerge": "bin/topomerge", + "topoquantize": "bin/topoquantize" + } + }, + "node_modules/topojson-server": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/topojson-server/-/topojson-server-3.0.1.tgz", + "integrity": "sha512-/VS9j/ffKr2XAOjlZ9CgyyeLmgJ9dMwq6Y0YEON8O7p/tGGk+dCWnrE03zEdu7i4L7YsFZLEPZPzCvcB7lEEXw==", + "dependencies": { + "commander": "2" + }, + "bin": { + "geo2topo": "bin/geo2topo" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-pattern": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-4.1.4.tgz", + "integrity": "sha512-Mcw65oUd1w5ktKi5BRwrnz16Otwk9iv7P0dKgvbi+A1albCDgnixohSqNLuFwIp5dzxPmTPm0iDQ6p1ZJr9uGw==" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==" + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "devOptional": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/zod": { + "version": "3.25.20", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.20.tgz", + "integrity": "sha512-z03fqpTMDF1G02VLKUMt6vyACE7rNWkh3gpXVHgPTw28NPtDFRGvcpTtPwn2kMKtQ0idtYJUTxchytmnqYswcw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/app/tool/chart_visualization/package.json b/app/tool/chart_visualization/package.json new file mode 100644 index 0000000..1e9189d --- /dev/null +++ b/app/tool/chart_visualization/package.json @@ -0,0 +1,22 @@ +{ + "name": "chart_visualization", + "version": "1.0.0", + "main": "src/index.ts", + "devDependencies": { + "@types/node": "^22.10.1", + "ts-node": "^10.9.2", + "typescript": "^5.7.2" + }, + "dependencies": { + "@visactor/vchart": "^1.13.7", + "@visactor/vmind": "2.0.5", + "get-stdin": "^9.0.0", + "puppeteer": "^24.9.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "" +} diff --git a/app/tool/chart_visualization/python_execute.py b/app/tool/chart_visualization/python_execute.py new file mode 100644 index 0000000..8a7b5bb --- /dev/null +++ b/app/tool/chart_visualization/python_execute.py @@ -0,0 +1,36 @@ +from app.config import config +from app.tool.python_execute import PythonExecute + + +class NormalPythonExecute(PythonExecute): + """A tool for executing Python code with timeout and safety restrictions.""" + + name: str = "python_execute" + description: str = """Execute Python code for in-depth data analysis / data report(task conclusion) / other normal task without direct visualization.""" + parameters: dict = { + "type": "object", + "properties": { + "code_type": { + "description": "code type, data process / data report / others", + "type": "string", + "default": "process", + "enum": ["process", "report", "others"], + }, + "code": { + "type": "string", + "description": """Python code to execute. +# Note +1. The code should generate a comprehensive text-based report containing dataset overview, column details, basic statistics, derived metrics, timeseries comparisons, outliers, and key insights. +2. Use print() for all outputs so the analysis (including sections like 'Dataset Overview' or 'Preprocessing Results') is clearly visible and save it also +3. Save any report / processed files / each analysis result in worksapce directory: {directory} +4. Data reports need to be content-rich, including your overall analysis process and corresponding data visualization. +5. You can invode this tool step-by-step to do data analysis from summary to in-depth with data report saved also""".format( + directory=config.workspace_root + ), + }, + }, + "required": ["code"], + } + + async def execute(self, code: str, code_type: str | None = None, timeout=5): + return await super().execute(code, timeout) diff --git a/app/tool/chart_visualization/src/chartVisualize.ts b/app/tool/chart_visualization/src/chartVisualize.ts new file mode 100644 index 0000000..c5091a3 --- /dev/null +++ b/app/tool/chart_visualization/src/chartVisualize.ts @@ -0,0 +1,372 @@ +import path from "path"; +import fs from "fs"; +import puppeteer from "puppeteer"; +import VMind, { ChartType, DataTable } from "@visactor/vmind"; +import { isString } from "@visactor/vutils"; + +enum AlgorithmType { + OverallTrending = "overallTrend", + AbnormalTrend = "abnormalTrend", + PearsonCorrelation = "pearsonCorrelation", + SpearmanCorrelation = "spearmanCorrelation", + ExtremeValue = "extremeValue", + MajorityValue = "majorityValue", + StatisticsAbnormal = "statisticsAbnormal", + StatisticsBase = "statisticsBase", + DbscanOutlier = "dbscanOutlier", + LOFOutlier = "lofOutlier", + TurningPoint = "turningPoint", + PageHinkley = "pageHinkley", + DifferenceOutlier = "differenceOutlier", + Volatility = "volatility", +} + +const getBase64 = async (spec: any, width?: number, height?: number) => { + spec.animation = false; + width && (spec.width = width); + height && (spec.height = height); + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.setContent(getHtmlVChart(spec, width, height)); + + const dataUrl = await page.evaluate(() => { + const canvas: any = document + .getElementById("chart-container") + ?.querySelector("canvas"); + return canvas?.toDataURL("image/png"); + }); + + const base64Data = dataUrl.replace(/^data:image\/png;base64,/, ""); + await browser.close(); + return Buffer.from(base64Data, "base64"); +}; + +const serializeSpec = (spec: any) => { + return JSON.stringify(spec, (key, value) => { + if (typeof value === "function") { + const funcStr = value + .toString() + .replace(/(\r\n|\n|\r)/gm, "") + .replace(/\s+/g, " "); + + return `__FUNCTION__${funcStr}`; + } + return value; + }); +}; + +function getHtmlVChart(spec: any, width?: number, height?: number) { + return ` + + + VChart Demo + + + +
+ + + +`; +} + +/** + * get file path saved string + * @param isUpdate {boolean} default: false, update existed file when is true + */ +function getSavedPathName( + directory: string, + fileName: string, + outputType: "html" | "png" | "json" | "md", + isUpdate: boolean = false +) { + let newFileName = fileName; + while ( + !isUpdate && + fs.existsSync( + path.join(directory, "visualization", `${newFileName}.${outputType}`) + ) + ) { + newFileName += "_new"; + } + return path.join(directory, "visualization", `${newFileName}.${outputType}`); +} + +const readStdin = (): Promise => { + return new Promise((resolve) => { + let input = ""; + process.stdin.setEncoding("utf-8"); // 确保编码与 Python 端一致 + process.stdin.on("data", (chunk) => (input += chunk)); + process.stdin.on("end", () => resolve(input)); + }); +}; + +/** Save insights markdown in local, and return content && path */ +const setInsightTemplate = ( + path: string, + title: string, + insights: string[] +) => { + let res = ""; + if (insights.length) { + res += `## ${title} Insights`; + insights.forEach((insight, index) => { + res += `\n${index + 1}. ${insight}`; + }); + } + if (res) { + fs.writeFileSync(path, res, "utf-8"); + return { insight_path: path, insight_md: res }; + } + return {}; +}; + +/** Save vmind result into local file, Return chart file path */ +async function saveChartRes(options: { + spec: any; + directory: string; + outputType: "png" | "html"; + fileName: string; + width?: number; + height?: number; + isUpdate?: boolean; +}) { + const { directory, fileName, spec, outputType, width, height, isUpdate } = + options; + const specPath = getSavedPathName(directory, fileName, "json", isUpdate); + fs.writeFileSync(specPath, JSON.stringify(spec, null, 2)); + const savedPath = getSavedPathName(directory, fileName, outputType, isUpdate); + if (outputType === "png") { + const base64 = await getBase64(spec, width, height); + fs.writeFileSync(savedPath, base64); + } else { + const html = getHtmlVChart(spec, width, height); + fs.writeFileSync(savedPath, html, "utf-8"); + } + return savedPath; +} + +async function generateChart( + vmind: VMind, + options: { + dataset: string | DataTable; + userPrompt: string; + directory: string; + outputType: "png" | "html"; + fileName: string; + width?: number; + height?: number; + language?: "en" | "zh"; + } +) { + let res: { + chart_path?: string; + error?: string; + insight_path?: string; + insight_md?: string; + } = {}; + const { + dataset, + userPrompt, + directory, + width, + height, + outputType, + fileName, + language, + } = options; + try { + // Get chart spec and save in local file + const jsonDataset = isString(dataset) ? JSON.parse(dataset) : dataset; + const { spec, error, chartType } = await vmind.generateChart( + userPrompt, + undefined, + jsonDataset, + { + enableDataQuery: false, + theme: "light", + } + ); + if (error || !spec) { + return { + error: error || "Spec of Chart was Empty!", + }; + } + + spec.title = { + text: userPrompt, + }; + if (!fs.existsSync(path.join(directory, "visualization"))) { + fs.mkdirSync(path.join(directory, "visualization")); + } + const specPath = getSavedPathName(directory, fileName, "json"); + res.chart_path = await saveChartRes({ + directory, + spec, + width, + height, + fileName, + outputType, + }); + + // get chart insights and save in local + const insights = []; + if ( + chartType && + [ + ChartType.BarChart, + ChartType.LineChart, + ChartType.AreaChart, + ChartType.ScatterPlot, + ChartType.DualAxisChart, + ].includes(chartType) + ) { + const { insights: vmindInsights } = await vmind.getInsights(spec, { + maxNum: 6, + algorithms: [ + AlgorithmType.OverallTrending, + AlgorithmType.AbnormalTrend, + AlgorithmType.PearsonCorrelation, + AlgorithmType.SpearmanCorrelation, + AlgorithmType.StatisticsAbnormal, + AlgorithmType.LOFOutlier, + AlgorithmType.DbscanOutlier, + AlgorithmType.MajorityValue, + AlgorithmType.PageHinkley, + AlgorithmType.TurningPoint, + AlgorithmType.StatisticsBase, + AlgorithmType.Volatility, + ], + usePolish: false, + language: language === "en" ? "english" : "chinese", + }); + insights.push(...vmindInsights); + } + const insightsText = insights + .map((insight) => insight.textContent?.plainText) + .filter((insight) => !!insight) as string[]; + spec.insights = insights; + fs.writeFileSync(specPath, JSON.stringify(spec, null, 2)); + res = { + ...res, + ...setInsightTemplate( + getSavedPathName(directory, fileName, "md"), + userPrompt, + insightsText + ), + }; + } catch (error: any) { + res.error = error.toString(); + } finally { + return res; + } +} + +async function updateChartWithInsight( + vmind: VMind, + options: { + directory: string; + outputType: "png" | "html"; + fileName: string; + insightsId: number[]; + } +) { + const { directory, outputType, fileName, insightsId } = options; + let res: { error?: string; chart_path?: string } = {}; + try { + const specPath = getSavedPathName(directory, fileName, "json", true); + const spec = JSON.parse(fs.readFileSync(specPath, "utf8")); + // llm select index from 1 + const insights = (spec.insights || []).filter( + (_insight: any, index: number) => insightsId.includes(index + 1) + ); + const { newSpec, error } = await vmind.updateSpecByInsights(spec, insights); + if (error) { + throw error; + } + res.chart_path = await saveChartRes({ + spec: newSpec, + directory, + outputType, + fileName, + isUpdate: true, + }); + } catch (error: any) { + res.error = error.toString(); + } finally { + return res; + } +} + +async function executeVMind() { + const input = await readStdin(); + const inputData = JSON.parse(input); + let res; + const { + llm_config, + width, + dataset = [], + height, + directory, + user_prompt: userPrompt, + output_type: outputType = "png", + file_name: fileName, + task_type: taskType = "visualization", + insights_id: insightsId = [], + language = "en", + } = inputData; + const { base_url: baseUrl, model, api_key: apiKey } = llm_config; + const vmind = new VMind({ + url: `${baseUrl}/chat/completions`, + model, + headers: { + "api-key": apiKey, + Authorization: `Bearer ${apiKey}`, + }, + }); + if (taskType === "visualization") { + res = await generateChart(vmind, { + dataset, + userPrompt, + directory, + outputType, + fileName, + width, + height, + language, + }); + } else if (taskType === "insight" && insightsId.length) { + res = await updateChartWithInsight(vmind, { + directory, + fileName, + outputType, + insightsId, + }); + } + console.log(JSON.stringify(res)); +} + +executeVMind(); diff --git a/app/tool/chart_visualization/test/chart_demo.py b/app/tool/chart_visualization/test/chart_demo.py new file mode 100644 index 0000000..d89d993 --- /dev/null +++ b/app/tool/chart_visualization/test/chart_demo.py @@ -0,0 +1,191 @@ +import asyncio + +from app.agent.data_analysis import DataAnalysis +from app.logger import logger + + +prefix = "Help me generate charts and save them locally, specifically:" +tasks = [ + { + "prompt": "Help me show the sales of different products in different regions", + "data": """Product Name,Region,Sales +Coke,South,2350 +Coke,East,1027 +Coke,West,1027 +Coke,North,1027 +Sprite,South,215 +Sprite,East,654 +Sprite,West,159 +Sprite,North,28 +Fanta,South,345 +Fanta,East,654 +Fanta,West,2100 +Fanta,North,1679 +Xingmu,South,1476 +Xingmu,East,830 +Xingmu,West,532 +Xingmu,North,498 +""", + }, + { + "prompt": "Show market share of each brand", + "data": """Brand Name,Market Share,Average Price,Net Profit +Apple,0.5,7068,314531 +Samsung,0.2,6059,362345 +Vivo,0.05,3406,234512 +Nokia,0.01,1064,-1345 +Xiaomi,0.1,4087,131345""", + }, + { + "prompt": "Please help me show the sales trend of each product", + "data": """Date,Type,Value +2023-01-01,Product A,52.9 +2023-01-01,Product B,63.6 +2023-01-01,Product C,11.2 +2023-01-02,Product A,45.7 +2023-01-02,Product B,89.1 +2023-01-02,Product C,21.4 +2023-01-03,Product A,67.2 +2023-01-03,Product B,82.4 +2023-01-03,Product C,31.7 +2023-01-04,Product A,80.7 +2023-01-04,Product B,55.1 +2023-01-04,Product C,21.1 +2023-01-05,Product A,65.6 +2023-01-05,Product B,78 +2023-01-05,Product C,31.3 +2023-01-06,Product A,75.6 +2023-01-06,Product B,89.1 +2023-01-06,Product C,63.5 +2023-01-07,Product A,67.3 +2023-01-07,Product B,77.2 +2023-01-07,Product C,43.7 +2023-01-08,Product A,96.1 +2023-01-08,Product B,97.6 +2023-01-08,Product C,59.9 +2023-01-09,Product A,96.1 +2023-01-09,Product B,100.6 +2023-01-09,Product C,66.8 +2023-01-10,Product A,101.6 +2023-01-10,Product B,108.3 +2023-01-10,Product C,56.9""", + }, + { + "prompt": "Show the popularity of search keywords", + "data": """Keyword,Popularity +Hot Word,1000 +Zao Le Wo Men,800 +Rao Jian Huo,400 +My Wish is World Peace,400 +Xiu Xiu Xiu,400 +Shenzhou 11,400 +Hundred Birds Facing the Wind,400 +China Women's Volleyball Team,400 +My Guan Na,400 +Leg Dong,400 +Hot Pot Hero,400 +Baby's Heart is Bitter,400 +Olympics,400 +Awesome My Brother,400 +Poetry and Distance,400 +Song Joong-ki,400 +PPAP,400 +Blue Thin Mushroom,400 +Rain Dew Evenly,400 +Friendship's Little Boat Says It Flips,400 +Beijing Slump,400 +Dedication,200 +Apple,200 +Dog Belt,200 +Old Driver,200 +Melon-Eating Crowd,200 +Zootopia,200 +City Will Play,200 +Routine,200 +Water Reverse,200 +Why Don't You Go to Heaven,200 +Snake Spirit Man,200 +Why Don't You Go to Heaven,200 +Samsung Explosion Gate,200 +Little Li Oscar,200 +Ugly People Need to Read More,200 +Boyfriend Power,200 +A Face of Confusion,200 +Descendants of the Sun,200""", + }, + { + "prompt": "Help me compare the performance of different electric vehicle brands using a scatter plot", + "data": """Range,Charging Time,Brand Name,Average Price +2904,46,Brand1,2350 +1231,146,Brand2,1027 +5675,324,Brand3,1242 +543,57,Brand4,6754 +326,234,Brand5,215 +1124,67,Brand6,654 +3426,81,Brand7,159 +2134,24,Brand8,28 +1234,52,Brand9,345 +2345,27,Brand10,654 +526,145,Brand11,2100 +234,93,Brand12,1679 +567,94,Brand13,1476 +789,45,Brand14,830 +469,75,Brand15,532 +5689,54,Brand16,498 +""", + }, + { + "prompt": "Show conversion rates for each process", + "data": """Process,Conversion Rate,Month +Step1,100,1 +Step2,80,1 +Step3,60,1 +Step4,40,1""", + }, + { + "prompt": "Show the difference in breakfast consumption between men and women", + "data": """Day,Men-Breakfast,Women-Breakfast +Monday,15,22 +Tuesday,12,10 +Wednesday,15,20 +Thursday,10,12 +Friday,13,15 +Saturday,10,15 +Sunday,12,14""", + }, + { + "prompt": "Help me show this person's performance in different aspects, is he a hexagonal warrior", + "data": """dimension,performance +Strength,5 +Speed,5 +Shooting,3 +Endurance,5 +Precision,5 +Growth,5""", + }, + { + "prompt": "Show data flow", + "data": """Origin,Destination,value +Node A,Node 1,10 +Node A,Node 2,5 +Node B,Node 2,8 +Node B,Node 3,2 +Node C,Node 2,4 +Node A,Node C,2 +Node C,Node 1,2""", + }, +] + + +async def main(): + for index, item in enumerate(tasks): + logger.info(f"Begin task {index} / {len(tasks)}!") + agent = DataAnalysis() + await agent.run( + f"{prefix},chart_description:{item['prompt']},Data:{item['data']}" + ) + logger.info(f"Finish with {item['prompt']}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/app/tool/chart_visualization/test/report_demo.py b/app/tool/chart_visualization/test/report_demo.py new file mode 100644 index 0000000..d66f8cf --- /dev/null +++ b/app/tool/chart_visualization/test/report_demo.py @@ -0,0 +1,27 @@ +import asyncio + +from app.agent.data_analysis import DataAnalysis + + +# from app.agent.manus import Manus + + +async def main(): + agent = DataAnalysis() + # agent = Manus() + await agent.run( + """Requirement: +1. Analyze the following data and generate a graphical data report in HTML format. The final product should be a data report. +Data: +Month | Team A | Team B | Team C +January | 1200 hours | 1350 hours | 1100 hours +February | 1250 hours | 1400 hours | 1150 hours +March | 1180 hours | 1300 hours | 1300 hours +April | 1220 hours | 1280 hours | 1400 hours +May | 1230 hours | 1320 hours | 1450 hours +June | 1200 hours | 1250 hours | 1500 hours """ + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/app/tool/chart_visualization/tsconfig.json b/app/tool/chart_visualization/tsconfig.json new file mode 100644 index 0000000..7e499fa --- /dev/null +++ b/app/tool/chart_visualization/tsconfig.json @@ -0,0 +1,109 @@ +{ + "include": [ + "src/**/*.ts", + ], + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Language and Environment */ + "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + "typeRoots": [ + "./node_modules/@types", + "src/types" + ], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* JavaScript Support */ + "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + "checkJs": false, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/app/tool/computer_use_tool.py b/app/tool/computer_use_tool.py new file mode 100644 index 0000000..0ea57a7 --- /dev/null +++ b/app/tool/computer_use_tool.py @@ -0,0 +1,487 @@ +import asyncio +import base64 +import logging +import os +import time +from typing import Dict, Literal, Optional + +import aiohttp +from pydantic import Field + +from app.daytona.tool_base import Sandbox, SandboxToolsBase +from app.tool.base import ToolResult + + +KEYBOARD_KEYS = [ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "enter", + "esc", + "backspace", + "tab", + "space", + "delete", + "ctrl", + "alt", + "shift", + "win", + "up", + "down", + "left", + "right", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "f10", + "f11", + "f12", + "ctrl+c", + "ctrl+v", + "ctrl+x", + "ctrl+z", + "ctrl+a", + "ctrl+s", + "alt+tab", + "alt+f4", + "ctrl+alt+delete", +] +MOUSE_BUTTONS = ["left", "right", "middle"] +_COMPUTER_USE_DESCRIPTION = """\ +A comprehensive computer automation tool that allows interaction with the desktop environment. +* This tool provides commands for controlling mouse, keyboard, and taking screenshots +* It maintains state including current mouse position +* Use this when you need to automate desktop applications, fill forms, or perform GUI interactions +Key capabilities include: +* Mouse Control: Move, click, drag, scroll +* Keyboard Input: Type text, press keys or key combinations +* Screenshots: Capture and save screen images +* Waiting: Pause execution for specified duration +""" + + +class ComputerUseTool(SandboxToolsBase): + """Computer automation tool for controlling the desktop environment.""" + + name: str = "computer_use" + description: str = _COMPUTER_USE_DESCRIPTION + parameters: dict = { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "move_to", + "click", + "scroll", + "typing", + "press", + "wait", + "mouse_down", + "mouse_up", + "drag_to", + "hotkey", + "screenshot", + ], + "description": "The computer action to perform", + }, + "x": {"type": "number", "description": "X coordinate for mouse actions"}, + "y": {"type": "number", "description": "Y coordinate for mouse actions"}, + "button": { + "type": "string", + "enum": MOUSE_BUTTONS, + "description": "Mouse button for click/drag actions", + "default": "left", + }, + "num_clicks": { + "type": "integer", + "description": "Number of clicks", + "enum": [1, 2, 3], + "default": 1, + }, + "amount": { + "type": "integer", + "description": "Scroll amount (positive for up, negative for down)", + "minimum": -10, + "maximum": 10, + }, + "text": {"type": "string", "description": "Text to type"}, + "key": { + "type": "string", + "enum": KEYBOARD_KEYS, + "description": "Key to press", + }, + "keys": { + "type": "string", + "enum": KEYBOARD_KEYS, + "description": "Key combination to press", + }, + "duration": { + "type": "number", + "description": "Duration in seconds to wait", + "default": 0.5, + }, + }, + "required": ["action"], + "dependencies": { + "move_to": ["x", "y"], + "click": [], + "scroll": ["amount"], + "typing": ["text"], + "press": ["key"], + "wait": [], + "mouse_down": [], + "mouse_up": [], + "drag_to": ["x", "y"], + "hotkey": ["keys"], + "screenshot": [], + }, + } + session: Optional[aiohttp.ClientSession] = Field(default=None, exclude=True) + mouse_x: int = Field(default=0, exclude=True) + mouse_y: int = Field(default=0, exclude=True) + api_base_url: Optional[str] = Field(default=None, exclude=True) + + def __init__(self, sandbox: Optional[Sandbox] = None, **data): + """Initialize with optional sandbox.""" + super().__init__(**data) + if sandbox is not None: + self._sandbox = sandbox # 直接操作基类的私有属性 + self.api_base_url = sandbox.get_preview_link(8000).url + logging.info( + f"Initialized ComputerUseTool with API URL: {self.api_base_url}" + ) + + @classmethod + def create_with_sandbox(cls, sandbox: Sandbox) -> "ComputerUseTool": + """Factory method to create a tool with sandbox.""" + return cls(sandbox=sandbox) # 通过构造函数初始化 + + async def _get_session(self) -> aiohttp.ClientSession: + """Get or create aiohttp session for API requests.""" + if self.session is None or self.session.closed: + self.session = aiohttp.ClientSession() + return self.session + + async def _api_request( + self, method: str, endpoint: str, data: Optional[Dict] = None + ) -> Dict: + """Send request to automation service API.""" + try: + session = await self._get_session() + url = f"{self.api_base_url}/api{endpoint}" + logging.debug(f"API request: {method} {url} {data}") + if method.upper() == "GET": + async with session.get(url) as response: + result = await response.json() + else: # POST + async with session.post(url, json=data) as response: + result = await response.json() + logging.debug(f"API response: {result}") + return result + except Exception as e: + logging.error(f"API request failed: {str(e)}") + return {"success": False, "error": str(e)} + + async def execute( + self, + action: Literal[ + "move_to", + "click", + "scroll", + "typing", + "press", + "wait", + "mouse_down", + "mouse_up", + "drag_to", + "hotkey", + "screenshot", + ], + x: Optional[float] = None, + y: Optional[float] = None, + button: str = "left", + num_clicks: int = 1, + amount: Optional[int] = None, + text: Optional[str] = None, + key: Optional[str] = None, + keys: Optional[str] = None, + duration: float = 0.5, + **kwargs, + ) -> ToolResult: + """ + Execute a specified computer automation action. + Args: + action: The action to perform + x: X coordinate for mouse actions + y: Y coordinate for mouse actions + button: Mouse button for click/drag actions + num_clicks: Number of clicks to perform + amount: Scroll amount (positive for up, negative for down) + text: Text to type + key: Key to press + keys: Key combination to press + duration: Duration in seconds to wait + **kwargs: Additional arguments + Returns: + ToolResult with the action's output or error + """ + try: + if action == "move_to": + if x is None or y is None: + return ToolResult(error="x and y coordinates are required") + x_int = int(round(float(x))) + y_int = int(round(float(y))) + result = await self._api_request( + "POST", "/automation/mouse/move", {"x": x_int, "y": y_int} + ) + if result.get("success", False): + self.mouse_x = x_int + self.mouse_y = y_int + return ToolResult(output=f"Moved to ({x_int}, {y_int})") + else: + return ToolResult( + error=f"Failed to move: {result.get('error', 'Unknown error')}" + ) + elif action == "click": + x_val = x if x is not None else self.mouse_x + y_val = y if y is not None else self.mouse_y + x_int = int(round(float(x_val))) + y_int = int(round(float(y_val))) + num_clicks = int(num_clicks) + result = await self._api_request( + "POST", + "/automation/mouse/click", + { + "x": x_int, + "y": y_int, + "clicks": num_clicks, + "button": button.lower(), + }, + ) + if result.get("success", False): + self.mouse_x = x_int + self.mouse_y = y_int + return ToolResult( + output=f"{num_clicks} {button} click(s) performed at ({x_int}, {y_int})" + ) + else: + return ToolResult( + error=f"Failed to click: {result.get('error', 'Unknown error')}" + ) + elif action == "scroll": + if amount is None: + return ToolResult(error="Scroll amount is required") + amount = int(float(amount)) + amount = max(-10, min(10, amount)) + result = await self._api_request( + "POST", + "/automation/mouse/scroll", + {"clicks": amount, "x": self.mouse_x, "y": self.mouse_y}, + ) + if result.get("success", False): + direction = "up" if amount > 0 else "down" + steps = abs(amount) + return ToolResult( + output=f"Scrolled {direction} {steps} step(s) at position ({self.mouse_x}, {self.mouse_y})" + ) + else: + return ToolResult( + error=f"Failed to scroll: {result.get('error', 'Unknown error')}" + ) + elif action == "typing": + if text is None: + return ToolResult(error="Text is required for typing") + text = str(text) + result = await self._api_request( + "POST", + "/automation/keyboard/write", + {"message": text, "interval": 0.01}, + ) + if result.get("success", False): + return ToolResult(output=f"Typed: {text}") + else: + return ToolResult( + error=f"Failed to type: {result.get('error', 'Unknown error')}" + ) + elif action == "press": + if key is None: + return ToolResult(error="Key is required for press action") + key = str(key).lower() + result = await self._api_request( + "POST", "/automation/keyboard/press", {"keys": key, "presses": 1} + ) + if result.get("success", False): + return ToolResult(output=f"Pressed key: {key}") + else: + return ToolResult( + error=f"Failed to press key: {result.get('error', 'Unknown error')}" + ) + elif action == "wait": + duration = float(duration) + duration = max(0, min(10, duration)) + await asyncio.sleep(duration) + return ToolResult(output=f"Waited {duration} seconds") + elif action == "mouse_down": + x_val = x if x is not None else self.mouse_x + y_val = y if y is not None else self.mouse_y + x_int = int(round(float(x_val))) + y_int = int(round(float(y_val))) + result = await self._api_request( + "POST", + "/automation/mouse/down", + {"x": x_int, "y": y_int, "button": button.lower()}, + ) + if result.get("success", False): + self.mouse_x = x_int + self.mouse_y = y_int + return ToolResult( + output=f"{button} button pressed at ({x_int}, {y_int})" + ) + else: + return ToolResult( + error=f"Failed to press button: {result.get('error', 'Unknown error')}" + ) + elif action == "mouse_up": + x_val = x if x is not None else self.mouse_x + y_val = y if y is not None else self.mouse_y + x_int = int(round(float(x_val))) + y_int = int(round(float(y_val))) + result = await self._api_request( + "POST", + "/automation/mouse/up", + {"x": x_int, "y": y_int, "button": button.lower()}, + ) + if result.get("success", False): + self.mouse_x = x_int + self.mouse_y = y_int + return ToolResult( + output=f"{button} button released at ({x_int}, {y_int})" + ) + else: + return ToolResult( + error=f"Failed to release button: {result.get('error', 'Unknown error')}" + ) + elif action == "drag_to": + if x is None or y is None: + return ToolResult(error="x and y coordinates are required") + target_x = int(round(float(x))) + target_y = int(round(float(y))) + start_x = self.mouse_x + start_y = self.mouse_y + result = await self._api_request( + "POST", + "/automation/mouse/drag", + {"x": target_x, "y": target_y, "duration": 0.3, "button": "left"}, + ) + if result.get("success", False): + self.mouse_x = target_x + self.mouse_y = target_y + return ToolResult( + output=f"Dragged from ({start_x}, {start_y}) to ({target_x}, {target_y})" + ) + else: + return ToolResult( + error=f"Failed to drag: {result.get('error', 'Unknown error')}" + ) + elif action == "hotkey": + if keys is None: + return ToolResult(error="Keys are required for hotkey action") + keys = str(keys).lower().strip() + key_sequence = keys.split("+") + result = await self._api_request( + "POST", + "/automation/keyboard/hotkey", + {"keys": key_sequence, "interval": 0.01}, + ) + if result.get("success", False): + return ToolResult(output=f"Pressed key combination: {keys}") + else: + return ToolResult( + error=f"Failed to press keys: {result.get('error', 'Unknown error')}" + ) + elif action == "screenshot": + result = await self._api_request("POST", "/automation/screenshot") + if "image" in result: + base64_str = result["image"] + timestamp = time.strftime("%Y%m%d_%H%M%S") + # Save screenshot to file + screenshots_dir = "screenshots" + if not os.path.exists(screenshots_dir): + os.makedirs(screenshots_dir) + timestamped_filename = os.path.join( + screenshots_dir, f"screenshot_{timestamp}.png" + ) + latest_filename = "latest_screenshot.png" + # Decode base64 string and save to file + img_data = base64.b64decode(base64_str) + with open(timestamped_filename, "wb") as f: + f.write(img_data) + # Save a copy as the latest screenshot + with open(latest_filename, "wb") as f: + f.write(img_data) + return ToolResult( + output=f"Screenshot saved as {timestamped_filename}", + base64_image=base64_str, + ) + else: + return ToolResult(error="Failed to capture screenshot") + else: + return ToolResult(error=f"Unknown action: {action}") + except Exception as e: + return ToolResult(error=f"Computer action failed: {str(e)}") + + async def cleanup(self): + """Clean up resources.""" + if self.session and not self.session.closed: + await self.session.close() + self.session = None + + def __del__(self): + """Ensure cleanup on destruction.""" + if hasattr(self, "session") and self.session is not None: + try: + asyncio.run(self.cleanup()) + except RuntimeError: + loop = asyncio.new_event_loop() + loop.run_until_complete(self.cleanup()) + loop.close() diff --git a/app/tool/crawl4ai.py b/app/tool/crawl4ai.py new file mode 100644 index 0000000..d0f9133 --- /dev/null +++ b/app/tool/crawl4ai.py @@ -0,0 +1,269 @@ +""" +Crawl4AI Web Crawler Tool for OpenManus + +This tool integrates Crawl4AI, a high-performance web crawler designed for LLMs and AI agents, +providing fast, precise, and AI-ready data extraction with clean Markdown generation. +""" + +import asyncio +from typing import List, Union +from urllib.parse import urlparse + +from app.logger import logger +from app.tool.base import BaseTool, ToolResult + + +class Crawl4aiTool(BaseTool): + """ + Web crawler tool powered by Crawl4AI. + + Provides clean markdown extraction optimized for AI processing. + """ + + name: str = "crawl4ai" + description: str = """Web crawler that extracts clean, AI-ready content from web pages. + + Features: + - Extracts clean markdown content optimized for LLMs + - Handles JavaScript-heavy sites and dynamic content + - Supports multiple URLs in a single request + - Fast and reliable with built-in error handling + + Perfect for content analysis, research, and feeding web content to AI models.""" + + parameters: dict = { + "type": "object", + "properties": { + "urls": { + "type": "array", + "items": {"type": "string"}, + "description": "(required) List of URLs to crawl. Can be a single URL or multiple URLs.", + "minItems": 1, + }, + "timeout": { + "type": "integer", + "description": "(optional) Timeout in seconds for each URL. Default is 30.", + "default": 30, + "minimum": 5, + "maximum": 120, + }, + "bypass_cache": { + "type": "boolean", + "description": "(optional) Whether to bypass cache and fetch fresh content. Default is false.", + "default": False, + }, + "word_count_threshold": { + "type": "integer", + "description": "(optional) Minimum word count for content blocks. Default is 10.", + "default": 10, + "minimum": 1, + }, + }, + "required": ["urls"], + } + + async def execute( + self, + urls: Union[str, List[str]], + timeout: int = 30, + bypass_cache: bool = False, + word_count_threshold: int = 10, + ) -> ToolResult: + """ + Execute web crawling for the specified URLs. + + Args: + urls: Single URL string or list of URLs to crawl + timeout: Timeout in seconds for each URL + bypass_cache: Whether to bypass cache + word_count_threshold: Minimum word count for content blocks + + Returns: + ToolResult with crawl results + """ + # Normalize URLs to list + if isinstance(urls, str): + url_list = [urls] + else: + url_list = urls + + # Validate URLs + valid_urls = [] + for url in url_list: + if self._is_valid_url(url): + valid_urls.append(url) + else: + logger.warning(f"Invalid URL skipped: {url}") + + if not valid_urls: + return ToolResult(error="No valid URLs provided") + + try: + # Import crawl4ai components + from crawl4ai import ( + AsyncWebCrawler, + BrowserConfig, + CacheMode, + CrawlerRunConfig, + ) + + # Configure browser settings + browser_config = BrowserConfig( + headless=True, + verbose=False, + browser_type="chromium", + ignore_https_errors=True, + java_script_enabled=True, + ) + + # Configure crawler settings + run_config = CrawlerRunConfig( + cache_mode=CacheMode.BYPASS if bypass_cache else CacheMode.ENABLED, + word_count_threshold=word_count_threshold, + process_iframes=True, + remove_overlay_elements=True, + excluded_tags=["script", "style"], + page_timeout=timeout * 1000, # Convert to milliseconds + verbose=False, + wait_until="domcontentloaded", + ) + + results = [] + successful_count = 0 + failed_count = 0 + + # Process each URL + async with AsyncWebCrawler(config=browser_config) as crawler: + for url in valid_urls: + try: + logger.info(f"🕷️ Crawling URL: {url}") + start_time = asyncio.get_event_loop().time() + + result = await crawler.arun(url=url, config=run_config) + + end_time = asyncio.get_event_loop().time() + execution_time = end_time - start_time + + if result.success: + # Count words in markdown + word_count = 0 + if hasattr(result, "markdown") and result.markdown: + word_count = len(result.markdown.split()) + + # Count links + links_count = 0 + if hasattr(result, "links") and result.links: + internal_links = result.links.get("internal", []) + external_links = result.links.get("external", []) + links_count = len(internal_links) + len(external_links) + + # Count images + images_count = 0 + if hasattr(result, "media") and result.media: + images = result.media.get("images", []) + images_count = len(images) + + results.append( + { + "url": url, + "success": True, + "status_code": getattr(result, "status_code", 200), + "title": result.metadata.get("title") + if result.metadata + else None, + "markdown": result.markdown + if hasattr(result, "markdown") + else None, + "word_count": word_count, + "links_count": links_count, + "images_count": images_count, + "execution_time": execution_time, + } + ) + successful_count += 1 + logger.info( + f"✅ Successfully crawled {url} in {execution_time:.2f}s" + ) + + else: + results.append( + { + "url": url, + "success": False, + "error_message": getattr( + result, "error_message", "Unknown error" + ), + "execution_time": execution_time, + } + ) + failed_count += 1 + logger.warning(f"❌ Failed to crawl {url}") + + except Exception as e: + error_msg = f"Error crawling {url}: {str(e)}" + logger.error(error_msg) + results.append( + {"url": url, "success": False, "error_message": error_msg} + ) + failed_count += 1 + + # Format output + output_lines = [f"🕷️ Crawl4AI Results Summary:"] + output_lines.append(f"📊 Total URLs: {len(valid_urls)}") + output_lines.append(f"✅ Successful: {successful_count}") + output_lines.append(f"❌ Failed: {failed_count}") + output_lines.append("") + + for i, result in enumerate(results, 1): + output_lines.append(f"{i}. {result['url']}") + + if result["success"]: + output_lines.append( + f" ✅ Status: Success (HTTP {result.get('status_code', 'N/A')})" + ) + if result.get("title"): + output_lines.append(f" 📄 Title: {result['title']}") + + if result.get("markdown"): + # Show first 300 characters of markdown content + content_preview = result["markdown"] + if len(result["markdown"]) > 300: + content_preview += "..." + output_lines.append(f" 📝 Content: {content_preview}") + + output_lines.append( + f" 📊 Stats: {result.get('word_count', 0)} words, {result.get('links_count', 0)} links, {result.get('images_count', 0)} images" + ) + + if result.get("execution_time"): + output_lines.append( + f" ⏱️ Time: {result['execution_time']:.2f}s" + ) + else: + output_lines.append(f" ❌ Status: Failed") + if result.get("error_message"): + output_lines.append(f" 🚫 Error: {result['error_message']}") + + output_lines.append("") + + return ToolResult(output="\n".join(output_lines)) + + except ImportError: + error_msg = "Crawl4AI is not installed. Please install it with: pip install crawl4ai" + logger.error(error_msg) + return ToolResult(error=error_msg) + except Exception as e: + error_msg = f"Crawl4AI execution failed: {str(e)}" + logger.error(error_msg) + return ToolResult(error=error_msg) + + def _is_valid_url(self, url: str) -> bool: + """Validate if a URL is properly formatted.""" + try: + result = urlparse(url) + return all([result.scheme, result.netloc]) and result.scheme in [ + "http", + "https", + ] + except Exception: + return False diff --git a/app/tool/create_chat_completion.py b/app/tool/create_chat_completion.py new file mode 100644 index 0000000..882a5be --- /dev/null +++ b/app/tool/create_chat_completion.py @@ -0,0 +1,169 @@ +from typing import Any, List, Optional, Type, Union, get_args, get_origin + +from pydantic import BaseModel, Field + +from app.tool import BaseTool + + +class CreateChatCompletion(BaseTool): + name: str = "create_chat_completion" + description: str = ( + "Creates a structured completion with specified output formatting." + ) + + # Type mapping for JSON schema + type_mapping: dict = { + str: "string", + int: "integer", + float: "number", + bool: "boolean", + dict: "object", + list: "array", + } + response_type: Optional[Type] = None + required: List[str] = Field(default_factory=lambda: ["response"]) + + def __init__(self, response_type: Optional[Type] = str): + """Initialize with a specific response type.""" + super().__init__() + self.response_type = response_type + self.parameters = self._build_parameters() + + def _build_parameters(self) -> dict: + """Build parameters schema based on response type.""" + if self.response_type == str: + return { + "type": "object", + "properties": { + "response": { + "type": "string", + "description": "The response text that should be delivered to the user.", + }, + }, + "required": self.required, + } + + if isinstance(self.response_type, type) and issubclass( + self.response_type, BaseModel + ): + schema = self.response_type.model_json_schema() + return { + "type": "object", + "properties": schema["properties"], + "required": schema.get("required", self.required), + } + + return self._create_type_schema(self.response_type) + + def _create_type_schema(self, type_hint: Type) -> dict: + """Create a JSON schema for the given type.""" + origin = get_origin(type_hint) + args = get_args(type_hint) + + # Handle primitive types + if origin is None: + return { + "type": "object", + "properties": { + "response": { + "type": self.type_mapping.get(type_hint, "string"), + "description": f"Response of type {type_hint.__name__}", + } + }, + "required": self.required, + } + + # Handle List type + if origin is list: + item_type = args[0] if args else Any + return { + "type": "object", + "properties": { + "response": { + "type": "array", + "items": self._get_type_info(item_type), + } + }, + "required": self.required, + } + + # Handle Dict type + if origin is dict: + value_type = args[1] if len(args) > 1 else Any + return { + "type": "object", + "properties": { + "response": { + "type": "object", + "additionalProperties": self._get_type_info(value_type), + } + }, + "required": self.required, + } + + # Handle Union type + if origin is Union: + return self._create_union_schema(args) + + return self._build_parameters() + + def _get_type_info(self, type_hint: Type) -> dict: + """Get type information for a single type.""" + if isinstance(type_hint, type) and issubclass(type_hint, BaseModel): + return type_hint.model_json_schema() + + return { + "type": self.type_mapping.get(type_hint, "string"), + "description": f"Value of type {getattr(type_hint, '__name__', 'any')}", + } + + def _create_union_schema(self, types: tuple) -> dict: + """Create schema for Union types.""" + return { + "type": "object", + "properties": { + "response": {"anyOf": [self._get_type_info(t) for t in types]} + }, + "required": self.required, + } + + async def execute(self, required: list | None = None, **kwargs) -> Any: + """Execute the chat completion with type conversion. + + Args: + required: List of required field names or None + **kwargs: Response data + + Returns: + Converted response based on response_type + """ + required = required or self.required + + # Handle case when required is a list + if isinstance(required, list) and len(required) > 0: + if len(required) == 1: + required_field = required[0] + result = kwargs.get(required_field, "") + else: + # Return multiple fields as a dictionary + return {field: kwargs.get(field, "") for field in required} + else: + required_field = "response" + result = kwargs.get(required_field, "") + + # Type conversion logic + if self.response_type == str: + return result + + if isinstance(self.response_type, type) and issubclass( + self.response_type, BaseModel + ): + return self.response_type(**kwargs) + + if get_origin(self.response_type) in (list, dict): + return result # Assuming result is already in correct format + + try: + return self.response_type(result) + except (ValueError, TypeError): + return result diff --git a/app/tool/file_operators.py b/app/tool/file_operators.py new file mode 100644 index 0000000..dd64c83 --- /dev/null +++ b/app/tool/file_operators.py @@ -0,0 +1,158 @@ +"""File operation interfaces and implementations for local and sandbox environments.""" + +import asyncio +from pathlib import Path +from typing import Optional, Protocol, Tuple, Union, runtime_checkable + +from app.config import SandboxSettings +from app.exceptions import ToolError +from app.sandbox.client import SANDBOX_CLIENT + + +PathLike = Union[str, Path] + + +@runtime_checkable +class FileOperator(Protocol): + """Interface for file operations in different environments.""" + + async def read_file(self, path: PathLike) -> str: + """Read content from a file.""" + ... + + async def write_file(self, path: PathLike, content: str) -> None: + """Write content to a file.""" + ... + + async def is_directory(self, path: PathLike) -> bool: + """Check if path points to a directory.""" + ... + + async def exists(self, path: PathLike) -> bool: + """Check if path exists.""" + ... + + async def run_command( + self, cmd: str, timeout: Optional[float] = 120.0 + ) -> Tuple[int, str, str]: + """Run a shell command and return (return_code, stdout, stderr).""" + ... + + +class LocalFileOperator(FileOperator): + """File operations implementation for local filesystem.""" + + encoding: str = "utf-8" + + async def read_file(self, path: PathLike) -> str: + """Read content from a local file.""" + try: + return Path(path).read_text(encoding=self.encoding) + except Exception as e: + raise ToolError(f"Failed to read {path}: {str(e)}") from None + + async def write_file(self, path: PathLike, content: str) -> None: + """Write content to a local file.""" + try: + Path(path).write_text(content, encoding=self.encoding) + except Exception as e: + raise ToolError(f"Failed to write to {path}: {str(e)}") from None + + async def is_directory(self, path: PathLike) -> bool: + """Check if path points to a directory.""" + return Path(path).is_dir() + + async def exists(self, path: PathLike) -> bool: + """Check if path exists.""" + return Path(path).exists() + + async def run_command( + self, cmd: str, timeout: Optional[float] = 120.0 + ) -> Tuple[int, str, str]: + """Run a shell command locally.""" + process = await asyncio.create_subprocess_shell( + cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + + try: + stdout, stderr = await asyncio.wait_for( + process.communicate(), timeout=timeout + ) + return ( + process.returncode or 0, + stdout.decode(), + stderr.decode(), + ) + except asyncio.TimeoutError as exc: + try: + process.kill() + except ProcessLookupError: + pass + raise TimeoutError( + f"Command '{cmd}' timed out after {timeout} seconds" + ) from exc + + +class SandboxFileOperator(FileOperator): + """File operations implementation for sandbox environment.""" + + def __init__(self): + self.sandbox_client = SANDBOX_CLIENT + + async def _ensure_sandbox_initialized(self): + """Ensure sandbox is initialized.""" + if not self.sandbox_client.sandbox: + await self.sandbox_client.create(config=SandboxSettings()) + + async def read_file(self, path: PathLike) -> str: + """Read content from a file in sandbox.""" + await self._ensure_sandbox_initialized() + try: + return await self.sandbox_client.read_file(str(path)) + except Exception as e: + raise ToolError(f"Failed to read {path} in sandbox: {str(e)}") from None + + async def write_file(self, path: PathLike, content: str) -> None: + """Write content to a file in sandbox.""" + await self._ensure_sandbox_initialized() + try: + await self.sandbox_client.write_file(str(path), content) + except Exception as e: + raise ToolError(f"Failed to write to {path} in sandbox: {str(e)}") from None + + async def is_directory(self, path: PathLike) -> bool: + """Check if path points to a directory in sandbox.""" + await self._ensure_sandbox_initialized() + result = await self.sandbox_client.run_command( + f"test -d {path} && echo 'true' || echo 'false'" + ) + return result.strip() == "true" + + async def exists(self, path: PathLike) -> bool: + """Check if path exists in sandbox.""" + await self._ensure_sandbox_initialized() + result = await self.sandbox_client.run_command( + f"test -e {path} && echo 'true' || echo 'false'" + ) + return result.strip() == "true" + + async def run_command( + self, cmd: str, timeout: Optional[float] = 120.0 + ) -> Tuple[int, str, str]: + """Run a command in sandbox environment.""" + await self._ensure_sandbox_initialized() + try: + stdout = await self.sandbox_client.run_command( + cmd, timeout=int(timeout) if timeout else None + ) + return ( + 0, # Always return 0 since we don't have explicit return code from sandbox + stdout, + "", # No stderr capture in the current sandbox implementation + ) + except TimeoutError as exc: + raise TimeoutError( + f"Command '{cmd}' timed out after {timeout} seconds in sandbox" + ) from exc + except Exception as exc: + return 1, "", f"Error executing command in sandbox: {str(exc)}" diff --git a/app/tool/mcp.py b/app/tool/mcp.py new file mode 100644 index 0000000..32fa824 --- /dev/null +++ b/app/tool/mcp.py @@ -0,0 +1,194 @@ +from contextlib import AsyncExitStack +from typing import Dict, List, Optional + +from mcp import ClientSession, StdioServerParameters +from mcp.client.sse import sse_client +from mcp.client.stdio import stdio_client +from mcp.types import ListToolsResult, TextContent + +from app.logger import logger +from app.tool.base import BaseTool, ToolResult +from app.tool.tool_collection import ToolCollection + + +class MCPClientTool(BaseTool): + """Represents a tool proxy that can be called on the MCP server from the client side.""" + + session: Optional[ClientSession] = None + server_id: str = "" # Add server identifier + original_name: str = "" + + async def execute(self, **kwargs) -> ToolResult: + """Execute the tool by making a remote call to the MCP server.""" + if not self.session: + return ToolResult(error="Not connected to MCP server") + + try: + logger.info(f"Executing tool: {self.original_name}") + result = await self.session.call_tool(self.original_name, kwargs) + content_str = ", ".join( + item.text for item in result.content if isinstance(item, TextContent) + ) + return ToolResult(output=content_str or "No output returned.") + except Exception as e: + return ToolResult(error=f"Error executing tool: {str(e)}") + + +class MCPClients(ToolCollection): + """ + A collection of tools that connects to multiple MCP servers and manages available tools through the Model Context Protocol. + """ + + sessions: Dict[str, ClientSession] = {} + exit_stacks: Dict[str, AsyncExitStack] = {} + description: str = "MCP client tools for server interaction" + + def __init__(self): + super().__init__() # Initialize with empty tools list + self.name = "mcp" # Keep name for backward compatibility + + async def connect_sse(self, server_url: str, server_id: str = "") -> None: + """Connect to an MCP server using SSE transport.""" + if not server_url: + raise ValueError("Server URL is required.") + + server_id = server_id or server_url + + # Always ensure clean disconnection before new connection + if server_id in self.sessions: + await self.disconnect(server_id) + + exit_stack = AsyncExitStack() + self.exit_stacks[server_id] = exit_stack + + streams_context = sse_client(url=server_url) + streams = await exit_stack.enter_async_context(streams_context) + session = await exit_stack.enter_async_context(ClientSession(*streams)) + self.sessions[server_id] = session + + await self._initialize_and_list_tools(server_id) + + async def connect_stdio( + self, command: str, args: List[str], server_id: str = "" + ) -> None: + """Connect to an MCP server using stdio transport.""" + if not command: + raise ValueError("Server command is required.") + + server_id = server_id or command + + # Always ensure clean disconnection before new connection + if server_id in self.sessions: + await self.disconnect(server_id) + + exit_stack = AsyncExitStack() + self.exit_stacks[server_id] = exit_stack + + server_params = StdioServerParameters(command=command, args=args) + stdio_transport = await exit_stack.enter_async_context( + stdio_client(server_params) + ) + read, write = stdio_transport + session = await exit_stack.enter_async_context(ClientSession(read, write)) + self.sessions[server_id] = session + + await self._initialize_and_list_tools(server_id) + + async def _initialize_and_list_tools(self, server_id: str) -> None: + """Initialize session and populate tool map.""" + session = self.sessions.get(server_id) + if not session: + raise RuntimeError(f"Session not initialized for server {server_id}") + + await session.initialize() + response = await session.list_tools() + + # Create proper tool objects for each server tool + for tool in response.tools: + original_name = tool.name + tool_name = f"mcp_{server_id}_{original_name}" + tool_name = self._sanitize_tool_name(tool_name) + + server_tool = MCPClientTool( + name=tool_name, + description=tool.description, + parameters=tool.inputSchema, + session=session, + server_id=server_id, + original_name=original_name, + ) + self.tool_map[tool_name] = server_tool + + # Update tools tuple + self.tools = tuple(self.tool_map.values()) + logger.info( + f"Connected to server {server_id} with tools: {[tool.name for tool in response.tools]}" + ) + + def _sanitize_tool_name(self, name: str) -> str: + """Sanitize tool name to match MCPClientTool requirements.""" + import re + + # Replace invalid characters with underscores + sanitized = re.sub(r"[^a-zA-Z0-9_-]", "_", name) + + # Remove consecutive underscores + sanitized = re.sub(r"_+", "_", sanitized) + + # Remove leading/trailing underscores + sanitized = sanitized.strip("_") + + # Truncate to 64 characters if needed + if len(sanitized) > 64: + sanitized = sanitized[:64] + + return sanitized + + async def list_tools(self) -> ListToolsResult: + """List all available tools.""" + tools_result = ListToolsResult(tools=[]) + for session in self.sessions.values(): + response = await session.list_tools() + tools_result.tools += response.tools + return tools_result + + async def disconnect(self, server_id: str = "") -> None: + """Disconnect from a specific MCP server or all servers if no server_id provided.""" + if server_id: + if server_id in self.sessions: + try: + exit_stack = self.exit_stacks.get(server_id) + + # Close the exit stack which will handle session cleanup + if exit_stack: + try: + await exit_stack.aclose() + except RuntimeError as e: + if "cancel scope" in str(e).lower(): + logger.warning( + f"Cancel scope error during disconnect from {server_id}, continuing with cleanup: {e}" + ) + else: + raise + + # Clean up references + self.sessions.pop(server_id, None) + self.exit_stacks.pop(server_id, None) + + # Remove tools associated with this server + self.tool_map = { + k: v + for k, v in self.tool_map.items() + if v.server_id != server_id + } + self.tools = tuple(self.tool_map.values()) + logger.info(f"Disconnected from MCP server {server_id}") + except Exception as e: + logger.error(f"Error disconnecting from server {server_id}: {e}") + else: + # Disconnect from all servers in a deterministic order + for sid in sorted(list(self.sessions.keys())): + await self.disconnect(sid) + self.tool_map = {} + self.tools = tuple() + logger.info("Disconnected from all MCP servers") diff --git a/app/tool/planning.py b/app/tool/planning.py new file mode 100644 index 0000000..47e334d --- /dev/null +++ b/app/tool/planning.py @@ -0,0 +1,363 @@ +# tool/planning.py +from typing import Dict, List, Literal, Optional + +from app.exceptions import ToolError +from app.tool.base import BaseTool, ToolResult + + +_PLANNING_TOOL_DESCRIPTION = """ +A planning tool that allows the agent to create and manage plans for solving complex tasks. +The tool provides functionality for creating plans, updating plan steps, and tracking progress. +""" + + +class PlanningTool(BaseTool): + """ + A planning tool that allows the agent to create and manage plans for solving complex tasks. + The tool provides functionality for creating plans, updating plan steps, and tracking progress. + """ + + name: str = "planning" + description: str = _PLANNING_TOOL_DESCRIPTION + parameters: dict = { + "type": "object", + "properties": { + "command": { + "description": "The command to execute. Available commands: create, update, list, get, set_active, mark_step, delete.", + "enum": [ + "create", + "update", + "list", + "get", + "set_active", + "mark_step", + "delete", + ], + "type": "string", + }, + "plan_id": { + "description": "Unique identifier for the plan. Required for create, update, set_active, and delete commands. Optional for get and mark_step (uses active plan if not specified).", + "type": "string", + }, + "title": { + "description": "Title for the plan. Required for create command, optional for update command.", + "type": "string", + }, + "steps": { + "description": "List of plan steps. Required for create command, optional for update command.", + "type": "array", + "items": {"type": "string"}, + }, + "step_index": { + "description": "Index of the step to update (0-based). Required for mark_step command.", + "type": "integer", + }, + "step_status": { + "description": "Status to set for a step. Used with mark_step command.", + "enum": ["not_started", "in_progress", "completed", "blocked"], + "type": "string", + }, + "step_notes": { + "description": "Additional notes for a step. Optional for mark_step command.", + "type": "string", + }, + }, + "required": ["command"], + "additionalProperties": False, + } + + plans: dict = {} # Dictionary to store plans by plan_id + _current_plan_id: Optional[str] = None # Track the current active plan + + async def execute( + self, + *, + command: Literal[ + "create", "update", "list", "get", "set_active", "mark_step", "delete" + ], + plan_id: Optional[str] = None, + title: Optional[str] = None, + steps: Optional[List[str]] = None, + step_index: Optional[int] = None, + step_status: Optional[ + Literal["not_started", "in_progress", "completed", "blocked"] + ] = None, + step_notes: Optional[str] = None, + **kwargs, + ): + """ + Execute the planning tool with the given command and parameters. + + Parameters: + - command: The operation to perform + - plan_id: Unique identifier for the plan + - title: Title for the plan (used with create command) + - steps: List of steps for the plan (used with create command) + - step_index: Index of the step to update (used with mark_step command) + - step_status: Status to set for a step (used with mark_step command) + - step_notes: Additional notes for a step (used with mark_step command) + """ + + if command == "create": + return self._create_plan(plan_id, title, steps) + elif command == "update": + return self._update_plan(plan_id, title, steps) + elif command == "list": + return self._list_plans() + elif command == "get": + return self._get_plan(plan_id) + elif command == "set_active": + return self._set_active_plan(plan_id) + elif command == "mark_step": + return self._mark_step(plan_id, step_index, step_status, step_notes) + elif command == "delete": + return self._delete_plan(plan_id) + else: + raise ToolError( + f"Unrecognized command: {command}. Allowed commands are: create, update, list, get, set_active, mark_step, delete" + ) + + def _create_plan( + self, plan_id: Optional[str], title: Optional[str], steps: Optional[List[str]] + ) -> ToolResult: + """Create a new plan with the given ID, title, and steps.""" + if not plan_id: + raise ToolError("Parameter `plan_id` is required for command: create") + + if plan_id in self.plans: + raise ToolError( + f"A plan with ID '{plan_id}' already exists. Use 'update' to modify existing plans." + ) + + if not title: + raise ToolError("Parameter `title` is required for command: create") + + if ( + not steps + or not isinstance(steps, list) + or not all(isinstance(step, str) for step in steps) + ): + raise ToolError( + "Parameter `steps` must be a non-empty list of strings for command: create" + ) + + # Create a new plan with initialized step statuses + plan = { + "plan_id": plan_id, + "title": title, + "steps": steps, + "step_statuses": ["not_started"] * len(steps), + "step_notes": [""] * len(steps), + } + + self.plans[plan_id] = plan + self._current_plan_id = plan_id # Set as active plan + + return ToolResult( + output=f"Plan created successfully with ID: {plan_id}\n\n{self._format_plan(plan)}" + ) + + def _update_plan( + self, plan_id: Optional[str], title: Optional[str], steps: Optional[List[str]] + ) -> ToolResult: + """Update an existing plan with new title or steps.""" + if not plan_id: + raise ToolError("Parameter `plan_id` is required for command: update") + + if plan_id not in self.plans: + raise ToolError(f"No plan found with ID: {plan_id}") + + plan = self.plans[plan_id] + + if title: + plan["title"] = title + + if steps: + if not isinstance(steps, list) or not all( + isinstance(step, str) for step in steps + ): + raise ToolError( + "Parameter `steps` must be a list of strings for command: update" + ) + + # Preserve existing step statuses for unchanged steps + old_steps = plan["steps"] + old_statuses = plan["step_statuses"] + old_notes = plan["step_notes"] + + # Create new step statuses and notes + new_statuses = [] + new_notes = [] + + for i, step in enumerate(steps): + # If the step exists at the same position in old steps, preserve status and notes + if i < len(old_steps) and step == old_steps[i]: + new_statuses.append(old_statuses[i]) + new_notes.append(old_notes[i]) + else: + new_statuses.append("not_started") + new_notes.append("") + + plan["steps"] = steps + plan["step_statuses"] = new_statuses + plan["step_notes"] = new_notes + + return ToolResult( + output=f"Plan updated successfully: {plan_id}\n\n{self._format_plan(plan)}" + ) + + def _list_plans(self) -> ToolResult: + """List all available plans.""" + if not self.plans: + return ToolResult( + output="No plans available. Create a plan with the 'create' command." + ) + + output = "Available plans:\n" + for plan_id, plan in self.plans.items(): + current_marker = " (active)" if plan_id == self._current_plan_id else "" + completed = sum( + 1 for status in plan["step_statuses"] if status == "completed" + ) + total = len(plan["steps"]) + progress = f"{completed}/{total} steps completed" + output += f"• {plan_id}{current_marker}: {plan['title']} - {progress}\n" + + return ToolResult(output=output) + + def _get_plan(self, plan_id: Optional[str]) -> ToolResult: + """Get details of a specific plan.""" + if not plan_id: + # If no plan_id is provided, use the current active plan + if not self._current_plan_id: + raise ToolError( + "No active plan. Please specify a plan_id or set an active plan." + ) + plan_id = self._current_plan_id + + if plan_id not in self.plans: + raise ToolError(f"No plan found with ID: {plan_id}") + + plan = self.plans[plan_id] + return ToolResult(output=self._format_plan(plan)) + + def _set_active_plan(self, plan_id: Optional[str]) -> ToolResult: + """Set a plan as the active plan.""" + if not plan_id: + raise ToolError("Parameter `plan_id` is required for command: set_active") + + if plan_id not in self.plans: + raise ToolError(f"No plan found with ID: {plan_id}") + + self._current_plan_id = plan_id + return ToolResult( + output=f"Plan '{plan_id}' is now the active plan.\n\n{self._format_plan(self.plans[plan_id])}" + ) + + def _mark_step( + self, + plan_id: Optional[str], + step_index: Optional[int], + step_status: Optional[str], + step_notes: Optional[str], + ) -> ToolResult: + """Mark a step with a specific status and optional notes.""" + if not plan_id: + # If no plan_id is provided, use the current active plan + if not self._current_plan_id: + raise ToolError( + "No active plan. Please specify a plan_id or set an active plan." + ) + plan_id = self._current_plan_id + + if plan_id not in self.plans: + raise ToolError(f"No plan found with ID: {plan_id}") + + if step_index is None: + raise ToolError("Parameter `step_index` is required for command: mark_step") + + plan = self.plans[plan_id] + + if step_index < 0 or step_index >= len(plan["steps"]): + raise ToolError( + f"Invalid step_index: {step_index}. Valid indices range from 0 to {len(plan['steps'])-1}." + ) + + if step_status and step_status not in [ + "not_started", + "in_progress", + "completed", + "blocked", + ]: + raise ToolError( + f"Invalid step_status: {step_status}. Valid statuses are: not_started, in_progress, completed, blocked" + ) + + if step_status: + plan["step_statuses"][step_index] = step_status + + if step_notes: + plan["step_notes"][step_index] = step_notes + + return ToolResult( + output=f"Step {step_index} updated in plan '{plan_id}'.\n\n{self._format_plan(plan)}" + ) + + def _delete_plan(self, plan_id: Optional[str]) -> ToolResult: + """Delete a plan.""" + if not plan_id: + raise ToolError("Parameter `plan_id` is required for command: delete") + + if plan_id not in self.plans: + raise ToolError(f"No plan found with ID: {plan_id}") + + del self.plans[plan_id] + + # If the deleted plan was the active plan, clear the active plan + if self._current_plan_id == plan_id: + self._current_plan_id = None + + return ToolResult(output=f"Plan '{plan_id}' has been deleted.") + + def _format_plan(self, plan: Dict) -> str: + """Format a plan for display.""" + output = f"Plan: {plan['title']} (ID: {plan['plan_id']})\n" + output += "=" * len(output) + "\n\n" + + # Calculate progress statistics + total_steps = len(plan["steps"]) + completed = sum(1 for status in plan["step_statuses"] if status == "completed") + in_progress = sum( + 1 for status in plan["step_statuses"] if status == "in_progress" + ) + blocked = sum(1 for status in plan["step_statuses"] if status == "blocked") + not_started = sum( + 1 for status in plan["step_statuses"] if status == "not_started" + ) + + output += f"Progress: {completed}/{total_steps} steps completed " + if total_steps > 0: + percentage = (completed / total_steps) * 100 + output += f"({percentage:.1f}%)\n" + else: + output += "(0%)\n" + + output += f"Status: {completed} completed, {in_progress} in progress, {blocked} blocked, {not_started} not started\n\n" + output += "Steps:\n" + + # Add each step with its status and notes + for i, (step, status, notes) in enumerate( + zip(plan["steps"], plan["step_statuses"], plan["step_notes"]) + ): + status_symbol = { + "not_started": "[ ]", + "in_progress": "[→]", + "completed": "[✓]", + "blocked": "[!]", + }.get(status, "[ ]") + + output += f"{i}. {status_symbol} {step}\n" + if notes: + output += f" Notes: {notes}\n" + + return output diff --git a/app/tool/python_execute.py b/app/tool/python_execute.py new file mode 100644 index 0000000..08ceffa --- /dev/null +++ b/app/tool/python_execute.py @@ -0,0 +1,75 @@ +import multiprocessing +import sys +from io import StringIO +from typing import Dict + +from app.tool.base import BaseTool + + +class PythonExecute(BaseTool): + """A tool for executing Python code with timeout and safety restrictions.""" + + name: str = "python_execute" + description: str = "Executes Python code string. Note: Only print outputs are visible, function return values are not captured. Use print statements to see results." + parameters: dict = { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "The Python code to execute.", + }, + }, + "required": ["code"], + } + + def _run_code(self, code: str, result_dict: dict, safe_globals: dict) -> None: + original_stdout = sys.stdout + try: + output_buffer = StringIO() + sys.stdout = output_buffer + exec(code, safe_globals, safe_globals) + result_dict["observation"] = output_buffer.getvalue() + result_dict["success"] = True + except Exception as e: + result_dict["observation"] = str(e) + result_dict["success"] = False + finally: + sys.stdout = original_stdout + + async def execute( + self, + code: str, + timeout: int = 5, + ) -> Dict: + """ + Executes the provided Python code with a timeout. + + Args: + code (str): The Python code to execute. + timeout (int): Execution timeout in seconds. + + Returns: + Dict: Contains 'output' with execution output or error message and 'success' status. + """ + + with multiprocessing.Manager() as manager: + result = manager.dict({"observation": "", "success": False}) + if isinstance(__builtins__, dict): + safe_globals = {"__builtins__": __builtins__} + else: + safe_globals = {"__builtins__": __builtins__.__dict__.copy()} + proc = multiprocessing.Process( + target=self._run_code, args=(code, result, safe_globals) + ) + proc.start() + proc.join(timeout) + + # timeout process + if proc.is_alive(): + proc.terminate() + proc.join(1) + return { + "observation": f"Execution timeout after {timeout} seconds", + "success": False, + } + return dict(result) diff --git a/app/tool/sandbox/sb_browser_tool.py b/app/tool/sandbox/sb_browser_tool.py new file mode 100644 index 0000000..b3a862e --- /dev/null +++ b/app/tool/sandbox/sb_browser_tool.py @@ -0,0 +1,450 @@ +import base64 +import io +import json +import traceback +from typing import Optional # Add this import for Optional + +from PIL import Image +from pydantic import Field + +from app.daytona.tool_base import ( # Ensure Sandbox is imported correctly + Sandbox, + SandboxToolsBase, + ThreadMessage, +) +from app.tool.base import ToolResult +from app.utils.logger import logger + + +# Context = TypeVar("Context") +_BROWSER_DESCRIPTION = """\ +A sandbox-based browser automation tool that allows interaction with web pages through various actions. +* This tool provides commands for controlling a browser session in a sandboxed environment +* It maintains state across calls, keeping the browser session alive until explicitly closed +* Use this when you need to browse websites, fill forms, click buttons, or extract content in a secure sandbox +* Each action requires specific parameters as defined in the tool's dependencies +Key capabilities include: +* Navigation: Go to specific URLs, go back in history +* Interaction: Click elements by index, input text, send keyboard commands +* Scrolling: Scroll up/down by pixel amount or scroll to specific text +* Tab management: Switch between tabs or close tabs +* Content extraction: Get dropdown options or select dropdown options +""" + + +# noinspection PyArgumentList +class SandboxBrowserTool(SandboxToolsBase): + """Tool for executing tasks in a Daytona sandbox with browser-use capabilities.""" + + name: str = "sandbox_browser" + description: str = _BROWSER_DESCRIPTION + parameters: dict = { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "navigate_to", + "go_back", + "wait", + "click_element", + "input_text", + "send_keys", + "switch_tab", + "close_tab", + "scroll_down", + "scroll_up", + "scroll_to_text", + "get_dropdown_options", + "select_dropdown_option", + "click_coordinates", + "drag_drop", + ], + "description": "The browser action to perform", + }, + "url": { + "type": "string", + "description": "URL for 'navigate_to' action", + }, + "index": { + "type": "integer", + "description": "Element index for interaction actions", + }, + "text": { + "type": "string", + "description": "Text for input or scroll actions", + }, + "amount": { + "type": "integer", + "description": "Pixel amount to scroll", + }, + "page_id": { + "type": "integer", + "description": "Tab ID for tab management actions", + }, + "keys": { + "type": "string", + "description": "Keys to send for keyboard actions", + }, + "seconds": { + "type": "integer", + "description": "Seconds to wait", + }, + "x": { + "type": "integer", + "description": "X coordinate for click or drag actions", + }, + "y": { + "type": "integer", + "description": "Y coordinate for click or drag actions", + }, + "element_source": { + "type": "string", + "description": "Source element for drag and drop", + }, + "element_target": { + "type": "string", + "description": "Target element for drag and drop", + }, + }, + "required": ["action"], + "dependencies": { + "navigate_to": ["url"], + "click_element": ["index"], + "input_text": ["index", "text"], + "send_keys": ["keys"], + "switch_tab": ["page_id"], + "close_tab": ["page_id"], + "scroll_down": ["amount"], + "scroll_up": ["amount"], + "scroll_to_text": ["text"], + "get_dropdown_options": ["index"], + "select_dropdown_option": ["index", "text"], + "click_coordinates": ["x", "y"], + "drag_drop": ["element_source", "element_target"], + "wait": ["seconds"], + }, + } + browser_message: Optional[ThreadMessage] = Field(default=None, exclude=True) + + def __init__( + self, sandbox: Optional[Sandbox] = None, thread_id: Optional[str] = None, **data + ): + """Initialize with optional sandbox and thread_id.""" + super().__init__(**data) + if sandbox is not None: + self._sandbox = sandbox # Directly set the base class private attribute + + def _validate_base64_image( + self, base64_string: str, max_size_mb: int = 10 + ) -> tuple[bool, str]: + """ + Validate base64 image data. + Args: + base64_string: The base64 encoded image data + max_size_mb: Maximum allowed image size in megabytes + Returns: + Tuple of (is_valid, error_message) + """ + try: + if not base64_string or len(base64_string) < 10: + return False, "Base64 string is empty or too short" + if base64_string.startswith("data:"): + try: + base64_string = base64_string.split(",", 1)[1] + except (IndexError, ValueError): + return False, "Invalid data URL format" + import re + + if not re.match(r"^[A-Za-z0-9+/]*={0,2}$", base64_string): + return False, "Invalid base64 characters detected" + if len(base64_string) % 4 != 0: + return False, "Invalid base64 string length" + try: + image_data = base64.b64decode(base64_string, validate=True) + except Exception as e: + return False, f"Base64 decoding failed: {str(e)}" + max_size_bytes = max_size_mb * 1024 * 1024 + if len(image_data) > max_size_bytes: + return False, f"Image size exceeds limit ({max_size_bytes} bytes)" + try: + image_stream = io.BytesIO(image_data) + with Image.open(image_stream) as img: + img.verify() + supported_formats = {"JPEG", "PNG", "GIF", "BMP", "WEBP", "TIFF"} + if img.format not in supported_formats: + return False, f"Unsupported image format: {img.format}" + image_stream.seek(0) + with Image.open(image_stream) as img_check: + width, height = img_check.size + max_dimension = 8192 + if width > max_dimension or height > max_dimension: + return ( + False, + f"Image dimensions exceed limit ({max_dimension}x{max_dimension})", + ) + if width < 1 or height < 1: + return False, f"Invalid image dimensions: {width}x{height}" + except Exception as e: + return False, f"Invalid image data: {str(e)}" + return True, "Valid image" + except Exception as e: + logger.error(f"Unexpected error during base64 image validation: {e}") + return False, f"Validation error: {str(e)}" + + async def _execute_browser_action( + self, endpoint: str, params: dict = None, method: str = "POST" + ) -> ToolResult: + """Execute a browser automation action through the sandbox API.""" + try: + await self._ensure_sandbox() + url = f"http://localhost:8003/api/automation/{endpoint}" + if method == "GET" and params: + query_params = "&".join([f"{k}={v}" for k, v in params.items()]) + url = f"{url}?{query_params}" + curl_cmd = ( + f"curl -s -X {method} '{url}' -H 'Content-Type: application/json'" + ) + else: + curl_cmd = ( + f"curl -s -X {method} '{url}' -H 'Content-Type: application/json'" + ) + if params: + json_data = json.dumps(params) + curl_cmd += f" -d '{json_data}'" + logger.debug(f"Executing curl command: {curl_cmd}") + response = self.sandbox.process.exec(curl_cmd, timeout=30) + if response.exit_code == 0: + try: + result = json.loads(response.result) + result.setdefault("content", "") + result.setdefault("role", "assistant") + if "screenshot_base64" in result: + screenshot_data = result["screenshot_base64"] + is_valid, validation_message = self._validate_base64_image( + screenshot_data + ) + if not is_valid: + logger.warning( + f"Screenshot validation failed: {validation_message}" + ) + result["image_validation_error"] = validation_message + del result["screenshot_base64"] + + # added_message = await self.thread_manager.add_message( + # thread_id=self.thread_id, + # type="browser_state", + # content=result, + # is_llm_message=False + # ) + message = ThreadMessage( + type="browser_state", content=result, is_llm_message=False + ) + self.browser_message = message + success_response = { + "success": result.get("success", False), + "message": result.get("message", "Browser action completed"), + } + # if added_message and 'message_id' in added_message: + # success_response['message_id'] = added_message['message_id'] + for field in [ + "url", + "title", + "element_count", + "pixels_below", + "ocr_text", + "image_url", + ]: + if field in result: + success_response[field] = result[field] + return ( + self.success_response(success_response) + if success_response["success"] + else self.fail_response(success_response) + ) + except json.JSONDecodeError as e: + logger.error(f"Failed to parse response JSON: {e}") + return self.fail_response(f"Failed to parse response JSON: {e}") + else: + logger.error(f"Browser automation request failed: {response}") + return self.fail_response( + f"Browser automation request failed: {response}" + ) + except Exception as e: + logger.error(f"Error executing browser action: {e}") + logger.debug(traceback.format_exc()) + return self.fail_response(f"Error executing browser action: {e}") + + async def execute( + self, + action: str, + url: Optional[str] = None, + index: Optional[int] = None, + text: Optional[str] = None, + amount: Optional[int] = None, + page_id: Optional[int] = None, + keys: Optional[str] = None, + seconds: Optional[int] = None, + x: Optional[int] = None, + y: Optional[int] = None, + element_source: Optional[str] = None, + element_target: Optional[str] = None, + **kwargs, + ) -> ToolResult: + """ + Execute a browser action in the sandbox environment. + Args: + action: The browser action to perform + url: URL for navigation + index: Element index for interaction + text: Text for input or scroll actions + amount: Pixel amount to scroll + page_id: Tab ID for tab management + keys: Keys to send for keyboard actions + seconds: Seconds to wait + x: X coordinate for click/drag + y: Y coordinate for click/drag + element_source: Source element for drag and drop + element_target: Target element for drag and drop + Returns: + ToolResult with the action's output or error + """ + # async with self.lock: + try: + # Navigation actions + if action == "navigate_to": + if not url: + return self.fail_response("URL is required for navigation") + return await self._execute_browser_action("navigate_to", {"url": url}) + elif action == "go_back": + return await self._execute_browser_action("go_back", {}) + # Interaction actions + elif action == "click_element": + if index is None: + return self.fail_response("Index is required for click_element") + return await self._execute_browser_action( + "click_element", {"index": index} + ) + elif action == "input_text": + if index is None or not text: + return self.fail_response( + "Index and text are required for input_text" + ) + return await self._execute_browser_action( + "input_text", {"index": index, "text": text} + ) + elif action == "send_keys": + if not keys: + return self.fail_response("Keys are required for send_keys") + return await self._execute_browser_action("send_keys", {"keys": keys}) + # Tab management + elif action == "switch_tab": + if page_id is None: + return self.fail_response("Page ID is required for switch_tab") + return await self._execute_browser_action( + "switch_tab", {"page_id": page_id} + ) + elif action == "close_tab": + if page_id is None: + return self.fail_response("Page ID is required for close_tab") + return await self._execute_browser_action( + "close_tab", {"page_id": page_id} + ) + # Scrolling actions + elif action == "scroll_down": + params = {"amount": amount} if amount is not None else {} + return await self._execute_browser_action("scroll_down", params) + elif action == "scroll_up": + params = {"amount": amount} if amount is not None else {} + return await self._execute_browser_action("scroll_up", params) + elif action == "scroll_to_text": + if not text: + return self.fail_response("Text is required for scroll_to_text") + return await self._execute_browser_action( + "scroll_to_text", {"text": text} + ) + # Dropdown actions + elif action == "get_dropdown_options": + if index is None: + return self.fail_response( + "Index is required for get_dropdown_options" + ) + return await self._execute_browser_action( + "get_dropdown_options", {"index": index} + ) + elif action == "select_dropdown_option": + if index is None or not text: + return self.fail_response( + "Index and text are required for select_dropdown_option" + ) + return await self._execute_browser_action( + "select_dropdown_option", {"index": index, "text": text} + ) + # Coordinate-based actions + elif action == "click_coordinates": + if x is None or y is None: + return self.fail_response( + "X and Y coordinates are required for click_coordinates" + ) + return await self._execute_browser_action( + "click_coordinates", {"x": x, "y": y} + ) + elif action == "drag_drop": + if not element_source or not element_target: + return self.fail_response( + "Source and target elements are required for drag_drop" + ) + return await self._execute_browser_action( + "drag_drop", + { + "element_source": element_source, + "element_target": element_target, + }, + ) + # Utility actions + elif action == "wait": + seconds_to_wait = seconds if seconds is not None else 3 + return await self._execute_browser_action( + "wait", {"seconds": seconds_to_wait} + ) + else: + return self.fail_response(f"Unknown action: {action}") + except Exception as e: + logger.error(f"Error executing browser action: {e}") + return self.fail_response(f"Error executing browser action: {e}") + + async def get_current_state( + self, message: Optional[ThreadMessage] = None + ) -> ToolResult: + """ + Get the current browser state as a ToolResult. + If context is not provided, uses self.context. + """ + try: + # Use provided context or fall back to self.context + message = message or self.browser_message + if not message: + return ToolResult(error="Browser context not initialized") + state = message.content + screenshot = state.get("screenshot_base64") + # Build the state info with all required fields + state_info = { + "url": state.get("url", ""), + "title": state.get("title", ""), + "tabs": [tab.model_dump() for tab in state.get("tabs", [])], + "pixels_above": getattr(state, "pixels_above", 0), + "pixels_below": getattr(state, "pixels_below", 0), + "help": "[0], [1], [2], etc., represent clickable indices corresponding to the elements listed. Clicking on these indices will navigate to or interact with the respective content behind them.", + } + + return ToolResult( + output=json.dumps(state_info, indent=4, ensure_ascii=False), + base64_image=screenshot, + ) + except Exception as e: + return ToolResult(error=f"Failed to get browser state: {str(e)}") + + @classmethod + def create_with_sandbox(cls, sandbox: Sandbox) -> "SandboxBrowserTool": + """Factory method to create a tool with sandbox.""" + return cls(sandbox=sandbox) diff --git a/app/tool/sandbox/sb_files_tool.py b/app/tool/sandbox/sb_files_tool.py new file mode 100644 index 0000000..be558b0 --- /dev/null +++ b/app/tool/sandbox/sb_files_tool.py @@ -0,0 +1,361 @@ +import asyncio +from typing import Optional, TypeVar + +from pydantic import Field + +from app.daytona.tool_base import Sandbox, SandboxToolsBase +from app.tool.base import ToolResult +from app.utils.files_utils import clean_path, should_exclude_file +from app.utils.logger import logger + + +Context = TypeVar("Context") + +_FILES_DESCRIPTION = """\ +A sandbox-based file system tool that allows file operations in a secure sandboxed environment. +* This tool provides commands for creating, reading, updating, and deleting files in the workspace +* All operations are performed relative to the /workspace directory for security +* Use this when you need to manage files, edit code, or manipulate file contents in a sandbox +* Each action requires specific parameters as defined in the tool's dependencies +Key capabilities include: +* File creation: Create new files with specified content and permissions +* File modification: Replace specific strings or completely rewrite files +* File deletion: Remove files from the workspace +* File reading: Read file contents with optional line range specification +""" + + +class SandboxFilesTool(SandboxToolsBase): + name: str = "sandbox_files" + description: str = _FILES_DESCRIPTION + parameters: dict = { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "create_file", + "str_replace", + "full_file_rewrite", + "delete_file", + ], + "description": "The file operation to perform", + }, + "file_path": { + "type": "string", + "description": "Path to the file, relative to /workspace (e.g., 'src/main.py')", + }, + "file_contents": { + "type": "string", + "description": "Content to write to the file", + }, + "old_str": { + "type": "string", + "description": "Text to be replaced (must appear exactly once)", + }, + "new_str": { + "type": "string", + "description": "Replacement text", + }, + "permissions": { + "type": "string", + "description": "File permissions in octal format (e.g., '644')", + "default": "644", + }, + }, + "required": ["action"], + "dependencies": { + "create_file": ["file_path", "file_contents"], + "str_replace": ["file_path", "old_str", "new_str"], + "full_file_rewrite": ["file_path", "file_contents"], + "delete_file": ["file_path"], + }, + } + SNIPPET_LINES: int = Field(default=4, exclude=True) + # workspace_path: str = Field(default="/workspace", exclude=True) + # sandbox: Optional[Sandbox] = Field(default=None, exclude=True) + + def __init__( + self, sandbox: Optional[Sandbox] = None, thread_id: Optional[str] = None, **data + ): + """Initialize with optional sandbox and thread_id.""" + super().__init__(**data) + if sandbox is not None: + self._sandbox = sandbox + + def clean_path(self, path: str) -> str: + """Clean and normalize a path to be relative to /workspace""" + return clean_path(path, self.workspace_path) + + def _should_exclude_file(self, rel_path: str) -> bool: + """Check if a file should be excluded based on path, name, or extension""" + return should_exclude_file(rel_path) + + def _file_exists(self, path: str) -> bool: + """Check if a file exists in the sandbox""" + try: + self.sandbox.fs.get_file_info(path) + return True + except Exception: + return False + + async def get_workspace_state(self) -> dict: + """Get the current workspace state by reading all files""" + files_state = {} + try: + # Ensure sandbox is initialized + await self._ensure_sandbox() + + files = self.sandbox.fs.list_files(self.workspace_path) + for file_info in files: + rel_path = file_info.name + + # Skip excluded files and directories + if self._should_exclude_file(rel_path) or file_info.is_dir: + continue + + try: + full_path = f"{self.workspace_path}/{rel_path}" + content = self.sandbox.fs.download_file(full_path).decode() + files_state[rel_path] = { + "content": content, + "is_dir": file_info.is_dir, + "size": file_info.size, + "modified": file_info.mod_time, + } + except Exception as e: + print(f"Error reading file {rel_path}: {e}") + except UnicodeDecodeError: + print(f"Skipping binary file: {rel_path}") + + return files_state + + except Exception as e: + print(f"Error getting workspace state: {str(e)}") + return {} + + async def execute( + self, + action: str, + file_path: Optional[str] = None, + file_contents: Optional[str] = None, + old_str: Optional[str] = None, + new_str: Optional[str] = None, + permissions: Optional[str] = "644", + **kwargs, + ) -> ToolResult: + """ + Execute a file operation in the sandbox environment. + Args: + action: The file operation to perform + file_path: Path to the file relative to /workspace + file_contents: Content to write to the file + old_str: Text to be replaced (for str_replace) + new_str: Replacement text (for str_replace) + permissions: File permissions in octal format + Returns: + ToolResult with the operation's output or error + """ + async with asyncio.Lock(): + try: + # File creation + if action == "create_file": + if not file_path or not file_contents: + return self.fail_response( + "file_path and file_contents are required for create_file" + ) + return await self._create_file( + file_path, file_contents, permissions + ) + + # String replacement + elif action == "str_replace": + if not file_path or not old_str or not new_str: + return self.fail_response( + "file_path, old_str, and new_str are required for str_replace" + ) + return await self._str_replace(file_path, old_str, new_str) + + # Full file rewrite + elif action == "full_file_rewrite": + if not file_path or not file_contents: + return self.fail_response( + "file_path and file_contents are required for full_file_rewrite" + ) + return await self._full_file_rewrite( + file_path, file_contents, permissions + ) + + # File deletion + elif action == "delete_file": + if not file_path: + return self.fail_response( + "file_path is required for delete_file" + ) + return await self._delete_file(file_path) + + else: + return self.fail_response(f"Unknown action: {action}") + + except Exception as e: + logger.error(f"Error executing file action: {e}") + return self.fail_response(f"Error executing file action: {e}") + + async def _create_file( + self, file_path: str, file_contents: str, permissions: str = "644" + ) -> ToolResult: + """Create a new file with the provided contents""" + try: + # Ensure sandbox is initialized + await self._ensure_sandbox() + + file_path = self.clean_path(file_path) + full_path = f"{self.workspace_path}/{file_path}" + if self._file_exists(full_path): + return self.fail_response( + f"File '{file_path}' already exists. Use full_file_rewrite to modify existing files." + ) + + # Create parent directories if needed + parent_dir = "/".join(full_path.split("/")[:-1]) + if parent_dir: + self.sandbox.fs.create_folder(parent_dir, "755") + + # Write the file content + self.sandbox.fs.upload_file(file_contents.encode(), full_path) + self.sandbox.fs.set_file_permissions(full_path, permissions) + + message = f"File '{file_path}' created successfully." + + # Check if index.html was created and add 8080 server info (only in root workspace) + if file_path.lower() == "index.html": + try: + website_link = self.sandbox.get_preview_link(8080) + website_url = ( + website_link.url + if hasattr(website_link, "url") + else str(website_link).split("url='")[1].split("'")[0] + ) + message += f"\n\n[Auto-detected index.html - HTTP server available at: {website_url}]" + message += "\n[Note: Use the provided HTTP server URL above instead of starting a new server]" + except Exception as e: + logger.warning( + f"Failed to get website URL for index.html: {str(e)}" + ) + + return self.success_response(message) + except Exception as e: + return self.fail_response(f"Error creating file: {str(e)}") + + async def _str_replace( + self, file_path: str, old_str: str, new_str: str + ) -> ToolResult: + """Replace specific text in a file""" + try: + # Ensure sandbox is initialized + await self._ensure_sandbox() + + file_path = self.clean_path(file_path) + full_path = f"{self.workspace_path}/{file_path}" + if not self._file_exists(full_path): + return self.fail_response(f"File '{file_path}' does not exist") + + content = self.sandbox.fs.download_file(full_path).decode() + old_str = old_str.expandtabs() + new_str = new_str.expandtabs() + + occurrences = content.count(old_str) + if occurrences == 0: + return self.fail_response(f"String '{old_str}' not found in file") + if occurrences > 1: + lines = [ + i + 1 + for i, line in enumerate(content.split("\n")) + if old_str in line + ] + return self.fail_response( + f"Multiple occurrences found in lines {lines}. Please ensure string is unique" + ) + + # Perform replacement + new_content = content.replace(old_str, new_str) + self.sandbox.fs.upload_file(new_content.encode(), full_path) + + # Show snippet around the edit + replacement_line = content.split(old_str)[0].count("\n") + start_line = max(0, replacement_line - self.SNIPPET_LINES) + end_line = replacement_line + self.SNIPPET_LINES + new_str.count("\n") + snippet = "\n".join(new_content.split("\n")[start_line : end_line + 1]) + + message = f"Replacement successful." + + return self.success_response(message) + + except Exception as e: + return self.fail_response(f"Error replacing string: {str(e)}") + + async def _full_file_rewrite( + self, file_path: str, file_contents: str, permissions: str = "644" + ) -> ToolResult: + """Completely rewrite an existing file with new content""" + try: + # Ensure sandbox is initialized + await self._ensure_sandbox() + + file_path = self.clean_path(file_path) + full_path = f"{self.workspace_path}/{file_path}" + if not self._file_exists(full_path): + return self.fail_response( + f"File '{file_path}' does not exist. Use create_file to create a new file." + ) + + self.sandbox.fs.upload_file(file_contents.encode(), full_path) + self.sandbox.fs.set_file_permissions(full_path, permissions) + + message = f"File '{file_path}' completely rewritten successfully." + + # Check if index.html was rewritten and add 8080 server info (only in root workspace) + if file_path.lower() == "index.html": + try: + website_link = self.sandbox.get_preview_link(8080) + website_url = ( + website_link.url + if hasattr(website_link, "url") + else str(website_link).split("url='")[1].split("'")[0] + ) + message += f"\n\n[Auto-detected index.html - HTTP server available at: {website_url}]" + message += "\n[Note: Use the provided HTTP server URL above instead of starting a new server]" + except Exception as e: + logger.warning( + f"Failed to get website URL for index.html: {str(e)}" + ) + + return self.success_response(message) + except Exception as e: + return self.fail_response(f"Error rewriting file: {str(e)}") + + async def _delete_file(self, file_path: str) -> ToolResult: + """Delete a file at the given path""" + try: + # Ensure sandbox is initialized + await self._ensure_sandbox() + + file_path = self.clean_path(file_path) + full_path = f"{self.workspace_path}/{file_path}" + if not self._file_exists(full_path): + return self.fail_response(f"File '{file_path}' does not exist") + + self.sandbox.fs.delete_file(full_path) + return self.success_response(f"File '{file_path}' deleted successfully.") + except Exception as e: + return self.fail_response(f"Error deleting file: {str(e)}") + + async def cleanup(self): + """Clean up sandbox resources.""" + + @classmethod + def create_with_context(cls, context: Context) -> "SandboxFilesTool[Context]": + """Factory method to create a SandboxFilesTool with a specific context.""" + raise NotImplementedError( + "create_with_context not implemented for SandboxFilesTool" + ) diff --git a/app/tool/sandbox/sb_shell_tool.py b/app/tool/sandbox/sb_shell_tool.py new file mode 100644 index 0000000..8a45244 --- /dev/null +++ b/app/tool/sandbox/sb_shell_tool.py @@ -0,0 +1,419 @@ +import asyncio +import time +from typing import Any, Dict, Optional, TypeVar +from uuid import uuid4 + +from app.daytona.tool_base import Sandbox, SandboxToolsBase +from app.tool.base import ToolResult +from app.utils.logger import logger + + +Context = TypeVar("Context") +_SHELL_DESCRIPTION = """\ +Execute a shell command in the workspace directory. +IMPORTANT: Commands are non-blocking by default and run in a tmux session. +This is ideal for long-running operations like starting servers or build processes. +Uses sessions to maintain state between commands. +This tool is essential for running CLI tools, installing packages, and managing system operations. +""" + + +class SandboxShellTool(SandboxToolsBase): + """Tool for executing tasks in a Daytona sandbox with browser-use capabilities. + Uses sessions for maintaining state between commands and provides comprehensive process management. + """ + + name: str = "sandbox_shell" + description: str = _SHELL_DESCRIPTION + parameters: dict = { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "execute_command", + "check_command_output", + "terminate_command", + "list_commands", + ], + "description": "The shell action to perform", + }, + "command": { + "type": "string", + "description": "The shell command to execute. Use this for running CLI tools, installing packages, " + "or system operations. Commands can be chained using &&, ||, and | operators.", + }, + "folder": { + "type": "string", + "description": "Optional relative path to a subdirectory of /workspace where the command should be " + "executed. Example: 'data/pdfs'", + }, + "session_name": { + "type": "string", + "description": "Optional name of the tmux session to use. Use named sessions for related commands " + "that need to maintain state. Defaults to a random session name.", + }, + "blocking": { + "type": "boolean", + "description": "Whether to wait for the command to complete. Defaults to false for non-blocking " + "execution.", + "default": False, + }, + "timeout": { + "type": "integer", + "description": "Optional timeout in seconds for blocking commands. Defaults to 60. Ignored for " + "non-blocking commands.", + "default": 60, + }, + "kill_session": { + "type": "boolean", + "description": "Whether to terminate the tmux session after checking. Set to true when you're done " + "with the command.", + "default": False, + }, + }, + "required": ["action"], + "dependencies": { + "execute_command": ["command"], + "check_command_output": ["session_name"], + "terminate_command": ["session_name"], + "list_commands": [], + }, + } + + def __init__( + self, sandbox: Optional[Sandbox] = None, thread_id: Optional[str] = None, **data + ): + """Initialize with optional sandbox and thread_id.""" + super().__init__(**data) + if sandbox is not None: + self._sandbox = sandbox + + async def _ensure_session(self, session_name: str = "default") -> str: + """Ensure a session exists and return its ID.""" + if session_name not in self._sessions: + session_id = str(uuid4()) + try: + await self._ensure_sandbox() # Ensure sandbox is initialized + self.sandbox.process.create_session(session_id) + self._sessions[session_name] = session_id + except Exception as e: + raise RuntimeError(f"Failed to create session: {str(e)}") + return self._sessions[session_name] + + async def _cleanup_session(self, session_name: str): + """Clean up a session if it exists.""" + if session_name in self._sessions: + try: + await self._ensure_sandbox() # Ensure sandbox is initialized + self.sandbox.process.delete_session(self._sessions[session_name]) + del self._sessions[session_name] + except Exception as e: + print(f"Warning: Failed to cleanup session {session_name}: {str(e)}") + + async def _execute_raw_command(self, command: str) -> Dict[str, Any]: + """Execute a raw command directly in the sandbox.""" + # Ensure session exists for raw commands + session_id = await self._ensure_session("raw_commands") + + # Execute command in session + from app.daytona.sandbox import SessionExecuteRequest + + req = SessionExecuteRequest( + command=command, run_async=False, cwd=self.workspace_path + ) + + response = self.sandbox.process.execute_session_command( + session_id=session_id, + req=req, + timeout=30, # Short timeout for utility commands + ) + + logs = self.sandbox.process.get_session_command_logs( + session_id=session_id, command_id=response.cmd_id + ) + + return {"output": logs, "exit_code": response.exit_code} + + async def _execute_command( + self, + command: str, + folder: Optional[str] = None, + session_name: Optional[str] = None, + blocking: bool = False, + timeout: int = 60, + ) -> ToolResult: + try: + # Ensure sandbox is initialized + await self._ensure_sandbox() + + # Set up working directory + cwd = self.workspace_path + if folder: + folder = folder.strip("/") + cwd = f"{self.workspace_path}/{folder}" + + # Generate a session name if not provided + if not session_name: + session_name = f"session_{str(uuid4())[:8]}" + + # Check if tmux session already exists + check_session = await self._execute_raw_command( + f"tmux has-session -t {session_name} 2>/dev/null || echo 'not_exists'" + ) + session_exists = "not_exists" not in check_session.get("output", "") + + if not session_exists: + # Create a new tmux session + await self._execute_raw_command( + f"tmux new-session -d -s {session_name}" + ) + + # Ensure we're in the correct directory and send command to tmux + full_command = f"cd {cwd} && {command}" + wrapped_command = full_command.replace('"', '\\"') # Escape double quotes + + # Send command to tmux session + await self._execute_raw_command( + f'tmux send-keys -t {session_name} "{wrapped_command}" Enter' + ) + + if blocking: + # For blocking execution, wait and capture output + start_time = time.time() + while (time.time() - start_time) < timeout: + # Wait a bit before checking + time.sleep(2) + + # Check if session still exists (command might have exited) + check_result = await self._execute_raw_command( + f"tmux has-session -t {session_name} 2>/dev/null || echo 'ended'" + ) + if "ended" in check_result.get("output", ""): + break + + # Get current output and check for common completion indicators + output_result = await self._execute_raw_command( + f"tmux capture-pane -t {session_name} -p -S - -E -" + ) + current_output = output_result.get("output", "") + + # Check for prompt indicators that suggest command completion + last_lines = current_output.split("\n")[-3:] + completion_indicators = [ + "$", + "#", + ">", + "Done", + "Completed", + "Finished", + "✓", + ] + if any( + indicator in line + for indicator in completion_indicators + for line in last_lines + ): + break + + # Capture final output + output_result = await self._execute_raw_command( + f"tmux capture-pane -t {session_name} -p -S - -E -" + ) + final_output = output_result.get("output", "") + + # Kill the session after capture + await self._execute_raw_command(f"tmux kill-session -t {session_name}") + + return self.success_response( + { + "output": final_output, + "session_name": session_name, + "cwd": cwd, + "completed": True, + } + ) + else: + # For non-blocking, just return immediately + return self.success_response( + { + "session_name": session_name, + "cwd": cwd, + "message": f"Command sent to tmux session '{session_name}'. Use check_command_output to view results.", + "completed": False, + } + ) + + except Exception as e: + # Attempt to clean up session in case of error + if session_name: + try: + await self._execute_raw_command( + f"tmux kill-session -t {session_name}" + ) + except: + pass + return self.fail_response(f"Error executing command: {str(e)}") + + async def _check_command_output( + self, session_name: str, kill_session: bool = False + ) -> ToolResult: + try: + # Ensure sandbox is initialized + await self._ensure_sandbox() + + # Check if session exists + check_result = await self._execute_raw_command( + f"tmux has-session -t {session_name} 2>/dev/null || echo 'not_exists'" + ) + if "not_exists" in check_result.get("output", ""): + return self.fail_response( + f"Tmux session '{session_name}' does not exist." + ) + + # Get output from tmux pane + output_result = await self._execute_raw_command( + f"tmux capture-pane -t {session_name} -p -S - -E -" + ) + output = output_result.get("output", "") + + # Kill session if requested + if kill_session: + await self._execute_raw_command(f"tmux kill-session -t {session_name}") + termination_status = "Session terminated." + else: + termination_status = "Session still running." + + return self.success_response( + { + "output": output, + "session_name": session_name, + "status": termination_status, + } + ) + + except Exception as e: + return self.fail_response(f"Error checking command output: {str(e)}") + + async def _terminate_command(self, session_name: str) -> ToolResult: + try: + # Ensure sandbox is initialized + await self._ensure_sandbox() + + # Check if session exists + check_result = await self._execute_raw_command( + f"tmux has-session -t {session_name} 2>/dev/null || echo 'not_exists'" + ) + if "not_exists" in check_result.get("output", ""): + return self.fail_response( + f"Tmux session '{session_name}' does not exist." + ) + + # Kill the session + await self._execute_raw_command(f"tmux kill-session -t {session_name}") + + return self.success_response( + {"message": f"Tmux session '{session_name}' terminated successfully."} + ) + + except Exception as e: + return self.fail_response(f"Error terminating command: {str(e)}") + + async def _list_commands(self) -> ToolResult: + try: + # Ensure sandbox is initialized + await self._ensure_sandbox() + + # List all tmux sessions + result = await self._execute_raw_command( + "tmux list-sessions 2>/dev/null || echo 'No sessions'" + ) + output = result.get("output", "") + + if "No sessions" in output or not output.strip(): + return self.success_response( + {"message": "No active tmux sessions found.", "sessions": []} + ) + + # Parse session list + sessions = [] + for line in output.split("\n"): + if line.strip(): + parts = line.split(":") + if parts: + session_name = parts[0].strip() + sessions.append(session_name) + + return self.success_response( + { + "message": f"Found {len(sessions)} active sessions.", + "sessions": sessions, + } + ) + + except Exception as e: + return self.fail_response(f"Error listing commands: {str(e)}") + + async def execute( + self, + action: str, + command: str, + folder: Optional[str] = None, + session_name: Optional[str] = None, + blocking: bool = False, + timeout: int = 60, + kill_session: bool = False, + ) -> ToolResult: + """ + Execute a browser action in the sandbox environment. + Args: + timeout: + blocking: + session_name: + folder: + command: + kill_session: + action: The browser action to perform + Returns: + ToolResult with the action's output or error + """ + async with asyncio.Lock(): + try: + # Navigation actions + if action == "execute_command": + if not command: + return self.fail_response("command is required for navigation") + return await self._execute_command( + command, folder, session_name, blocking, timeout + ) + elif action == "check_command_output": + if session_name is None: + return self.fail_response( + "session_name is required for navigation" + ) + return await self._check_command_output(session_name, kill_session) + elif action == "terminate_command": + if session_name is None: + return self.fail_response( + "session_name is required for click_element" + ) + return await self._terminate_command(session_name) + elif action == "list_commands": + return await self._list_commands() + else: + return self.fail_response(f"Unknown action: {action}") + except Exception as e: + logger.error(f"Error executing shell action: {e}") + return self.fail_response(f"Error executing shell action: {e}") + + async def cleanup(self): + """Clean up all sessions.""" + for session_name in list(self._sessions.keys()): + await self._cleanup_session(session_name) + + # Also clean up any tmux sessions + try: + await self._ensure_sandbox() + await self._execute_raw_command("tmux kill-server 2>/dev/null || true") + except Exception as e: + logger.error(f"Error shell box cleanup action: {e}") diff --git a/app/tool/sandbox/sb_vision_tool.py b/app/tool/sandbox/sb_vision_tool.py new file mode 100644 index 0000000..ffe847d --- /dev/null +++ b/app/tool/sandbox/sb_vision_tool.py @@ -0,0 +1,178 @@ +import base64 +import mimetypes +import os +from io import BytesIO +from typing import Optional + +from PIL import Image +from pydantic import Field + +from app.daytona.tool_base import Sandbox, SandboxToolsBase, ThreadMessage +from app.tool.base import ToolResult + + +# 最大文件大小(原图10MB,压缩后5MB) +MAX_IMAGE_SIZE = 10 * 1024 * 1024 +MAX_COMPRESSED_SIZE = 5 * 1024 * 1024 + +# 压缩设置 +DEFAULT_MAX_WIDTH = 1920 +DEFAULT_MAX_HEIGHT = 1080 +DEFAULT_JPEG_QUALITY = 85 +DEFAULT_PNG_COMPRESS_LEVEL = 6 + +_VISION_DESCRIPTION = """ +A sandbox-based vision tool that allows the agent to read image files inside the sandbox using the see_image action. +* Only the see_image action is supported, with the parameter being the relative path of the image under /workspace. +* The image will be compressed and converted to base64 for use in subsequent context. +* Supported formats: JPG, PNG, GIF, WEBP. Maximum size: 10MB. +""" + + +class SandboxVisionTool(SandboxToolsBase): + name: str = "sandbox_vision" + description: str = _VISION_DESCRIPTION + parameters: dict = { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": ["see_image"], + "description": "要执行的视觉动作,目前仅支持 see_image", + }, + "file_path": { + "type": "string", + "description": "图片在 /workspace 下的相对路径,如 'screenshots/image.png'", + }, + }, + "required": ["action", "file_path"], + "dependencies": {"see_image": ["file_path"]}, + } + + # def __init__(self, project_id: str, thread_id: str, thread_manager: ThreadManager): + # super().__init__(project_id=project_id, thread_manager=thread_manager) + # self.thread_id = thread_id + # self.thread_manager = thread_manager + + vision_message: Optional[ThreadMessage] = Field(default=None, exclude=True) + + def __init__( + self, sandbox: Optional[Sandbox] = None, thread_id: Optional[str] = None, **data + ): + """Initialize with optional sandbox and thread_id.""" + super().__init__(**data) + if sandbox is not None: + self._sandbox = sandbox + + def compress_image(self, image_bytes: bytes, mime_type: str, file_path: str): + """压缩图片,保持合理质量。""" + try: + img = Image.open(BytesIO(image_bytes)) + if img.mode in ("RGBA", "LA", "P"): + background = Image.new("RGB", img.size, (255, 255, 255)) + if img.mode == "P": + img = img.convert("RGBA") + background.paste( + img, mask=img.split()[-1] if img.mode == "RGBA" else None + ) + img = background + width, height = img.size + if width > DEFAULT_MAX_WIDTH or height > DEFAULT_MAX_HEIGHT: + ratio = min(DEFAULT_MAX_WIDTH / width, DEFAULT_MAX_HEIGHT / height) + new_width = int(width * ratio) + new_height = int(height * ratio) + img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) + output = BytesIO() + if mime_type == "image/gif": + img.save(output, format="GIF", optimize=True) + output_mime = "image/gif" + elif mime_type == "image/png": + img.save( + output, + format="PNG", + optimize=True, + compress_level=DEFAULT_PNG_COMPRESS_LEVEL, + ) + output_mime = "image/png" + else: + img.save( + output, format="JPEG", quality=DEFAULT_JPEG_QUALITY, optimize=True + ) + output_mime = "image/jpeg" + compressed_bytes = output.getvalue() + return compressed_bytes, output_mime + except Exception: + return image_bytes, mime_type + + async def execute( + self, action: str, file_path: Optional[str] = None, **kwargs + ) -> ToolResult: + """ + 执行视觉动作,目前仅支持 see_image。 + 参数: + action: 必须为 'see_image' + file_path: 图片相对路径 + """ + if action != "see_image": + return self.fail_response(f"未知的视觉动作: {action}") + if not file_path: + return self.fail_response("file_path 参数不能为空") + try: + await self._ensure_sandbox() + cleaned_path = self.clean_path(file_path) + full_path = f"{self.workspace_path}/{cleaned_path}" + try: + file_info = self.sandbox.fs.get_file_info(full_path) + if file_info.is_dir: + return self.fail_response(f"路径 '{cleaned_path}' 是目录,不是图片文件。") + except Exception: + return self.fail_response(f"图片文件未找到: '{cleaned_path}'") + if file_info.size > MAX_IMAGE_SIZE: + return self.fail_response( + f"图片文件 '{cleaned_path}' 过大 ({file_info.size / (1024*1024):.2f}MB),最大允许 {MAX_IMAGE_SIZE / (1024*1024)}MB。" + ) + try: + image_bytes = self.sandbox.fs.download_file(full_path) + except Exception: + return self.fail_response(f"无法读取图片文件: {cleaned_path}") + mime_type, _ = mimetypes.guess_type(full_path) + if not mime_type or not mime_type.startswith("image/"): + ext = os.path.splitext(cleaned_path)[1].lower() + if ext == ".jpg" or ext == ".jpeg": + mime_type = "image/jpeg" + elif ext == ".png": + mime_type = "image/png" + elif ext == ".gif": + mime_type = "image/gif" + elif ext == ".webp": + mime_type = "image/webp" + else: + return self.fail_response( + f"不支持或未知的图片格式: '{cleaned_path}'。支持: JPG, PNG, GIF, WEBP。" + ) + compressed_bytes, compressed_mime_type = self.compress_image( + image_bytes, mime_type, cleaned_path + ) + if len(compressed_bytes) > MAX_COMPRESSED_SIZE: + return self.fail_response( + f"图片文件 '{cleaned_path}' 压缩后仍过大 ({len(compressed_bytes) / (1024*1024):.2f}MB),最大允许 {MAX_COMPRESSED_SIZE / (1024*1024)}MB。" + ) + base64_image = base64.b64encode(compressed_bytes).decode("utf-8") + image_context_data = { + "mime_type": compressed_mime_type, + "base64": base64_image, + "file_path": cleaned_path, + "original_size": file_info.size, + "compressed_size": len(compressed_bytes), + } + message = ThreadMessage( + type="image_context", content=image_context_data, is_llm_message=False + ) + self.vision_message = message + # return self.success_response(f"成功加载并压缩图片 '{cleaned_path}' (由 {file_info.size / 1024:.1f}KB 压缩到 {len(compressed_bytes) / 1024:.1f}KB)。") + return ToolResult( + output=f"成功加载并压缩图片 '{cleaned_path}'", + base64_image=base64_image, + ) + except Exception as e: + return self.fail_response(f"see_image 执行异常: {str(e)}") diff --git a/app/tool/search/__init__.py b/app/tool/search/__init__.py new file mode 100644 index 0000000..fe127ae --- /dev/null +++ b/app/tool/search/__init__.py @@ -0,0 +1,14 @@ +from app.tool.search.baidu_search import BaiduSearchEngine +from app.tool.search.base import WebSearchEngine +from app.tool.search.bing_search import BingSearchEngine +from app.tool.search.duckduckgo_search import DuckDuckGoSearchEngine +from app.tool.search.google_search import GoogleSearchEngine + + +__all__ = [ + "WebSearchEngine", + "BaiduSearchEngine", + "DuckDuckGoSearchEngine", + "GoogleSearchEngine", + "BingSearchEngine", +] diff --git a/app/tool/search/baidu_search.py b/app/tool/search/baidu_search.py new file mode 100644 index 0000000..8beb330 --- /dev/null +++ b/app/tool/search/baidu_search.py @@ -0,0 +1,54 @@ +from typing import List + +from baidusearch.baidusearch import search + +from app.tool.search.base import SearchItem, WebSearchEngine + + +class BaiduSearchEngine(WebSearchEngine): + def perform_search( + self, query: str, num_results: int = 10, *args, **kwargs + ) -> List[SearchItem]: + """ + Baidu search engine. + + Returns results formatted according to SearchItem model. + """ + raw_results = search(query, num_results=num_results) + + # Convert raw results to SearchItem format + results = [] + for i, item in enumerate(raw_results): + if isinstance(item, str): + # If it's just a URL + results.append( + SearchItem(title=f"Baidu Result {i+1}", url=item, description=None) + ) + elif isinstance(item, dict): + # If it's a dictionary with details + results.append( + SearchItem( + title=item.get("title", f"Baidu Result {i+1}"), + url=item.get("url", ""), + description=item.get("abstract", None), + ) + ) + else: + # Try to get attributes directly + try: + results.append( + SearchItem( + title=getattr(item, "title", f"Baidu Result {i+1}"), + url=getattr(item, "url", ""), + description=getattr(item, "abstract", None), + ) + ) + except Exception: + # Fallback to a basic result + results.append( + SearchItem( + title=f"Baidu Result {i+1}", url=str(item), description=None + ) + ) + + return results diff --git a/app/tool/search/base.py b/app/tool/search/base.py new file mode 100644 index 0000000..31d78b9 --- /dev/null +++ b/app/tool/search/base.py @@ -0,0 +1,40 @@ +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class SearchItem(BaseModel): + """Represents a single search result item""" + + title: str = Field(description="The title of the search result") + url: str = Field(description="The URL of the search result") + description: Optional[str] = Field( + default=None, description="A description or snippet of the search result" + ) + + def __str__(self) -> str: + """String representation of a search result item.""" + return f"{self.title} - {self.url}" + + +class WebSearchEngine(BaseModel): + """Base class for web search engines.""" + + model_config = {"arbitrary_types_allowed": True} + + def perform_search( + self, query: str, num_results: int = 10, *args, **kwargs + ) -> List[SearchItem]: + """ + Perform a web search and return a list of search items. + + Args: + query (str): The search query to submit to the search engine. + num_results (int, optional): The number of search results to return. Default is 10. + args: Additional arguments. + kwargs: Additional keyword arguments. + + Returns: + List[SearchItem]: A list of SearchItem objects matching the search query. + """ + raise NotImplementedError diff --git a/app/tool/search/bing_search.py b/app/tool/search/bing_search.py new file mode 100644 index 0000000..620620c --- /dev/null +++ b/app/tool/search/bing_search.py @@ -0,0 +1,144 @@ +from typing import List, Optional, Tuple + +import requests +from bs4 import BeautifulSoup + +from app.logger import logger +from app.tool.search.base import SearchItem, WebSearchEngine + + +ABSTRACT_MAX_LENGTH = 300 + +USER_AGENTS = [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36", + "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/49.0.2623.108 Chrome/49.0.2623.108 Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-BR) AppleWebKit/533.3 (KHTML, like Gecko) QtWeb Internet Browser/3.7 http://www.QtWeb.net", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) ChromePlus/4.0.222.3 Chrome/4.0.222.3 Safari/532.2", + "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4pre) Gecko/20070404 K-Ninja/2.1.3", + "Mozilla/5.0 (Future Star Technologies Corp.; Star-Blade OS; x86_64; U; en-US) iNet Browser 4.7", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; rv:2.2) Gecko/20110201", + "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080414 Firefox/2.0.0.13 Pogo/2.0.0.13.6866", +] + +HEADERS = { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": USER_AGENTS[0], + "Referer": "https://www.bing.com/", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "zh-CN,zh;q=0.9", +} + +BING_HOST_URL = "https://www.bing.com" +BING_SEARCH_URL = "https://www.bing.com/search?q=" + + +class BingSearchEngine(WebSearchEngine): + session: Optional[requests.Session] = None + + def __init__(self, **data): + """Initialize the BingSearch tool with a requests session.""" + super().__init__(**data) + self.session = requests.Session() + self.session.headers.update(HEADERS) + + def _search_sync(self, query: str, num_results: int = 10) -> List[SearchItem]: + """ + Synchronous Bing search implementation to retrieve search results. + + Args: + query (str): The search query to submit to Bing. + num_results (int, optional): Maximum number of results to return. Defaults to 10. + + Returns: + List[SearchItem]: A list of search items with title, URL, and description. + """ + if not query: + return [] + + list_result = [] + first = 1 + next_url = BING_SEARCH_URL + query + + while len(list_result) < num_results: + data, next_url = self._parse_html( + next_url, rank_start=len(list_result), first=first + ) + if data: + list_result.extend(data) + if not next_url: + break + first += 10 + + return list_result[:num_results] + + def _parse_html( + self, url: str, rank_start: int = 0, first: int = 1 + ) -> Tuple[List[SearchItem], str]: + """ + Parse Bing search result HTML to extract search results and the next page URL. + + Returns: + tuple: (List of SearchItem objects, next page URL or None) + """ + try: + res = self.session.get(url=url) + res.encoding = "utf-8" + root = BeautifulSoup(res.text, "lxml") + + list_data = [] + ol_results = root.find("ol", id="b_results") + if not ol_results: + return [], None + + for li in ol_results.find_all("li", class_="b_algo"): + title = "" + url = "" + abstract = "" + try: + h2 = li.find("h2") + if h2: + title = h2.text.strip() + url = h2.a["href"].strip() + + p = li.find("p") + if p: + abstract = p.text.strip() + + if ABSTRACT_MAX_LENGTH and len(abstract) > ABSTRACT_MAX_LENGTH: + abstract = abstract[:ABSTRACT_MAX_LENGTH] + + rank_start += 1 + + # Create a SearchItem object + list_data.append( + SearchItem( + title=title or f"Bing Result {rank_start}", + url=url, + description=abstract, + ) + ) + except Exception: + continue + + next_btn = root.find("a", title="Next page") + if not next_btn: + return list_data, None + + next_url = BING_HOST_URL + next_btn["href"] + return list_data, next_url + except Exception as e: + logger.warning(f"Error parsing HTML: {e}") + return [], None + + def perform_search( + self, query: str, num_results: int = 10, *args, **kwargs + ) -> List[SearchItem]: + """ + Bing search engine. + + Returns results formatted according to SearchItem model. + """ + return self._search_sync(query, num_results=num_results) diff --git a/app/tool/search/duckduckgo_search.py b/app/tool/search/duckduckgo_search.py new file mode 100644 index 0000000..ca269b8 --- /dev/null +++ b/app/tool/search/duckduckgo_search.py @@ -0,0 +1,57 @@ +from typing import List + +from duckduckgo_search import DDGS + +from app.tool.search.base import SearchItem, WebSearchEngine + + +class DuckDuckGoSearchEngine(WebSearchEngine): + def perform_search( + self, query: str, num_results: int = 10, *args, **kwargs + ) -> List[SearchItem]: + """ + DuckDuckGo search engine. + + Returns results formatted according to SearchItem model. + """ + raw_results = DDGS().text(query, max_results=num_results) + + results = [] + for i, item in enumerate(raw_results): + if isinstance(item, str): + # If it's just a URL + results.append( + SearchItem( + title=f"DuckDuckGo Result {i + 1}", url=item, description=None + ) + ) + elif isinstance(item, dict): + # Extract data from the dictionary + results.append( + SearchItem( + title=item.get("title", f"DuckDuckGo Result {i + 1}"), + url=item.get("href", ""), + description=item.get("body", None), + ) + ) + else: + # Try to extract attributes directly + try: + results.append( + SearchItem( + title=getattr(item, "title", f"DuckDuckGo Result {i + 1}"), + url=getattr(item, "href", ""), + description=getattr(item, "body", None), + ) + ) + except Exception: + # Fallback + results.append( + SearchItem( + title=f"DuckDuckGo Result {i + 1}", + url=str(item), + description=None, + ) + ) + + return results diff --git a/app/tool/search/google_search.py b/app/tool/search/google_search.py new file mode 100644 index 0000000..bd0838e --- /dev/null +++ b/app/tool/search/google_search.py @@ -0,0 +1,33 @@ +from typing import List + +from googlesearch import search + +from app.tool.search.base import SearchItem, WebSearchEngine + + +class GoogleSearchEngine(WebSearchEngine): + def perform_search( + self, query: str, num_results: int = 10, *args, **kwargs + ) -> List[SearchItem]: + """ + Google search engine. + + Returns results formatted according to SearchItem model. + """ + raw_results = search(query, num_results=num_results, advanced=True) + + results = [] + for i, item in enumerate(raw_results): + if isinstance(item, str): + # If it's just a URL + results.append( + {"title": f"Google Result {i+1}", "url": item, "description": ""} + ) + else: + results.append( + SearchItem( + title=item.title, url=item.url, description=item.description + ) + ) + + return results diff --git a/app/tool/str_replace_editor.py b/app/tool/str_replace_editor.py new file mode 100644 index 0000000..a907f41 --- /dev/null +++ b/app/tool/str_replace_editor.py @@ -0,0 +1,432 @@ +"""File and directory manipulation tool with sandbox support.""" + +from collections import defaultdict +from pathlib import Path +from typing import Any, DefaultDict, List, Literal, Optional, get_args + +from app.config import config +from app.exceptions import ToolError +from app.tool import BaseTool +from app.tool.base import CLIResult, ToolResult +from app.tool.file_operators import ( + FileOperator, + LocalFileOperator, + PathLike, + SandboxFileOperator, +) + + +Command = Literal[ + "view", + "create", + "str_replace", + "insert", + "undo_edit", +] + +# Constants +SNIPPET_LINES: int = 4 +MAX_RESPONSE_LEN: int = 16000 +TRUNCATED_MESSAGE: str = ( + "To save on context only part of this file has been shown to you. " + "You should retry this tool after you have searched inside the file with `grep -n` " + "in order to find the line numbers of what you are looking for." +) + +# Tool description +_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files +* State is persistent across command calls and discussions with the user +* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep +* The `create` command cannot be used if the specified `path` already exists as a file +* If a `command` generates a long output, it will be truncated and marked with `` +* The `undo_edit` command will revert the last edit made to the file at `path` + +Notes for using the `str_replace` command: +* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! +* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique +* The `new_str` parameter should contain the edited lines that should replace the `old_str` +""" + + +def maybe_truncate( + content: str, truncate_after: Optional[int] = MAX_RESPONSE_LEN +) -> str: + """Truncate content and append a notice if content exceeds the specified length.""" + if not truncate_after or len(content) <= truncate_after: + return content + return content[:truncate_after] + TRUNCATED_MESSAGE + + +class StrReplaceEditor(BaseTool): + """A tool for viewing, creating, and editing files with sandbox support.""" + + name: str = "str_replace_editor" + description: str = _STR_REPLACE_EDITOR_DESCRIPTION + parameters: dict = { + "type": "object", + "properties": { + "command": { + "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`.", + "enum": ["view", "create", "str_replace", "insert", "undo_edit"], + "type": "string", + }, + "path": { + "description": "Absolute path to file or directory.", + "type": "string", + }, + "file_text": { + "description": "Required parameter of `create` command, with the content of the file to be created.", + "type": "string", + }, + "old_str": { + "description": "Required parameter of `str_replace` command containing the string in `path` to replace.", + "type": "string", + }, + "new_str": { + "description": "Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert.", + "type": "string", + }, + "insert_line": { + "description": "Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`.", + "type": "integer", + }, + "view_range": { + "description": "Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.", + "items": {"type": "integer"}, + "type": "array", + }, + }, + "required": ["command", "path"], + } + _file_history: DefaultDict[PathLike, List[str]] = defaultdict(list) + _local_operator: LocalFileOperator = LocalFileOperator() + _sandbox_operator: SandboxFileOperator = SandboxFileOperator() + + # def _get_operator(self, use_sandbox: bool) -> FileOperator: + def _get_operator(self) -> FileOperator: + """Get the appropriate file operator based on execution mode.""" + return ( + self._sandbox_operator + if config.sandbox.use_sandbox + else self._local_operator + ) + + async def execute( + self, + *, + command: Command, + path: str, + file_text: str | None = None, + view_range: list[int] | None = None, + old_str: str | None = None, + new_str: str | None = None, + insert_line: int | None = None, + **kwargs: Any, + ) -> str: + """Execute a file operation command.""" + # Get the appropriate file operator + operator = self._get_operator() + + # Validate path and command combination + await self.validate_path(command, Path(path), operator) + + # Execute the appropriate command + if command == "view": + result = await self.view(path, view_range, operator) + elif command == "create": + if file_text is None: + raise ToolError("Parameter `file_text` is required for command: create") + await operator.write_file(path, file_text) + self._file_history[path].append(file_text) + result = ToolResult(output=f"File created successfully at: {path}") + elif command == "str_replace": + if old_str is None: + raise ToolError( + "Parameter `old_str` is required for command: str_replace" + ) + result = await self.str_replace(path, old_str, new_str, operator) + elif command == "insert": + if insert_line is None: + raise ToolError( + "Parameter `insert_line` is required for command: insert" + ) + if new_str is None: + raise ToolError("Parameter `new_str` is required for command: insert") + result = await self.insert(path, insert_line, new_str, operator) + elif command == "undo_edit": + result = await self.undo_edit(path, operator) + else: + # This should be caught by type checking, but we include it for safety + raise ToolError( + f'Unrecognized command {command}. The allowed commands for the {self.name} tool are: {", ".join(get_args(Command))}' + ) + + return str(result) + + async def validate_path( + self, command: str, path: Path, operator: FileOperator + ) -> None: + """Validate path and command combination based on execution environment.""" + # Check if path is absolute + if not path.is_absolute(): + raise ToolError(f"The path {path} is not an absolute path") + + # Only check if path exists for non-create commands + if command != "create": + if not await operator.exists(path): + raise ToolError( + f"The path {path} does not exist. Please provide a valid path." + ) + + # Check if path is a directory + is_dir = await operator.is_directory(path) + if is_dir and command != "view": + raise ToolError( + f"The path {path} is a directory and only the `view` command can be used on directories" + ) + + # Check if file exists for create command + elif command == "create": + exists = await operator.exists(path) + if exists: + raise ToolError( + f"File already exists at: {path}. Cannot overwrite files using command `create`." + ) + + async def view( + self, + path: PathLike, + view_range: Optional[List[int]] = None, + operator: FileOperator = None, + ) -> CLIResult: + """Display file or directory content.""" + # Determine if path is a directory + is_dir = await operator.is_directory(path) + + if is_dir: + # Directory handling + if view_range: + raise ToolError( + "The `view_range` parameter is not allowed when `path` points to a directory." + ) + + return await self._view_directory(path, operator) + else: + # File handling + return await self._view_file(path, operator, view_range) + + @staticmethod + async def _view_directory(path: PathLike, operator: FileOperator) -> CLIResult: + """Display directory contents.""" + find_cmd = f"find {path} -maxdepth 2 -not -path '*/\\.*'" + + # Execute command using the operator + returncode, stdout, stderr = await operator.run_command(find_cmd) + + if not stderr: + stdout = ( + f"Here's the files and directories up to 2 levels deep in {path}, " + f"excluding hidden items:\n{stdout}\n" + ) + + return CLIResult(output=stdout, error=stderr) + + async def _view_file( + self, + path: PathLike, + operator: FileOperator, + view_range: Optional[List[int]] = None, + ) -> CLIResult: + """Display file content, optionally within a specified line range.""" + # Read file content + file_content = await operator.read_file(path) + init_line = 1 + + # Apply view range if specified + if view_range: + if len(view_range) != 2 or not all(isinstance(i, int) for i in view_range): + raise ToolError( + "Invalid `view_range`. It should be a list of two integers." + ) + + file_lines = file_content.split("\n") + n_lines_file = len(file_lines) + init_line, final_line = view_range + + # Validate view range + if init_line < 1 or init_line > n_lines_file: + raise ToolError( + f"Invalid `view_range`: {view_range}. Its first element `{init_line}` should be " + f"within the range of lines of the file: {[1, n_lines_file]}" + ) + if final_line > n_lines_file: + raise ToolError( + f"Invalid `view_range`: {view_range}. Its second element `{final_line}` should be " + f"smaller than the number of lines in the file: `{n_lines_file}`" + ) + if final_line != -1 and final_line < init_line: + raise ToolError( + f"Invalid `view_range`: {view_range}. Its second element `{final_line}` should be " + f"larger or equal than its first `{init_line}`" + ) + + # Apply range + if final_line == -1: + file_content = "\n".join(file_lines[init_line - 1 :]) + else: + file_content = "\n".join(file_lines[init_line - 1 : final_line]) + + # Format and return result + return CLIResult( + output=self._make_output(file_content, str(path), init_line=init_line) + ) + + async def str_replace( + self, + path: PathLike, + old_str: str, + new_str: Optional[str] = None, + operator: FileOperator = None, + ) -> CLIResult: + """Replace a unique string in a file with a new string.""" + # Read file content and expand tabs + file_content = (await operator.read_file(path)).expandtabs() + old_str = old_str.expandtabs() + new_str = new_str.expandtabs() if new_str is not None else "" + + # Check if old_str is unique in the file + occurrences = file_content.count(old_str) + if occurrences == 0: + raise ToolError( + f"No replacement was performed, old_str `{old_str}` did not appear verbatim in {path}." + ) + elif occurrences > 1: + # Find line numbers of occurrences + file_content_lines = file_content.split("\n") + lines = [ + idx + 1 + for idx, line in enumerate(file_content_lines) + if old_str in line + ] + raise ToolError( + f"No replacement was performed. Multiple occurrences of old_str `{old_str}` " + f"in lines {lines}. Please ensure it is unique" + ) + + # Replace old_str with new_str + new_file_content = file_content.replace(old_str, new_str) + + # Write the new content to the file + await operator.write_file(path, new_file_content) + + # Save the original content to history + self._file_history[path].append(file_content) + + # Create a snippet of the edited section + replacement_line = file_content.split(old_str)[0].count("\n") + start_line = max(0, replacement_line - SNIPPET_LINES) + end_line = replacement_line + SNIPPET_LINES + new_str.count("\n") + snippet = "\n".join(new_file_content.split("\n")[start_line : end_line + 1]) + + # Prepare the success message + success_msg = f"The file {path} has been edited. " + success_msg += self._make_output( + snippet, f"a snippet of {path}", start_line + 1 + ) + success_msg += "Review the changes and make sure they are as expected. Edit the file again if necessary." + + return CLIResult(output=success_msg) + + async def insert( + self, + path: PathLike, + insert_line: int, + new_str: str, + operator: FileOperator = None, + ) -> CLIResult: + """Insert text at a specific line in a file.""" + # Read and prepare content + file_text = (await operator.read_file(path)).expandtabs() + new_str = new_str.expandtabs() + file_text_lines = file_text.split("\n") + n_lines_file = len(file_text_lines) + + # Validate insert_line + if insert_line < 0 or insert_line > n_lines_file: + raise ToolError( + f"Invalid `insert_line` parameter: {insert_line}. It should be within " + f"the range of lines of the file: {[0, n_lines_file]}" + ) + + # Perform insertion + new_str_lines = new_str.split("\n") + new_file_text_lines = ( + file_text_lines[:insert_line] + + new_str_lines + + file_text_lines[insert_line:] + ) + + # Create a snippet for preview + snippet_lines = ( + file_text_lines[max(0, insert_line - SNIPPET_LINES) : insert_line] + + new_str_lines + + file_text_lines[insert_line : insert_line + SNIPPET_LINES] + ) + + # Join lines and write to file + new_file_text = "\n".join(new_file_text_lines) + snippet = "\n".join(snippet_lines) + + await operator.write_file(path, new_file_text) + self._file_history[path].append(file_text) + + # Prepare success message + success_msg = f"The file {path} has been edited. " + success_msg += self._make_output( + snippet, + "a snippet of the edited file", + max(1, insert_line - SNIPPET_LINES + 1), + ) + success_msg += "Review the changes and make sure they are as expected (correct indentation, no duplicate lines, etc). Edit the file again if necessary." + + return CLIResult(output=success_msg) + + async def undo_edit( + self, path: PathLike, operator: FileOperator = None + ) -> CLIResult: + """Revert the last edit made to a file.""" + if not self._file_history[path]: + raise ToolError(f"No edit history found for {path}.") + + old_text = self._file_history[path].pop() + await operator.write_file(path, old_text) + + return CLIResult( + output=f"Last edit to {path} undone successfully. {self._make_output(old_text, str(path))}" + ) + + def _make_output( + self, + file_content: str, + file_descriptor: str, + init_line: int = 1, + expand_tabs: bool = True, + ) -> str: + """Format file content for display with line numbers.""" + file_content = maybe_truncate(file_content) + if expand_tabs: + file_content = file_content.expandtabs() + + # Add line numbers to each line + file_content = "\n".join( + [ + f"{i + init_line:6}\t{line}" + for i, line in enumerate(file_content.split("\n")) + ] + ) + + return ( + f"Here's the result of running `cat -n` on {file_descriptor}:\n" + + file_content + + "\n" + ) diff --git a/app/tool/terminate.py b/app/tool/terminate.py new file mode 100644 index 0000000..8c2d82c --- /dev/null +++ b/app/tool/terminate.py @@ -0,0 +1,25 @@ +from app.tool.base import BaseTool + + +_TERMINATE_DESCRIPTION = """Terminate the interaction when the request is met OR if the assistant cannot proceed further with the task. +When you have finished all the tasks, call this tool to end the work.""" + + +class Terminate(BaseTool): + name: str = "terminate" + description: str = _TERMINATE_DESCRIPTION + parameters: dict = { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "The finish status of the interaction.", + "enum": ["success", "failure"], + } + }, + "required": ["status"], + } + + async def execute(self, status: str) -> str: + """Finish the current execution""" + return f"The interaction has been completed with status: {status}" diff --git a/app/tool/tool_collection.py b/app/tool/tool_collection.py new file mode 100644 index 0000000..297ab6c --- /dev/null +++ b/app/tool/tool_collection.py @@ -0,0 +1,71 @@ +"""Collection classes for managing multiple tools.""" +from typing import Any, Dict, List + +from app.exceptions import ToolError +from app.logger import logger +from app.tool.base import BaseTool, ToolFailure, ToolResult + + +class ToolCollection: + """A collection of defined tools.""" + + class Config: + arbitrary_types_allowed = True + + def __init__(self, *tools: BaseTool): + self.tools = tools + self.tool_map = {tool.name: tool for tool in tools} + + def __iter__(self): + return iter(self.tools) + + def to_params(self) -> List[Dict[str, Any]]: + return [tool.to_param() for tool in self.tools] + + async def execute( + self, *, name: str, tool_input: Dict[str, Any] = None + ) -> ToolResult: + tool = self.tool_map.get(name) + if not tool: + return ToolFailure(error=f"Tool {name} is invalid") + try: + result = await tool(**tool_input) + return result + except ToolError as e: + return ToolFailure(error=e.message) + + async def execute_all(self) -> List[ToolResult]: + """Execute all tools in the collection sequentially.""" + results = [] + for tool in self.tools: + try: + result = await tool() + results.append(result) + except ToolError as e: + results.append(ToolFailure(error=e.message)) + return results + + def get_tool(self, name: str) -> BaseTool: + return self.tool_map.get(name) + + def add_tool(self, tool: BaseTool): + """Add a single tool to the collection. + + If a tool with the same name already exists, it will be skipped and a warning will be logged. + """ + if tool.name in self.tool_map: + logger.warning(f"Tool {tool.name} already exists in collection, skipping") + return self + + self.tools += (tool,) + self.tool_map[tool.name] = tool + return self + + def add_tools(self, *tools: BaseTool): + """Add multiple tools to the collection. + + If any tool has a name conflict with an existing tool, it will be skipped and a warning will be logged. + """ + for tool in tools: + self.add_tool(tool) + return self diff --git a/app/tool/web_search.py b/app/tool/web_search.py new file mode 100644 index 0000000..b9b9e31 --- /dev/null +++ b/app/tool/web_search.py @@ -0,0 +1,418 @@ +import asyncio +from typing import Any, Dict, List, Optional + +import requests +from bs4 import BeautifulSoup +from pydantic import BaseModel, ConfigDict, Field, model_validator +from tenacity import retry, stop_after_attempt, wait_exponential + +from app.config import config +from app.logger import logger +from app.tool.base import BaseTool, ToolResult +from app.tool.search import ( + BaiduSearchEngine, + BingSearchEngine, + DuckDuckGoSearchEngine, + GoogleSearchEngine, + WebSearchEngine, +) +from app.tool.search.base import SearchItem + + +class SearchResult(BaseModel): + """Represents a single search result returned by a search engine.""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + position: int = Field(description="Position in search results") + url: str = Field(description="URL of the search result") + title: str = Field(default="", description="Title of the search result") + description: str = Field( + default="", description="Description or snippet of the search result" + ) + source: str = Field(description="The search engine that provided this result") + raw_content: Optional[str] = Field( + default=None, description="Raw content from the search result page if available" + ) + + def __str__(self) -> str: + """String representation of a search result.""" + return f"{self.title} ({self.url})" + + +class SearchMetadata(BaseModel): + """Metadata about the search operation.""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + total_results: int = Field(description="Total number of results found") + language: str = Field(description="Language code used for the search") + country: str = Field(description="Country code used for the search") + + +class SearchResponse(ToolResult): + """Structured response from the web search tool, inheriting ToolResult.""" + + query: str = Field(description="The search query that was executed") + results: List[SearchResult] = Field( + default_factory=list, description="List of search results" + ) + metadata: Optional[SearchMetadata] = Field( + default=None, description="Metadata about the search" + ) + + @model_validator(mode="after") + def populate_output(self) -> "SearchResponse": + """Populate output or error fields based on search results.""" + if self.error: + return self + + result_text = [f"Search results for '{self.query}':"] + + for i, result in enumerate(self.results, 1): + # Add title with position number + title = result.title.strip() or "No title" + result_text.append(f"\n{i}. {title}") + + # Add URL with proper indentation + result_text.append(f" URL: {result.url}") + + # Add description if available + if result.description.strip(): + result_text.append(f" Description: {result.description}") + + # Add content preview if available + if result.raw_content: + content_preview = result.raw_content[:1000].replace("\n", " ").strip() + if len(result.raw_content) > 1000: + content_preview += "..." + result_text.append(f" Content: {content_preview}") + + # Add metadata at the bottom if available + if self.metadata: + result_text.extend( + [ + f"\nMetadata:", + f"- Total results: {self.metadata.total_results}", + f"- Language: {self.metadata.language}", + f"- Country: {self.metadata.country}", + ] + ) + + self.output = "\n".join(result_text) + return self + + +class WebContentFetcher: + """Utility class for fetching web content.""" + + @staticmethod + async def fetch_content(url: str, timeout: int = 10) -> Optional[str]: + """ + Fetch and extract the main content from a webpage. + + Args: + url: The URL to fetch content from + timeout: Request timeout in seconds + + Returns: + Extracted text content or None if fetching fails + """ + headers = { + "WebSearch": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + } + + try: + # Use asyncio to run requests in a thread pool + response = await asyncio.get_event_loop().run_in_executor( + None, lambda: requests.get(url, headers=headers, timeout=timeout) + ) + + if response.status_code != 200: + logger.warning( + f"Failed to fetch content from {url}: HTTP {response.status_code}" + ) + return None + + # Parse HTML with BeautifulSoup + soup = BeautifulSoup(response.text, "html.parser") + + # Remove script and style elements + for script in soup(["script", "style", "header", "footer", "nav"]): + script.extract() + + # Get text content + text = soup.get_text(separator="\n", strip=True) + + # Clean up whitespace and limit size (100KB max) + text = " ".join(text.split()) + return text[:10000] if text else None + + except Exception as e: + logger.warning(f"Error fetching content from {url}: {e}") + return None + + +class WebSearch(BaseTool): + """Search the web for information using various search engines.""" + + name: str = "web_search" + description: str = """Search the web for real-time information about any topic. + This tool returns comprehensive search results with relevant information, URLs, titles, and descriptions. + If the primary search engine fails, it automatically falls back to alternative engines.""" + parameters: dict = { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "(required) The search query to submit to the search engine.", + }, + "num_results": { + "type": "integer", + "description": "(optional) The number of search results to return. Default is 5.", + "default": 5, + }, + "lang": { + "type": "string", + "description": "(optional) Language code for search results (default: en).", + "default": "en", + }, + "country": { + "type": "string", + "description": "(optional) Country code for search results (default: us).", + "default": "us", + }, + "fetch_content": { + "type": "boolean", + "description": "(optional) Whether to fetch full content from result pages. Default is false.", + "default": False, + }, + }, + "required": ["query"], + } + _search_engine: dict[str, WebSearchEngine] = { + "google": GoogleSearchEngine(), + "baidu": BaiduSearchEngine(), + "duckduckgo": DuckDuckGoSearchEngine(), + "bing": BingSearchEngine(), + } + content_fetcher: WebContentFetcher = WebContentFetcher() + + async def execute( + self, + query: str, + num_results: int = 5, + lang: Optional[str] = None, + country: Optional[str] = None, + fetch_content: bool = False, + ) -> SearchResponse: + """ + Execute a Web search and return detailed search results. + + Args: + query: The search query to submit to the search engine + num_results: The number of search results to return (default: 5) + lang: Language code for search results (default from config) + country: Country code for search results (default from config) + fetch_content: Whether to fetch content from result pages (default: False) + + Returns: + A structured response containing search results and metadata + """ + # Get settings from config + retry_delay = ( + getattr(config.search_config, "retry_delay", 60) + if config.search_config + else 60 + ) + max_retries = ( + getattr(config.search_config, "max_retries", 3) + if config.search_config + else 3 + ) + + # Use config values for lang and country if not specified + if lang is None: + lang = ( + getattr(config.search_config, "lang", "en") + if config.search_config + else "en" + ) + + if country is None: + country = ( + getattr(config.search_config, "country", "us") + if config.search_config + else "us" + ) + + search_params = {"lang": lang, "country": country} + + # Try searching with retries when all engines fail + for retry_count in range(max_retries + 1): + results = await self._try_all_engines(query, num_results, search_params) + + if results: + # Fetch content if requested + if fetch_content: + results = await self._fetch_content_for_results(results) + + # Return a successful structured response + return SearchResponse( + status="success", + query=query, + results=results, + metadata=SearchMetadata( + total_results=len(results), + language=lang, + country=country, + ), + ) + + if retry_count < max_retries: + # All engines failed, wait and retry + logger.warning( + f"All search engines failed. Waiting {retry_delay} seconds before retry {retry_count + 1}/{max_retries}..." + ) + await asyncio.sleep(retry_delay) + else: + logger.error( + f"All search engines failed after {max_retries} retries. Giving up." + ) + + # Return an error response + return SearchResponse( + query=query, + error="All search engines failed to return results after multiple retries.", + results=[], + ) + + async def _try_all_engines( + self, query: str, num_results: int, search_params: Dict[str, Any] + ) -> List[SearchResult]: + """Try all search engines in the configured order.""" + engine_order = self._get_engine_order() + failed_engines = [] + + for engine_name in engine_order: + engine = self._search_engine[engine_name] + logger.info(f"🔎 Attempting search with {engine_name.capitalize()}...") + search_items = await self._perform_search_with_engine( + engine, query, num_results, search_params + ) + + if not search_items: + continue + + if failed_engines: + logger.info( + f"Search successful with {engine_name.capitalize()} after trying: {', '.join(failed_engines)}" + ) + + # Transform search items into structured results + return [ + SearchResult( + position=i + 1, + url=item.url, + title=item.title + or f"Result {i+1}", # Ensure we always have a title + description=item.description or "", + source=engine_name, + ) + for i, item in enumerate(search_items) + ] + + if failed_engines: + logger.error(f"All search engines failed: {', '.join(failed_engines)}") + return [] + + async def _fetch_content_for_results( + self, results: List[SearchResult] + ) -> List[SearchResult]: + """Fetch and add web content to search results.""" + if not results: + return [] + + # Create tasks for each result + tasks = [self._fetch_single_result_content(result) for result in results] + + # Type annotation to help type checker + fetched_results = await asyncio.gather(*tasks) + + # Explicit validation of return type + return [ + ( + result + if isinstance(result, SearchResult) + else SearchResult(**result.dict()) + ) + for result in fetched_results + ] + + async def _fetch_single_result_content(self, result: SearchResult) -> SearchResult: + """Fetch content for a single search result.""" + if result.url: + content = await self.content_fetcher.fetch_content(result.url) + if content: + result.raw_content = content + return result + + def _get_engine_order(self) -> List[str]: + """Determines the order in which to try search engines.""" + preferred = ( + getattr(config.search_config, "engine", "google").lower() + if config.search_config + else "google" + ) + fallbacks = ( + [engine.lower() for engine in config.search_config.fallback_engines] + if config.search_config + and hasattr(config.search_config, "fallback_engines") + else [] + ) + + # Start with preferred engine, then fallbacks, then remaining engines + engine_order = [preferred] if preferred in self._search_engine else [] + engine_order.extend( + [ + fb + for fb in fallbacks + if fb in self._search_engine and fb not in engine_order + ] + ) + engine_order.extend([e for e in self._search_engine if e not in engine_order]) + + return engine_order + + @retry( + stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10) + ) + async def _perform_search_with_engine( + self, + engine: WebSearchEngine, + query: str, + num_results: int, + search_params: Dict[str, Any], + ) -> List[SearchItem]: + """Execute search with the given engine and parameters.""" + return await asyncio.get_event_loop().run_in_executor( + None, + lambda: list( + engine.perform_search( + query, + num_results=num_results, + lang=search_params.get("lang"), + country=search_params.get("country"), + ) + ), + ) + + +if __name__ == "__main__": + web_search = WebSearch() + search_response = asyncio.run( + web_search.execute( + query="Python programming", fetch_content=True, num_results=1 + ) + ) + print(search_response.to_tool_result()) diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..4d3ecf1 --- /dev/null +++ b/app/utils/__init__.py @@ -0,0 +1 @@ +# Utility functions and constants for agent tools diff --git a/app/utils/files_utils.py b/app/utils/files_utils.py new file mode 100644 index 0000000..d14ad55 --- /dev/null +++ b/app/utils/files_utils.py @@ -0,0 +1,87 @@ +import os + + +# Files to exclude from operations +EXCLUDED_FILES = { + ".DS_Store", + ".gitignore", + "package-lock.json", + "postcss.config.js", + "postcss.config.mjs", + "jsconfig.json", + "components.json", + "tsconfig.tsbuildinfo", + "tsconfig.json", +} + +# Directories to exclude from operations +EXCLUDED_DIRS = {"node_modules", ".next", "dist", "build", ".git"} + +# File extensions to exclude from operations +EXCLUDED_EXT = { + ".ico", + ".svg", + ".png", + ".jpg", + ".jpeg", + ".gif", + ".bmp", + ".tiff", + ".webp", + ".db", + ".sql", +} + + +def should_exclude_file(rel_path: str) -> bool: + """Check if a file should be excluded based on path, name, or extension + + Args: + rel_path: Relative path of the file to check + + Returns: + True if the file should be excluded, False otherwise + """ + # Check filename + filename = os.path.basename(rel_path) + if filename in EXCLUDED_FILES: + return True + + # Check directory + dir_path = os.path.dirname(rel_path) + if any(excluded in dir_path for excluded in EXCLUDED_DIRS): + return True + + # Check extension + _, ext = os.path.splitext(filename) + if ext.lower() in EXCLUDED_EXT: + return True + + return False + + +def clean_path(path: str, workspace_path: str = "/workspace") -> str: + """Clean and normalize a path to be relative to the workspace + + Args: + path: The path to clean + workspace_path: The base workspace path to remove (default: "/workspace") + + Returns: + The cleaned path, relative to the workspace + """ + # Remove any leading slash + path = path.lstrip("/") + + # Remove workspace prefix if present + if path.startswith(workspace_path.lstrip("/")): + path = path[len(workspace_path.lstrip("/")) :] + + # Remove workspace/ prefix if present + if path.startswith("workspace/"): + path = path[9:] + + # Remove any remaining leading slash + path = path.lstrip("/") + + return path diff --git a/app/utils/logger.py b/app/utils/logger.py new file mode 100644 index 0000000..3c4f645 --- /dev/null +++ b/app/utils/logger.py @@ -0,0 +1,32 @@ +import logging +import os + +import structlog + + +ENV_MODE = os.getenv("ENV_MODE", "LOCAL") + +renderer = [structlog.processors.JSONRenderer()] +if ENV_MODE.lower() == "local".lower(): + renderer = [structlog.dev.ConsoleRenderer()] + +structlog.configure( + processors=[ + structlog.stdlib.add_log_level, + structlog.stdlib.PositionalArgumentsFormatter(), + structlog.processors.dict_tracebacks, + structlog.processors.CallsiteParameterAdder( + { + structlog.processors.CallsiteParameter.FILENAME, + structlog.processors.CallsiteParameter.FUNC_NAME, + structlog.processors.CallsiteParameter.LINENO, + } + ), + structlog.processors.TimeStamper(fmt="iso"), + structlog.contextvars.merge_contextvars, + *renderer, + ], + cache_logger_on_first_use=True, +) + +logger: structlog.stdlib.BoundLogger = structlog.get_logger(level=logging.DEBUG) diff --git a/assets/community_group.png b/assets/community_group.png new file mode 100644 index 0000000000000000000000000000000000000000..d422946a639394b25b91523ee5894f7ca82bf557 GIT binary patch literal 177676 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

XpAE33!8R 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%0A2>(^-Vpx>3)QTwM! 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 ZNK%&X1&gfzH(uZ1WBLE}(wJ{U{{bAq0IC20 literal 0 HcmV?d00001 diff --git a/config/.gitignore b/config/.gitignore new file mode 100644 index 0000000..eaff182 --- /dev/null +++ b/config/.gitignore @@ -0,0 +1,2 @@ +# prevent the local config file from being uploaded to the remote repository +config.toml diff --git a/config/config.example-daytona.toml b/config/config.example-daytona.toml new file mode 100644 index 0000000..10975df --- /dev/null +++ b/config/config.example-daytona.toml @@ -0,0 +1,114 @@ +# Global LLM configuration +[llm] +model = "claude-3-7-sonnet-20250219" # The LLM model to use +base_url = "https://api.anthropic.com/v1/" # API endpoint URL +api_key = "YOUR_API_KEY" # Your API key +max_tokens = 8192 # Maximum number of tokens in the response +temperature = 0.0 # Controls randomness + +# [llm] # Amazon Bedrock +# api_type = "aws" # Required +# model = "us.anthropic.claude-3-7-sonnet-20250219-v1:0" # Bedrock supported modelID +# base_url = "bedrock-runtime.us-west-2.amazonaws.com" # Not used now +# max_tokens = 8192 +# temperature = 1.0 +# api_key = "bear" # Required but not used for Bedrock + +# [llm] #AZURE OPENAI: +# api_type= 'azure' +# model = "YOUR_MODEL_NAME" #"gpt-4o-mini" +# base_url = "{YOUR_AZURE_ENDPOINT.rstrip('/')}/openai/deployments/{AZURE_DEPLOYMENT_ID}" +# api_key = "AZURE API KEY" +# max_tokens = 8096 +# temperature = 0.0 +# api_version="AZURE API VERSION" #"2024-08-01-preview" + +# [llm] #OLLAMA: +# api_type = 'ollama' +# model = "llama3.2" +# base_url = "http://localhost:11434/v1" +# api_key = "ollama" +# max_tokens = 4096 +# temperature = 0.0 + +# Optional configuration for specific LLM models +[llm.vision] +model = "claude-3-7-sonnet-20250219" # The vision model to use +base_url = "https://api.anthropic.com/v1/" # API endpoint URL for vision model +api_key = "YOUR_API_KEY" # Your API key for vision model +max_tokens = 8192 # Maximum number of tokens in the response +temperature = 0.0 # Controls randomness for vision model + +# [llm.vision] #OLLAMA VISION: +# api_type = 'ollama' +# model = "llama3.2-vision" +# base_url = "http://localhost:11434/v1" +# api_key = "ollama" +# max_tokens = 4096 +# temperature = 0.0 + +# Optional configuration for specific browser configuration +# [browser] +# Whether to run browser in headless mode (default: false) +#headless = false +# Disable browser security features (default: true) +#disable_security = true +# Extra arguments to pass to the browser +#extra_chromium_args = [] +# Path to a Chrome instance to use to connect to your normal browser +# e.g. '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' +#chrome_instance_path = "" +# Connect to a browser instance via WebSocket +#wss_url = "" +# Connect to a browser instance via CDP +#cdp_url = "" + +# Optional configuration, Proxy settings for the browser +# [browser.proxy] +# server = "http://proxy-server:port" +# username = "proxy-username" +# password = "proxy-password" + +# Optional configuration, Search settings. +# [search] +# Search engine for agent to use. Default is "Google", can be set to "Baidu" or "DuckDuckGo" or "Bing". +#engine = "Google" +# Fallback engine order. Default is ["DuckDuckGo", "Baidu", "Bing"] - will try in this order after primary engine fails. +#fallback_engines = ["DuckDuckGo", "Baidu", "Bing"] +# Seconds to wait before retrying all engines again when they all fail due to rate limits. Default is 60. +#retry_delay = 60 +# Maximum number of times to retry all engines when all fail. Default is 3. +#max_retries = 3 +# Language code for search results. Options: "en" (English), "zh" (Chinese), etc. +#lang = "en" +# Country code for search results. Options: "us" (United States), "cn" (China), etc. +#country = "us" + + +## Sandbox configuration +#[sandbox] +#use_sandbox = false +#image = "python:3.12-slim" +#work_dir = "/workspace" +#memory_limit = "1g" # 512m +#cpu_limit = 2.0 +#timeout = 300 +#network_enabled = true + +# Daytona configuration +[daytona] +daytona_api_key = "" +#daytona_server_url = "https://app.daytona.io/api" +#daytona_target = "us" #Daytona is currently available in the following regions:United States (us)、Europe (eu) +#sandbox_image_name = "whitezxj/sandbox:0.1.0" #If you don't use this default image,sandbox tools may be useless +#sandbox_entrypoint = "/usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf" #If you change this entrypoint,server in sandbox may be useless +#VNC_password = #The password you set to log in sandbox by VNC,it will be 123456 if you don't set + +# MCP (Model Context Protocol) configuration +[mcp] +server_reference = "app.mcp.server" # default server module reference + +# Optional Runflow configuration +# Your can add additional agents into run-flow workflow to solve different-type tasks. +[runflow] +use_data_analysis_agent = false # The Data Analysi Agent to solve various data analysis tasks diff --git a/config/config.example-model-anthropic.toml b/config/config.example-model-anthropic.toml new file mode 100644 index 0000000..b9fe7a8 --- /dev/null +++ b/config/config.example-model-anthropic.toml @@ -0,0 +1,16 @@ +# Global LLM configuration +[llm] +model = "claude-3-7-sonnet-latest" # The LLM model to use +base_url = "https://api.anthropic.com/v1/" # API endpoint URL +api_key = "YOUR_API_KEY" # Your API key +max_tokens = 8192 # Maximum number of tokens in the response +temperature = 0.0 # Controls randomness + + +# Optional configuration for specific LLM models +[llm.vision] +model = "claude-3-7-sonnet-20250219" # The vision model to use +base_url = "https://api.anthropic.com/v1/" # API endpoint URL for vision model +api_key = "YOUR_API_KEY" # Your API key for vision model +max_tokens = 8192 # Maximum number of tokens in the response +temperature = 0.0 # Controls randomness for vision model diff --git a/config/config.example-model-azure.toml b/config/config.example-model-azure.toml new file mode 100644 index 0000000..37f61dd --- /dev/null +++ b/config/config.example-model-azure.toml @@ -0,0 +1,18 @@ +# Global LLM configuration +[llm] #AZURE OPENAI: +api_type= 'azure' +model = "gpt-4o-mini" # The LLM model to use +base_url = "{YOUR_AZURE_ENDPOINT.rstrip('/')}/openai/deployments/{AZURE_DEPLOYMENT_ID}" # API endpoint URL +api_key = "YOUR_API_KEY" # Your API key +max_tokens = 8096 # Maximum number of tokens in the response +temperature = 0.0 # Controls randomness +api_version="AZURE API VERSION" #"2024-08-01-preview" # Azure Openai version if AzureOpenai + + +# Optional configuration for specific LLM models +[llm.vision] +model = "gpt-4o" # The vision model to use +base_url = "{YOUR_AZURE_ENDPOINT.rstrip('/')}/openai/deployments/{AZURE_DEPLOYMENT_ID}" +api_key = "YOUR_API_KEY" # Your API key for vision model +max_tokens = 8192 # Maximum number of tokens in the response +temperature = 0.0 # Controls randomness for vision model diff --git a/config/config.example-model-google.toml b/config/config.example-model-google.toml new file mode 100644 index 0000000..6c1b566 --- /dev/null +++ b/config/config.example-model-google.toml @@ -0,0 +1,16 @@ + # Global LLM configuration +[llm] +model = "gemini-2.0-flash" # The LLM model to use +base_url = "https://generativelanguage.googleapis.com/v1beta/openai/" # API endpoint URL +api_key = "YOUR_API_KEY" # Your API key +temperature = 0.0 # Controls randomness +max_tokens = 8096 # Maximum number of tokens in the response + + +# Optional configuration for specific LLM models for Google +[llm.vision] +model = "gemini-2.0-flash-exp" # The vision model to use +base_url = "https://generativelanguage.googleapis.com/v1beta/openai/" # API endpoint URL for vision model +api_key = "YOUR_API_KEY" # Your API key for vision model +max_tokens = 8192 # Maximum number of tokens in the response +temperature = 0.0 # Controls randomness for vision model diff --git a/config/config.example-model-jiekouai.toml b/config/config.example-model-jiekouai.toml new file mode 100644 index 0000000..ea40f27 --- /dev/null +++ b/config/config.example-model-jiekouai.toml @@ -0,0 +1,17 @@ +# Global LLM configuration +[llm] #Jiekou.AI: +api_type = 'jiekou' +model = "claude-sonnet-4-5-20250929" # The LLM model to use +base_url = "https://api.jiekou.ai/openai" # API endpoint URL +api_key = "your Jiekou.AI api key" # Your API key +max_tokens = 64000 # Maximum number of tokens in the response +temperature = 0.0 # Controls randomness + + +[llm.vision] #Jiekou.AI VISION: +api_type = 'jiekou' +model = "claude-sonnet-4-5-20250929" # The vision model to use +base_url = "https://api.jiekou.ai/openai" # API endpoint URL for vision model +api_key = "your Jiekou.AI api key" # Your API key for vision model +max_tokens = 64000 # Maximum number of tokens in the response +temperature = 0.0 # Controls randomness for vision model diff --git a/config/config.example-model-ollama.toml b/config/config.example-model-ollama.toml new file mode 100644 index 0000000..2d064d8 --- /dev/null +++ b/config/config.example-model-ollama.toml @@ -0,0 +1,17 @@ +# Global LLM configuration +[llm] #OLLAMA: +api_type = 'ollama' +model = "llama3.2" # The LLM model to use +base_url = "http://localhost:11434/v1" # API endpoint URL +api_key = "ollama" # Your API key +max_tokens = 4096 # Maximum number of tokens in the response +temperature = 0.0 # Controls randomness + + +[llm.vision] #OLLAMA VISION: +api_type = 'ollama' +model = "llama3.2-vision" # The vision model to use +base_url = "http://localhost:11434/v1" # API endpoint URL for vision model +api_key = "ollama" # Your API key for vision model +max_tokens = 4096 # Maximum number of tokens in the response +temperature = 0.0 # Controls randomness for vision model diff --git a/config/config.example-model-ppio.toml b/config/config.example-model-ppio.toml new file mode 100644 index 0000000..64c6335 --- /dev/null +++ b/config/config.example-model-ppio.toml @@ -0,0 +1,17 @@ +# Global LLM configuration +[llm] #PPIO: +api_type = 'ppio' +model = "deepseek/deepseek-v3-0324" # The LLM model to use +base_url = "https://api.ppinfra.com/v3/openai" # API endpoint URL +api_key = "your ppio api key" # Your API key +max_tokens = 16000 # Maximum number of tokens in the response +temperature = 0.0 # Controls randomness + + +[llm.vision] #PPIO VISION: +api_type = 'ppio' +model = "qwen/qwen2.5-vl-72b-instruct" # The vision model to use +base_url = "https://api.ppinfra.com/v3/openai" # API endpoint URL for vision model +api_key = "your ppio api key" # Your API key for vision model +max_tokens = 96000 # Maximum number of tokens in the response +temperature = 0.0 # Controls randomness for vision model diff --git a/config/config.example.toml b/config/config.example.toml new file mode 100644 index 0000000..2e99bf8 --- /dev/null +++ b/config/config.example.toml @@ -0,0 +1,113 @@ +# Global LLM configuration +[llm] +model = "claude-3-7-sonnet-20250219" # The LLM model to use +base_url = "https://api.anthropic.com/v1/" # API endpoint URL +api_key = "YOUR_API_KEY" # Your API key +max_tokens = 8192 # Maximum number of tokens in the response +temperature = 0.0 # Controls randomness + +# [llm] # Amazon Bedrock +# api_type = "aws" # Required +# model = "us.anthropic.claude-3-7-sonnet-20250219-v1:0" # Bedrock supported modelID +# base_url = "bedrock-runtime.us-west-2.amazonaws.com" # Not used now +# max_tokens = 8192 +# temperature = 1.0 +# api_key = "bear" # Required but not used for Bedrock + +# [llm] #AZURE OPENAI: +# api_type= 'azure' +# model = "YOUR_MODEL_NAME" #"gpt-4o-mini" +# base_url = "{YOUR_AZURE_ENDPOINT.rstrip('/')}/openai/deployments/{AZURE_DEPLOYMENT_ID}" +# api_key = "AZURE API KEY" +# max_tokens = 8096 +# temperature = 0.0 +# api_version="AZURE API VERSION" #"2024-08-01-preview" + +# [llm] #OLLAMA: +# api_type = 'ollama' +# model = "llama3.2" +# base_url = "http://localhost:11434/v1" +# api_key = "ollama" +# max_tokens = 4096 +# temperature = 0.0 + +# [llm] #Jiekou.AI: +# api_type = 'jiekou' +# model = "claude-sonnet-4-5-20250929" # The LLM model to use +# base_url = "https://api.jiekou.ai/openai" # API endpoint URL +# api_key = "your Jiekou.AI api key" # Your API key +# max_tokens = 64000 # Maximum number of tokens in the response +# temperature = 0.0 # Controls randomness + +# Optional configuration for specific LLM models +[llm.vision] +model = "claude-3-7-sonnet-20250219" # The vision model to use +base_url = "https://api.anthropic.com/v1/" # API endpoint URL for vision model +api_key = "YOUR_API_KEY" # Your API key for vision model +max_tokens = 8192 # Maximum number of tokens in the response +temperature = 0.0 # Controls randomness for vision model + +# [llm.vision] #OLLAMA VISION: +# api_type = 'ollama' +# model = "llama3.2-vision" +# base_url = "http://localhost:11434/v1" +# api_key = "ollama" +# max_tokens = 4096 +# temperature = 0.0 + +# Optional configuration for specific browser configuration +# [browser] +# Whether to run browser in headless mode (default: false) +#headless = false +# Disable browser security features (default: true) +#disable_security = true +# Extra arguments to pass to the browser +#extra_chromium_args = [] +# Path to a Chrome instance to use to connect to your normal browser +# e.g. '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' +#chrome_instance_path = "" +# Connect to a browser instance via WebSocket +#wss_url = "" +# Connect to a browser instance via CDP +#cdp_url = "" + +# Optional configuration, Proxy settings for the browser +# [browser.proxy] +# server = "http://proxy-server:port" +# username = "proxy-username" +# password = "proxy-password" + +# Optional configuration, Search settings. +# [search] +# Search engine for agent to use. Default is "Google", can be set to "Baidu" or "DuckDuckGo" or "Bing". +#engine = "Google" +# Fallback engine order. Default is ["DuckDuckGo", "Baidu", "Bing"] - will try in this order after primary engine fails. +#fallback_engines = ["DuckDuckGo", "Baidu", "Bing"] +# Seconds to wait before retrying all engines again when they all fail due to rate limits. Default is 60. +#retry_delay = 60 +# Maximum number of times to retry all engines when all fail. Default is 3. +#max_retries = 3 +# Language code for search results. Options: "en" (English), "zh" (Chinese), etc. +#lang = "en" +# Country code for search results. Options: "us" (United States), "cn" (China), etc. +#country = "us" + + +## Sandbox configuration +#[sandbox] +#use_sandbox = false +#image = "python:3.12-slim" +#work_dir = "/workspace" +#memory_limit = "1g" # 512m +#cpu_limit = 2.0 +#timeout = 300 +#network_enabled = true + +# MCP (Model Context Protocol) configuration +[mcp] +server_reference = "app.mcp.server" # default server module reference + +# Optional Runflow configuration +# Your can add additional agents into run-flow workflow to solve different-type tasks. +[runflow] +use_data_analysis_agent = false # The Data Analysi Agent to solve various data analysis tasks diff --git a/config/mcp.example.json b/config/mcp.example.json new file mode 100644 index 0000000..1946425 --- /dev/null +++ b/config/mcp.example.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "server1": { + "type": "sse", + "url": "http://localhost:8000/sse" + } + } +} diff --git a/examples/benchmarks/__init__.py b/examples/benchmarks/__init__.py new file mode 100644 index 0000000..0f96a64 --- /dev/null +++ b/examples/benchmarks/__init__.py @@ -0,0 +1,3 @@ +""" +OpenManus benchmark system for standardized agent evaluation. +""" diff --git a/examples/use_case/japan-travel-plan/japan_travel_guide_instructions.txt b/examples/use_case/japan-travel-plan/japan_travel_guide_instructions.txt new file mode 100644 index 0000000..a45128f --- /dev/null +++ b/examples/use_case/japan-travel-plan/japan_travel_guide_instructions.txt @@ -0,0 +1,62 @@ +JAPAN TRAVEL HANDBOOK - GUIDE TO VERSIONS + +Location: D:/OpenManus/ + +1. DETAILED DIGITAL VERSION +File: japan_travel_handbook.html +Best for: Desktop/laptop viewing +Features: +- Complete comprehensive guide +- Detailed itinerary +- Full proposal planning section +- All hotel recommendations +- Comprehensive budget breakdown +Usage: Open in web browser for trip planning and detailed reference + +2. PRINT-FRIENDLY VERSION +File: japan_travel_handbook_print.html +Best for: Physical reference during travel +Features: +- Condensed essential information +- Optimized for paper printing +- Clear, printer-friendly formatting +- Quick reference tables +Usage: Print and keep in travel documents folder + +3. MOBILE-OPTIMIZED VERSION +File: japan_travel_handbook_mobile.html +Best for: On-the-go reference during trip +Features: +- Touch-friendly interface +- Collapsible sections +- Quick access emergency buttons +- Dark mode support +- Responsive design +Usage: Save to phone's browser bookmarks for quick access + +RECOMMENDED SETUP: +1. Before Trip: + - Use detailed version for planning + - Print the print-friendly version + - Save mobile version to phone + +2. During Trip: + - Keep printed version with travel documents + - Use mobile version for daily reference + - Access detailed version when needed for specific information + +3. Emergency Access: + - Mobile version has quick-access emergency information + - Keep printed version as backup + - All emergency numbers and contacts in both versions + +Note: All versions contain the same core information but are formatted differently for optimal use in different situations. + +IMPORTANT DATES: +- Trip Duration: April 15-23, 2024 +- Proposal Day: April 19, 2024 +- Key Reservation Deadlines: + * Flights: Book by January 2024 + * Hotels: Book by February 2024 + * Restaurant Reservations: Book by January 2024 + * JR Pass: Purchase by March 2024 diff --git a/examples/use_case/japan-travel-plan/japan_travel_handbook.html b/examples/use_case/japan-travel-plan/japan_travel_handbook.html new file mode 100644 index 0000000..5b5965e --- /dev/null +++ b/examples/use_case/japan-travel-plan/japan_travel_handbook.html @@ -0,0 +1,124 @@ + + + + + + Japan Travel Handbook - April 15-23, 2024 + + + +

+ [Previous content remains the same...] + +
+

🌸 Proposal Planning Guide 🌸

+ +

Ring Security & Transport

+
    +
  • Carrying the Ring: +
      +
    • Always keep the ring in your carry-on luggage, never in checked bags
    • +
    • Use a discrete, non-branded box or case
    • +
    • Consider travel insurance that covers jewelry
    • +
    • Keep receipt/appraisal documentation separate from the ring
    • +
    +
  • +
  • Airport Security Tips: +
      +
    • No need to declare the ring unless value exceeds ¥1,000,000 (~$6,700)
    • +
    • If asked, simply state it's "personal jewelry"
    • +
    • Consider requesting private screening to maintain surprise
    • +
    • Keep ring in original box until through security, then transfer to more discrete case
    • +
    +
  • +
+ +

Proposal Location Details - Maruyama Park

+
    +
  • Best Timing: +
      +
    • Date: April 19 (Day 5)
    • +
    • Time: 5:30 PM (30 minutes before sunset)
    • +
    • Park closes at 8:00 PM in April
    • +
    +
  • +
  • Specific Spot Recommendations: +
      +
    • Primary Location: Near the famous weeping cherry tree +
      - Less crowded in early evening +
      - Beautiful illumination starts at dusk +
      - Iconic Kyoto backdrop +
    • +
    • Backup Location: Gion Shirakawa area +
      - Atmospheric stone-paved street +
      - Traditional buildings and cherry trees +
      - Beautiful in light rain +
    • +
    +
  • +
+ +

Proposal Day Planning

+
    +
  • Morning Preparation: +
      +
    • Confirm weather forecast
    • +
    • Transfer ring to secure pocket/bag
    • +
    • Have backup indoor location details ready
    • +
    +
  • +
  • Suggested Timeline: +
      +
    • 4:00 PM: Start heading to Maruyama Park area
    • +
    • 4:30 PM: Light refreshments at nearby tea house
    • +
    • 5:15 PM: Begin walk through park
    • +
    • 5:30 PM: Arrive at proposal spot
    • +
    • 6:00 PM: Sunset and illumination begins
    • +
    • 7:00 PM: Celebratory dinner reservation
    • +
    +
  • +
+ +

Celebration Dinner Options

+
    +
  • Traditional Japanese: Kikunoi Roan +
    - Intimate 2-star Michelin restaurant +
    - Advance reservation required (3 months) +
    - Price: ¥15,000-20,000 per person +
  • +
  • Modern Fusion: The Sodoh +
    - Beautiful garden views +
    - Western-style seating available +
    - Price: ¥12,000-15,000 per person +
  • +
+ +
+

Important Notes:

+
    +
  • Keep proposal plans in separate notes from shared itinerary
  • +
  • Have a backup plan in case of rain (indoor locations listed above)
  • +
  • Consider hiring a local photographer to capture the moment
  • +
  • Save restaurant staff contact info in case of timing changes
  • +
+
+
+
+ + diff --git a/examples/use_case/japan-travel-plan/japan_travel_handbook_mobile.html b/examples/use_case/japan-travel-plan/japan_travel_handbook_mobile.html new file mode 100644 index 0000000..00e1a92 --- /dev/null +++ b/examples/use_case/japan-travel-plan/japan_travel_handbook_mobile.html @@ -0,0 +1,255 @@ + + + + + + Japan Travel Guide (Mobile) + + + +
+

Japan Travel Guide

+

April 15-23, 2024

+ +
+ + + + +
+ +
+

Emergency Contacts

+
    +
  • 🚑 Emergency: 119
  • +
  • 👮 Police: 110
  • +
  • 🏢 US Embassy: +81-3-3224-5000
  • +
  • ℹ️ Tourist Info: 03-3201-3331
  • +
+
+ + +
+
+ + + + + + + + + +
DateLocationActivities
Apr 15TokyoArrival, Shinjuku
Apr 16TokyoMeiji, Harajuku, Senso-ji
Apr 17TokyoTea Ceremony, Budokan
Apr 18KyotoTravel, Kinkaku-ji
Apr 19KyotoFushimi Inari, Proposal
Apr 20NaraDeer Park, Temples
Apr 21TokyoReturn, Bay Cruise
+
+
+ + +
+
+ + + + + + + +
EnglishJapanese
Thank youありがとう
Excuse meすみません
Pleaseお願いします
Where is......はどこですか
Help!助けて!
+
+
+ + +
+
+

Key Routes

+
    +
  • Tokyo-Kyoto: 2h15m
  • +
  • Kyoto-Nara: 45m
  • +
  • Last trains: ~midnight
  • +
+

JR Pass: Activate April 15

+
+
+ + +
+
+

April 19 Timeline

+
    +
  • 4:00 PM: Head to Maruyama Park
  • +
  • 5:30 PM: Arrive at spot
  • +
  • 7:00 PM: Dinner at Kikunoi Roan
  • +
+

Backup: Gion Shirakawa area

+
+
+ + +
+
+ + + + + + + +
ItemBudget
Hotels$1500-2000
Transport$600-800
Food$800-1000
Activities$600-800
Shopping$500-400
+
+
+
+ + + + diff --git a/examples/use_case/japan-travel-plan/japan_travel_handbook_print.html b/examples/use_case/japan-travel-plan/japan_travel_handbook_print.html new file mode 100644 index 0000000..b924628 --- /dev/null +++ b/examples/use_case/japan-travel-plan/japan_travel_handbook_print.html @@ -0,0 +1,162 @@ + + + + + Japan Travel Handbook (Print Version) - April 15-23, 2024 + + + +

Japan Travel Handbook (Print Version)

+

Trip Dates: April 15-23, 2024

+ +
+

Emergency Contacts & Important Information

+
    +
  • Emergency in Japan: 119 (Ambulance/Fire) / 110 (Police)
  • +
  • US Embassy Tokyo: +81-3-3224-5000
  • +
  • Tourist Information Hotline: 03-3201-3331
  • +
  • Your Travel Insurance: [Write number here]
  • +
+
+ +
+

Daily Itinerary Summary

+ + + + + + + + + +
DateLocationKey Activities
Apr 15TokyoArrival, Shinjuku area exploration
Apr 16TokyoMeiji Shrine, Harajuku, Senso-ji, Skytree
Apr 17TokyoTea Ceremony, Budokan, Yanaka Ginza
Apr 18KyotoTravel to Kyoto, Kinkaku-ji, Gion
Apr 19KyotoFushimi Inari, Arashiyama, Evening Proposal
Apr 20Nara/KyotoNara Park day trip, deer feeding
Apr 21TokyoReturn to Tokyo, bay cruise
+
+ +
+ +
+

Essential Japanese Phrases

+ + + + + + + +
EnglishJapaneseWhen to Use
Arigatou gozaimasuありがとうございますThank you (formal)
SumimasenすみませんExcuse me/Sorry
Onegaishimasuお願いしますPlease
Toire wa doko desu ka?トイレはどこですか?Where is the bathroom?
Eigo ga hanasemasu ka?英語が話せますか?Do you speak English?
+
+ +
+

Transportation Notes

+
    +
  • JR Pass: Activate on April 15
  • +
  • Tokyo-Kyoto Shinkansen: ~2h15m
  • +
  • Kyoto-Nara Local Train: ~45m
  • +
  • Last trains: Usually around midnight
  • +
  • Keep ¥3000 for unexpected taxi rides
  • +
+
+ +
+ +
+

Proposal Day Timeline (April 19)

+ + + + + + + +
TimeActivityNotes
4:00 PMHead to Maruyama ParkCheck weather first
4:30 PMTea house visitLight refreshments
5:15 PMPark walk beginsHead to weeping cherry tree
5:30 PMArrive at spotFind quiet area
7:00 PMDinner reservationKikunoi Roan
+

Backup Location: Gion Shirakawa area (in case of rain)

+
+ +
+

Quick Reference Budget

+ + + + + + + +
ItemBudget (USD)Notes
Hotels1500-2000Pre-booked
Transport600-800Including JR Pass
Food800-1000~$60/person/day
Activities600-800Including tea ceremony
Shopping500-400Souvenirs/gifts
+
+ + diff --git a/examples/use_case/pictures/japan-travel-plan-1.png b/examples/use_case/pictures/japan-travel-plan-1.png new file mode 100644 index 0000000000000000000000000000000000000000..e9e344e81c5a1d9110424d9207edbd5630f770b3 GIT binary patch literal 168010 zcmeFZXH-*L*9MBmf)NA+6_jQa1sheGlnAJxC`Cm?N>HkFq=c44rC9-`6N-Qc(vcck zMAT41lTLt86Iuw6P?C`320ZOO-+8}rf823@eR~Xs1hdzgd#<^j`OLER3nK&F{risY zD3w|;h@-E(X?I9OTNE^FQKvsz$)Ut9J=30x~$yg~WsfF%xmsQ8^H z;z?1~&v*J9lT(rdW(X&Yaa^;bT;g*ZxQRW&dw$qCei!G_hLV|=8m`@oV!>}Sa~_q2 zbe?FcoEi?iebhGpBfaJDh+DjzX4i-F3kC%3EBF<0VI7vLM=aL4u(HB?L;_#e9sXms1&xr9KYl+fbpDuaZ2Ga;%KFs;rDL|x&1H0(SBFOY zen8!7pE({4VtR^lp8)^{CyB7Y^MBkn5V#kxUI-{eNUR1ks{UF~85I7$R zqhuzAg)rygjH1Gif810Y&&U7U?;q}2mpzFx;o>wS+Wcrw1EuW;K&h=@B{-uWep7Gn z?_aWRKPIv3_uq@ZIPL)E9p?MYToz|}^7HM}(e86d^$&~U`~3@FlC}Q2@oCp@+IP<% zqXdg@UfRFctT8HTbvhn=>d(<+b^j^Ko{$BbAN3pGWpdfma08&@Twqxi6~uDT*8Y70 z4zJ&Uz~6s=vimC6UNbwr8DZQ;*ioL97WM>SW-I*KUm_U%Q+95^qlWiJ=Ork}4rs*R z#J=>`J=W~>Hd@1n5?cCNe2Eewee@=qj54iH+#YK_ghkanLBJ(8c z-A|tmmMo81QL=@zRrdji8lMgK0%AWadmY>!#2U732L~li{<-qSD(C$4a=wnfIa$ej zDlDqA;8|2>*&oL6{9nHQT0+A6OTMN9c)%-fvYm?N_aXE|M&?#3Dxu9E?}$5Amagu0 zBTg_}ElJ6{zxYG{r$(MZdG9IJWyPd{%jVybPm3EC7>3bG zGK)ZW=BUES(J`md^|#u)=X`=j>_swNGeGZr&$uRr(UKI-aQ%An1D#tt;2%#8(~}w^ z=m74&msG*+zPzcJ#5i8dKbo!=5|U(nxn3O{Ss!J@@9kmtjSx~r_99e)B*!D(kGeX3 z)Hros!L+E*btPHa`e&a8#DM=eU`gw+=WCZ>P&@dn>utUpZ?T)sSw+(#pTL!_6uUh( z(sOdFeic6FX3^tJy_|K<)ppBDs3ouvYoU-6jxBPXpF2aVJ^qOvBLUsNEwb$6ZAjRC z4e%GV@NkLoKi1lH0ROz*q!P*ZocEm6(+DYvTVN18|KyntO9jnS+%{4TuYq&(PSDQx zoRSk2XdmzA^|8}A(^sMID^F?`y{{V*a-dy2SM%t0kX0SHOhGlf^5%+=D-ZFpdbHO* zTFHBm#9Tsn4*5)>{!p45+`DZ9l%I>g!HiG)>*OI#9}x zaIGiQBFER-PLSzGDr}@-o&AB$$*}q$?j0P5-|pTA*bP4&dX?4W*#3rDQW$D@&R9iz z&xOmPbMnI=-@sOK|J^xYVHe{HR$y$eV?>AO*?HW7J!j_Rr@%8A8sp2?KxG&Doh!oO zi_cq|`-@I4*yvWYHIxj=-B+8Wd0!xy(vxAB33+=^>Pir+x=6gdqE8Ci&%as4*ALcW z@{P}ZT(zt_f3%}mgSu?cfk+;{_Y3J?A7cBLUTv*gY#5ad*<`%N*{$u>)>g=5GGrIx zKfxkhU;;d?r)RODa8wWK3McV4EI#!ni{$Rss^Sio&*Nk$@+K+BR1qUS7<`vT2;dTK z=1*6=VF(N37gE-eT82jW`a2LO9;6f;e-b-g@CqNIvfdPrS3Q1#koG_(c~;w7-vKiX z8TRFPa^Ukur0^@FuOiK$`#Lr92-Ll0E8o=(LKHwpTk*4-27It@wRN$7=czbZ zqx3l7&{0m=pEBC%m8T>Qf=f$Pm5wTh_qFF}PH;gq47xU&xAZYfZDid0DSq*>NaOmu zyl|Xpzht4gdffii3P<%uugi%)XKI_f=6->ito<)b3Ju!LkX6s5 z&9mY3aPpM$X;}QA@LoXMYub8aG58}*L!RMU`@vn1$v6(*Y5IqH&?;c&z%57}q$ttZ zttK1Jux`q7g1Ohc`$*`EWJC-GOoW)Z6`6gGkU~C?9v1IGu7w~s;;B<=*b@$p~N-uvLmNmDCmJ?{Mdi~NX*z3ObV$J4_`}PO;_Z<=HplpuA&do+XNUA8C z&LOK7BKp9q6*;cssym^j`aR3P^dTg-1@gOtwPnEdF3crfR_7*@#DWp~QOwlLEWa|jMemMzT-FSS)UuEu$cI`Nb)Kd-qE8@ zC$i~z)UE7mDQLAKi8RD+=coG~4G^BFYHl=@T(Iz7I)QK#AC@lEcq}CpJ&B)KEOqDL z@B#pV2bhl6v`=de6el6;)s${qiHsQyja95Nf~%xYtVcFK)$9xw@YLv8xq|dmMHcH0 zbhODq$K!%jcQpImu^kerII+0Cry#`=N}(xDdruyXPsf}!ZSR>(Oa^yG?q(QY)(A&D z?(t%P@-0WokS6Z0hQum$)g=3WT=jk9?JKMn-I3)pWDIQ5SU?^Vg7 zmAr1^ru~?7Ym2f=q8wj@*hbN{IVq(MMqa(L9x3H{{($u(=38NrNha`TEa5kS2GG|b zxKKaqSlo^SudK2o$}lq`2NsGi^e@a=RvI!JjVP{A1HT(TmHc0eZFWY?GUSiZ22Nnd z<^@9izxm%+YotxJlub@z55%XpQlip}kR(&J*uK2dvp2%%+p>5TLd}TAS7m}%4WJa;g{UYZ@swF{M3izDpDQ{-1YVEsdt{CbY;SQOoHII* zPrMM9f5K@Y;gvXdYl#>M>V5AguI0qon6Jn8{PUcXkC$E#CReK2?%=?1XlwIO1uKq@ zF0tN6vHWiRcJ6a6C22W+Z(yCuy#pa~wYypO;#glIu)zAN=e|8Ycd>DMQWvPMh$&E6 zhJ4G{^CR)$7?m^7g}1Ie^y(v=b}pCM;M-JR>GK#$_;E~H2Vp9IP~p4ZRp_mO$U{&@ zI*>5eY2il5Rg*Rk&w+jhoqMR+vRptgQ4#+MGbqaJ=iJtgHRcy{rY%BHd3!?gALupF zCSJ*~x!n^=WmYQA&ezRCPEbtMjR)mCD8k+G$s(Ztr7A7mr*!%L;wX0_?%ilk%jP%Z z($5DM%yV04-`L*j4egscX(7S6qX^NxUX(Ds6g1ypR~oeLS-SS^P-%+Ur5>1a=|XMA zgFbO}kyw8ZYO$F{<%5c_R3nAm(XXG<%oo*^Ft-iDk}x7Jvyjl zn(9erSR38#HLWn1?*J>PX*4{0=UMN&=n-^d`c|)Cv?fDObKm?JTDnF$%7o*rYg&WT#0W`l);NiPR}<)MTcLJG6z0OKF@Uz<<13 zchQ%xF?q12QyTU^rhCv z^dr9GO5;JkvS-NO#xf^4I1WdC-(v16f6(`D+s44(GIE6-vuUrnX|qw~y?4XXP(KCT z|3Nx26WqO6872MP%5RZ!DKQ+AC@8PLh()ly?U3qAlB$hT?8A58TRW_jgwyyKK+B6x z(zO%&5)KHQA38Puw$z@Xbwv9UHz}Ybul_peOUdSGqNZe=_XA7w+7TEt-wYs#P$b=x zb5O8Bv2?Vt_Fj%+<@R_#`S!@f)?h|lAW6{$<#8(DhT(*UGk!&&J0wIpp)K--M}h~n zUpm2g(5O8Ext8^{BVMf|>CMw6>#ohu(&Ea5qPg*B>7I9YYKzx8x>0sTdgre9@AOp7 z!+F>2+FFB;djT3B%jUm46(EX2J^}!i)d8a-yrj(xzZn~ZXO)wTwl>KnN#Xb%uQ4W9 zz;k2upL1WH&Ut~wHrm&gaDshK?M`skjc%5jX@A?>cSzUsn}IT*NPF^@zMFBSxPxi8 zdjlgA_&8qgXY-t5qyLO2Bq($gi|(`)Bx|R!HVJHgduj05IHWxxYbimiyr%6#`fK@k z%$(QbEOHpq1Lf-y*>e2h&$U|)o7mpVuUMh42$m`id!Vad6F1t8IXi?_tt4^g0$eS5 z_BtV_D$>kTtfe2>kYFJ0NUQ@eT^Ou(R9wMP@HfZPB7`1H3KMLm9Ns$_er_+|P%h5S zAyC#j#t(g0^59|xnHqxj)K^T8f$`q(HlYbJ%v%R9eJT|MroU{9{04b%+z2|fVxTyS6UUl>rN%MI`n`0i zM0i$UD{$gId|gl=gd_*DN{p9~bIp=8=^4vtst!(kywyo(g^1P<9-U$Ic0 zpoU*r1t9_X?eO=$O4C`k(&)nkw*6jec$UsDj|^;c8THWrsD{N42}r)XLZ-m^+s-|g z7FC>=8FmzVa|^^j?-Gz5lBIxuT*bi>J#b&O*w#S3OT)^JK>KJ`qc-e*6aK1CP$nMJ zrK^NdgcHI{r4zeJZ_&2Tgl@}sO~CD!H_-4~b z?4;3QWMojryfe8^(Q8&GLx$(B&w(9Db^8D}?d0#qN7uxjgDz=z{t&h&-<}=Oepjqx zpB%4QY_DYHxzm+Cf2TSst$zKdn)=`|+^j=kBC$G0)N3--$Lo$}SfLUWHv>$!^V+mf z<|JnJwU0)NNs`W$GunD7U7_Ollo9Ta%;EmDtL8w@l+=$lqre#vwq ztV>Ig2#w8j)%Danq#xevF*|_z{Q0fnqAxWibD2Z+o^3Ab8KGIZ>qN+~ry?!zSW?lm zPdeJYDrJ#bL!U->5@KkNNRw00;?|DQDYB9O>wAyU=@hawz~#@Eespd(H<;v4!-%eqCgdeSugu{$$07FzjG46!WAA0vHMb_q2ZE0qY{_yFmfreJyi$A;}il;pH z*c|t@9r!?~D+)hb_(Y@JNaYZaCt|p*he#UtM-t~bGxTFCv12( z`%YFCMLqjI?Y+F>M;pjaDq9Rk$=;Niy>VgBbwAsFpVMZ~QLs^w7ZC>{o8D<CoijQz<@#uJYk1oO|NfZuWI2S!>Ukit zednyr0gYKguY}lLvE5ms%!%;l&2W)e2Y^NuG zS!l!VMiDmF{OG;uHJj(WD(aLWpTGMR8VZ?A{Ai`-?vi-61AJj#{4+#-Uwgs%y#TvL z9=2Ibbjg)kL?!1Guoae6bM)qr>Cl8f1d{CyUfG*g`BV4Jp|gim2=Wq?zKTKGXc*FO zu}qaFcI4Y>0O0&QMGo@X|g3dE|?g5oI5UR_`yQ48S7f< zS-^?aUCK^uhVhy;OBkttd12talz6bp?r4ggamt1GNCermTgR30>upjCG0ApCMQU;?w?HF*n|x-eJ)2bE2@hErf1ZA^nK_qI(@b}Z z2d1aOQvF1ttqmc{xi3a_6dxS}*0$>3vidOsrUFl=iqf#b@Od){aFZQC5)MzyJ9?X0EEgK zwR8O0@oJ=2=KM6Nt(lpcQkBZV!L{eFob7g|c+2IBn}bzEWZv5`NsOt&9YfmK)mQk- zl$@O3Af zswKOu=+#~W$@r4%%VIjrw{qdp|IAmj|JeEJGe_VxWahUqZR{2lP9Kou#Ci4P!yMa^ z;r87YH6^anH%t7Iq1-pG;J!Yj>3wKXi^Gg_8X!su(n)Vfu8BnPCf)ZRUbP06`>CFu zUd>7H=nA?Eokp%TrH`qog(+jcbAI*O&IASPxLL^R29teD2SAALAvhWUtB9bh?gcMG}5{cYJA-0aO`N|VZZ7G-oWh>ucs!vDsQo0zkNxMjIuq;SpT zb2GSIt3?%P6jnUfu_lt1vNv$)gu{euugK6HX8R4z z+u35_vpU-6*%6R1uEq#AE+Z_&579ZAq|3(KyD=TukdK%l&~mRx7vcgh@O-ZgY_sqR|M9`AKt`eu4dVfvq{rWF%TgY0 zkE2K{1s>r)QR^D>Q02i9RXhH7^ORkMVL$vIUlrk(SAJIT_IrOz`qn9~Ac$sM*rhV(*pLvO`~>vo*FZh~ z`v7rz#<_f0H*sI5xa6eo#Wnlo$B$3l-%NAt3i5^arthlyjZT|hLG%1Tqbig16Fshp z(^Vy6dWe<#sI|dKOe&9Al#gia^6X>V$#jcD?4|E&{qNLHU4bYX zYbwX(mfRulb+?eun;59-_^EqEv?|at4XpO4ZffKSjqaU2w^KDfaI_TKq;_mIMuAP4 z4@9Fl-Td`t*!f65?;Cj1>(hFqq(cEs`IbJMrz{Nc1s7SIiTNx@MI|Pd(19q&U39t| z7q_}zJYD0^cI(b!2s_zTFC&>)8NI225EC8+aC2C5|BH9x_N3aZMEth%3}n|e!CP!X z>xIByt;?vlsVK6N&;D*{?8G)DXLi7qorbjcUGO1h-nP1@tUEo~7T2E8>RR_x?Wc%e z$0M)i@U+qD2TC2Hf{=jX2sw!hHTC)UEc zPOaM7eX+cj^8Lf2CcbViNvpg~9ch#ZwV}zS4AqiS*Fp`H(o*8sM*EQi5jkaK#+`hG} zI@{-w$o9`GrIQ3Q7Wx9)*EJuWMXJTK0=sa=IW(8Lm0kgR>ey#9B)m5t>eFaJeSru+ zFF=Vs{LLrrJeFwrdI5(P#$!e%ROAe=0Ex>KSOrc+JsvJzG1wVc>ZhtIaGq&+kUz;1 zd?kmgFLvDWp}d7!{Ait%9#trjt1VK6R*b$_3Bb&fhb5C;Lf8$CKOU-9h<}t5pdjw0Lgu~<7G6(BqS_#Bm z@Q#)abgHh@Zye{&{H@?8bcAPRYu6sPVoRZ;H}hF_`OPLP48<6JH0`GF6q27%+F{AZ zl`4t(?$}^q#>c=bdI6DGz4zx$&At=~I!-ob`JpbYizh+@mOjdw_G$Pnowbh=wIEi;rEP6bN z!m;&96t|FdAc5ud8=f(LvUmHSWMKlaI&CU*jE7y5>B&}FxT4o|3w}61sj-z4J4vKh z(M6&dxA^!!GwybjD0*!!TC0zUIE%Y0e`|kJ@wyntbM8E%kvHGRt2_kaizz4cx6L|n zf9*J@hPt~5a_vjQ-CA_&F;;JHo)qF5jpRRY$BY>RGmLrh1Npoex3ltuge`IzRe&p62UH;&i4{>3j*AGYi^=6G`D>K+qc zQRa?&wwDtee?B?G@#UE;3Yd;6C~NkngQ7QoJMil=CqnG#DT6RbSl3oon!RPL>$kGe z?+<|LN$f2Pr=9pxh4gF?+0PV}qd4ARk8G!@qs_{?ML%%x_c3gU5Jw?Jc0zkAM8k7! z33aA_a;&kd=q>+momI zBRBP$_Jr8#&9J*feDr1nQ5&IKeIj{E65e>s5%##UJGVIeRMlol)*^X1mAv}q%8p}G zVXAP$b@%5wY1I=IJ2_rQQn>(w*eqv|-|=5pMj=Cbf97Q_PJcGB{Svg*cyX+io$9G& z1gUS;8i*X79zJa%E2OuAMGFxI$ zJSUxXu^s4O@j-9%9bZ0E-v$%ulN-u@?Lr){5o{qIfqZ|L;2}!(dYMO5{9pYLRmUVk zo^M|&dmhC6KJG7$@x+rY;jd{uxxY$vebuu1Yn>;bMe+R_bV4Y^I3qN;_AiwCu^R}y zynTI>+xy4!{?1!ovzO)9e9vw8uD&?D4Trz~pO9ck2;Z*>(Z^o%vlrs81_W8@y#U~^ z2_v+GPO>vxe`~_9;N#c+HK9oG|2^QAW&HoTg6$V2D6L@T&&_Et(@vE8_+MQktpBsF zEqngk_L2SeBbeRovy)M%N#Nn}cOA*^p_^i8&TiN{E(s={bw;a0CkJSC?Cuw2d}S!% zF2Wa6CT4EWsN=-6sG2^6zUj>s)OT#ZNxvf=ymr(qY2~Ze&(G6xSw8zuBMp3dkg;Pz zc2>0IC7w6>&`mdj@|GcdDG2|U&1(IeA;$liC){LLAb)$J2)6PpZ($$fg3rLC=KyVO zSblVMkB#cAO7)n*0J;#@xH9j&2YtaOK|<<|Z_24-J+WuO*#xQSs~i5mcW>c&|7B-C zd9gH}^l5QT{=B80UYKb$M|X(Ueq03h2>)YXZZE+?Xu3gTuB*C0XYvqd2|LO%KJx5F zvDCiZM%08Hm77qjF$dx7U{?D3%jqjj*mNA@nylpEp~ywbDXd-#mH2_wnCU)zz)`7faS z<#L%;(y@C7!XAHJ(yg)N0_<*hS%I+_$N)+7q>`SU)Z7)b=g~9rchv`cA8Wz}`YN)W z17|IR`QOP#)Z9H7;kkO)4&4xUtoY~87{!{Xs65?!%8=o2TEKpiCw2b=KYc9dz{L3YtZqI4=D; z=+qY8w^#m&=r=;Rz?U2Lo#eig>C&&GXep%2n{wkY|Gru6YZF2K2Ii(CAFV7kj&cPn z@0%NW{O-#&ueuLyK9&s6A-Pf>z$}H+OxR*=9pkm3KP16MtJb`y)rw~`t)!1o=Qh53sH8ZFd%(QA5+&%Wpe zM)*9tq;FXK!R=KJG(>>eNk1^MUH?fX*qH%f+t zxRon|{aS)H@AVU)V)UkNduMxKExWgCWTj)96(l47WWJ*(uX4Vy>fJAx9876TMYKlg=t&V+HMIV^ioIgA0^wQz+iL9Yfq*$P z^2hs!;{wlWYsZr4#r|u#pfq+TB?Vs^nPfwnr`0s!l1&hYr6j!nP217C>|%&)5cNDZ zmYzDf@Jvi=K(}Onbs5oP$hp;ZR4M=GZC^geN7$Tl^y)Qcp~3^%9A}8RI|L>;fZWZl zIDWjTb)>Lp%2V!v`Qs|h{X4a;*oDVfWLMu3m72#tBv(5}4}&jTn*L&VvT$}Z!W;w( zlUbXUZO7NMQSO-jT=*c1jM5YQlEqp936ZBw-%zJm4=|)VMg4GFOf0IaNN(nm$5gg* z+dOO|ncSi6r1ZJy+}#%@CS3q9Pl$W0-xZAU0{c8IVgE_NEx*7XrJ!dC#A?0=v|nozso&)oX9-D=fHLq?ulw> zROb)y?5h!R(~4{5Hk!-suc=lWj-PIE1)qSgR3VG@^9>?O1)doy>m--FNJf1kF;x6>E#3ztGeUtp1 zy+@(aPdulXx9y2vdE46NXOZRoI3@FrpzrDwEPBXmkrr!%pPH3PYKyP=a&-ZVUv<~8 z>x0pEc!BxGsz?LZ$m6^jWh=%z zzx3i}rTa{hNlh9xgSzyBjyV8#n}(td62mpYZ!&`p-W57k;)8^BZ!UdJQuV>SL5X5< zuwiw&<>1&ORM7Ht1Aw9B&ENfIonE6Lene6<CtHv| zei~&&7o(8g zEQq9aLdn}0{9HlskC@?!`8Dv9JrbG{F@T)c&kMZrc6n|=2Zw!Am5z2lE6@wgh0H0S z&cWsIaJVH-(vr1KTManRop)L5#aNXiJ=cuUwwN%48}uGeL*TPIl3%X_BGoqzk#!Dy z0qF2af=cY~LOXT+rW&#_aoqdr*53JVDpMwfBNJ|?vC{Xx8NMI`yI3z^I6-$6I-{Pw zl9eki@8i6UNjtd$h4}gHAm~-gQm1uE&LIXOBeGGpd6^dx=|HC?_*2Xu96QThtZuEj zX}Ch37{~^{{%T{Fp3vM<=Efl3HkQrp-4i6uXs`vQ&zo32Lwd}8MbJEVFPi8%o>iCm z9AKy}wD<~3$D;z8L|#QiYETp7KLhiv)_WbcBcyfYupzy|9U^;knLJ&^? zu~gZCK@L#j0w5&h=t4@^K2pf9V**!Rt4Y|3t5QOpX&DuiES4=MD{Y4U0e%naPj7~I z7bTXT%U@Yj;JxzTiIVrRuR3qn)vfDSvfDh1SY!(+tqp`H-|>A~+4(=ZF20oLc%d!ARv2yB zelsV}uzqH3)q-p&PWmb?iE@N(hcCpZ$H_@GSPxWIbyBM{Fg%TtysJuB&t(_k3Y90QrM<`+l(+Vp5Y<&Gy?5e#p+v4Tsrb69ic; z38kzb35G%a3GSPAWC#D$^`9N9&i0~v^ENj=4$}Gr+dhqzm_tTENE5Up1q&BJ`2E%0w-_85$Okhk!4DgO>ClH|!vi zh)qTCrfsE0{X8a)8Z1Cly{0$A5LY&2G$VX2%@M`x9K~3x*5DrBBQvV@n#}2V3}VgG z9t7>K-Duj`P+z5)m7cI7k5fXR1d`Y5qw!r16-l}<6G9!u-#8Du&c-@4=Qn%CVM7Ng#78dX^mA>=E^=3BzKZD|+<%37&`;A&tUoAt8%_Qzwn< z<=k0$$BN1I;h4(l%xaitcMv_(^2oTQLk3@n?_zIu+i4W)GeDcH$e^s1rmttiZILUfe4X9&0bce`c8xnIwn}F!XGj&jO}z`5gXa z(<5&J4fUiF6+neQP7Q!=>gCj81luwj)b;3>Lu~eGYwI+mHmq9&60*lXU#Dl**8QlZ zG(m~Xin@N~;1z=;P)@b-MVTR#Q!4H00BEY7_NVjG;o1}W*Fg~>!ATwfW6YiM-DRFN zi4!6RvicHO{iq@y?u{B&-|&@8ce%}IR=6C9&dEqJljH)3`Y)X>Jv3^m8xqndy_=rQ zMkA@oOdO9`ckdC-7hmp{*JQHu3lCISot?G)TrHFq%L5)5wuY z;V^mv(vWh++u$HSf8^5jHG+=z?Bu7%JK|T2Zg?hw_7aNtVI}sYJ+X4?nhC8bm2wgX z>%a=c-XAwdg`y?a=MWiw(+D_NbjFW* zlc7Q)u{Rux1n(>Ab!J<=X+$C?j1D9pV%^JF3Od4n>+6p3%>1t$mu*iO@rNGk@hTtF z*RkA#ajm=@Sau z@wS8XYo7B$2e;C!zbv`u1)sICgk*Q6G;@V^ygh-VM+XxH&sPg?jCKzPW67mnGbk3Wk#vJi;VvG3M+INt`M`FW2v8$u#+0rae!#4Fvky!XeaLePhKc1kZXMNFc5%A?m2oeN zwa^@eaRxke1wF8IpfZF9$94-V55QerV700B+K8%37ruQJY_LuQ8|C z!bnJ}B5xfo4T4c;lWEni$(~VGl>eHY@GgIBEp=fMZz~k>@;4Q_lmvyX=5T1DFP#8$ zbQ@A)ZJ3ZLAk{v%%B#>?K4AKFYL&{P3t^$a^cDpCI2!fM20sZuhzXrxC|W;)*HSpA z?ZSwRZuFHWdX?*i7AHs7{tP8AmRTD~{YQ$miSB9lH1HN^okaQks#XyQqz`@Raqv^kWr{=k=@AJhT#cr7kYhpQ{y|-~wBnja!=GwL4tsqFWOTVh80AvzKe{|8 zrrySXf-7~li{Y4y-S|oLvy}jK-?>-zr926tc3WIk9;B3)W}zU-mEE;Mr+YEOtfJd z-yqd=h|r1Q7}HdL4Z!@pXrrc8*qQ1ywaUOwGH%arD^qmpgmAVwK=Ja0KAY%%b| zQ4%egaftu(gBWqEdkWc$>rdaGz-xk}yHdioK&0A>9Y@>NQM1|jZZO;*Vx8;nzmjR# zJ1JN`35{esE~g-DEmP94pFI1t0*-JNQCogMo{3j(%LBD^T=BAeeUp)DreMf=q{!R2 zj=Rh-E?*DLZX0s1S-U&BE^K1C5P15cvZyJ2wAiaI(EW~AmJ?b~fi(W2j`&|a+q0;s z^*neXTYo4&SHG@JgutFaeA7tiLp}%z0cOd1E!}O|=I;{|fXsiggOHF~(2yLyMEoeZ z!|Iw~HH(B(#AhLXY|6l67fYG1P_p=8lrlb>^*+9igP;Fv-P(7c|PE(56JkkMp4d(8yQS?GVTaD@$9mkDf1&1^?rNg-VRV_ zTgeXbfRBU{YZdcfe$PVWf4Fk?15;M7*L-2P!yaJ_nF!eH)ef8zzNS}>#UxP}T&IA* zVK4Zm4*5IodE=!dv?etJ`6(qb@Wd|_zB=(QPUh=z0AT$5a<+R?ytKNLX3)eAFPw&! z%1kO9vlCTkpGxbS$GZ~Mx6?i%hp+yVa{fINyA>kE{Fg%5KUvA&i(P*N4S&P@zY1Fa zZzhac`XTI|U~1+6y>@e@6;H>pG`GXH2n+hi7np7_?vOR}?{$+ugpvJb2&dg9&*c^M zk=*IXUoGF(DB%Ip-nE)%jCKz4f7IPhaPNAw)oP%*z&hjtr~EqA()0kO;GvsN{T~lx zcd04Nhg}*}dZsaCQ?3`;vM};`z@<{e8DQ(cDP?l}EikR@1muU)^RaTjiHO#o~p{ zY-{2P|Hh&$_&>z?9oJ`q#cA8ReZTAfM3Mc&+x?$b{=c^sZLZzd>@GrsMHR)f!<#09sk|xw{G9=klpOuu7`1)TpD@6ZkqV3o`1(N{n+WP z9rm9leS!;6=azcWKOQ+>C}yZ>XC+9e+i2dbH=iqYegq}mSCDZ+FPeg3E&0{eMH<43 z&Ohu|TZ=fc_O?IW1=bRWNG~e`VGFmxtd%p+;4SQ{5)Uy>|Mg~r_y62rx8CST(18*? zeOf;3RPblmX#H*WOEMh$hAYg%L*|~mJgesWO!I`qg>w2S>+o$Lm6b0uirPYEN&_!r zd&e97h7;ZD=Qy%H6`|C9beHqzcAvdAMcUd8ewB=F*R%D9f$5)4{(A7L$p0qGYh?*a zvtTU4^4`heqTQkyuJYe9q0u(!5f~1n0i)Ov7EI7BJ z(LG)E%)cvJOgT=Do9MVBQP#UFROzd%k>D}FPC-kgr-xLnl~dv9tf-As@L1SgWewY( zy_L=g{>?;LiJ0N6t6?E?#QP&>uulwyegNhygjBBmCKIZamu%^;0R);=R*Z}%?Dl%E zp(!?B5&2a%qKI_~9OknZeHka#bGG)bNB+ggMw=_}dtT%{za$L)ZxU*2CmkP4-z{40 zaYb5cqLQ;ehMm}zR%QT!&LU^tg%Nne?gi+aiuI|R@bY>Pxp@D+p49#~+`G*ujEpNS zxw5#|&F;mwOH^G*4`vAOe(GIglX72AkK!Z0$M{k*;aX-H99Be^L;p7kr%BiS`BpA9yGq2d`JSnGqx<4uT%%&$n8< z&r_^2fVimljb5X}*kw3b152hPSt}G!D%zq_Pm}~xHh~cxSF!$T;_fFo~|I-`WcyWIvBvM;ji*5=+;@gRZ zv5LZ0fM=?A3nF$J2=F_tU+G7f=t)&*NauuC6vz?jL25D8c64M(S^G8=_;LNW&CJK| zw7>C$r;0Y$#i_5n_rGLdKXTek5I>Dmp%r#Hr~ZY~JVSNE$Jj}zCFlwe)~+pGXz{-M_Vj&UbY9(#SM9QxXycv}`$8|jtXYyuPwXqk z%y^n5)c^ol#8)((o{|ea0-+vzk|m@*M0&ykUBMu8ELBNM&(YD!(Mu(}>Mj^}1**u$ zf4${E|1a}1m-?^f7bVMmd!sY>a6t3!2P;}{RXUA1&+qx|@QcLv25ZApekW^h<%J}a zG2c#`puLIg{c*}!s-2=2ifLKe+$JYfqd8E+~Wv#M0pi|T}nb~C3h zno_PQ9m}Gy8<6)9iWAI-l7Gjq6MxBqnFwbVc%8+6dAd^oNlR8caOcUS}U6j_(jvc1VL1xhr6CX3A*sA&;W4!P1hT${h5E6oa^)@tQEirfpmkakZg^(K`KPy*-ZX4Gm-__7x zZ|ixYxfjro`WZTy&l>uCk@NW0SzGV`?~5l}ZK97#Z-pPai=nx40Y<_CT-+17TN@Je zNGj}?>3E_IICg}GU5uKftyjRAgGLc>u%;hEN88?OiqnosTwv4Q>O-d@8TWz46Xd3|v;E|pZ#i1jlkpO{1Uw*HL)iQVj7gLqit3CrLt(*1D~o7M zY~{k**3ryTEz7$%{Xcwo+QG>MOcznE_boe1ZbxGjE1?z(MbsS}ot(eaQ6TSEc25fO zV^(e-pdcyR>TW~>JIMRh>-U=L4;MMS#y*M?c27~5};wM6tCqD!KFnu^oiXS##uM*Mq{nLq7HiLK-&_7WzQvYJ9I zTtp2l=Z0cGP4-31aM>+G3FZ3u1;$zXPq5c_ycSy+*=_y>`#TDJ?FM11{g!Lvh+uZ3 zZ#VvH@32kv!il{0q)dpk&vZ|}nzQugdo{w3_y})0uI{7EPn5?;BH2E9cui#MjSy=a zxwyRgrrwp7>|ACQy~>O}_Yql9Fsw0^BI-p<-0Jt8F67~ipZB#0xLJd3iZg?|LtJad zQYrMGFIAVD?3IhYss4=hYRE6dk5M^!5!1m9a?r^c<94;Gpn?2UY!1Od=aEKk2^2Px ze;4|*;?v653j0k2FStxV)s3f)aYu-39l&)$KJ42RP5&_ZktKEGarZGh)i~+qS zELN?2!m)Dd(8{#dwqZjwh}&E$O|sWN{4@FE1HzAwsx06#a=Fkcxvb;OK58lsHlDA# zfGzeO5TPGhcW9eY(Dp~&9a4R~`4&$;`7o&?d@1?cILAF^spi7^sh$c(3VC&tLNdcf zbQ@aeYjR#^F%!HkzEW!+FD4<^$_4BP>r8p-2|4}?HA+oFVx`lXlGP|MPklCxK3zlP z&KKV?(4IA?&;(zUl(6Z_VK4-CSA1}T#ZbE1&&Fehc8(MD6`8%O?V7jCTSWzq6I@r; zvlut%V@ZRxiv}DV9(?>s>^9l|hqm_&YjW$lg|W~?X(GKUDi(V0(ga1CD7}aj5eU6Q zg3=WP6zRRA^crdqP>>E%0)!$UEg=Mj5J(7|8-3pWZavT5*ZJ{%{14Z?)|zXsS;iRi zf3c%~J!q4bJUJIUzHuI{PRiK)=q2?KGveMf6Di7K>dS@;rk+Pu4IWb{7aZz}yBD^~ zuTRieI5C>cH`+~zdda!sni_YEtF~BOm)NfM-}i0V8t#@@BHeLS3feE0#%J#BX%`T| zSWnLV;HXorUL))-P%aTi!?XdOG_G~V<|UFBg9$qP&~7d%*BP_8o@e*#B?nTRn%QFI zzL4HwUCbWnC68Glw6fON!5G6mEC4Qg0x?6=4%=8;g&4|^=tF)VTpqQ{Qz*;lMA$7o zm~4BgDY0hPm-Y;kPKkfo!%j32SM;v$4?A-7DJzK8Vx>dw2S%>otUP$P?VCDR0R}bG zplVLdVCSL$&>CHCFa`@z&v*)?jrK~J6HJE8ssyrxnAGeqI?pfIKn}8w+zw=_iP6Pf zVy1>q#0@LLtdgH9a5)1|pDA|Yt-}#7%_51^p)YHqlHx6CgJMhc;Vyv7zd7ilu)1g+ zV>6exD;$Zi+qO=TsLSG^CQRKpI;0KH>6wiP^+&F>0`vmLQF(!yRK=+_!R3InBwf@$ zeZ$`n#yqE#Qh$~efTACAJKGpZMVtxB3Ai_Y%qEssikjF;hxyM`ISoRJq&I$alQI5K z>5Ln`5qqI-dkr2O0m2HbQFYcQbJV!K&(DnLpT?yw2|_9nkTp6t!5klnft0VC3{T7~ zza7i|%qJw>%w?((Z>X*J~o_<_gwZV^eM%ZfaI@Doh$g(?6ixyzlvylkWaCUc$bvTw9X=Jb^s#>-~et zLP%^Xl~Se6>i#HEi2?KFX^o!Z%Q7B^tR3?w1DK=vw`hwa7CN%pxqu1aDRiC@mdrGM zg=CNqcL45}DLp-FHPXj4r2`OQTl&HTliqXG3fdv_R8D#?T@0#trFg!xyNDz3tFlK? z4!{^l7~-TJT<7FzK*SACFm<>GFl2SMZW}PSb(1{W_{Zy!?59!uQAjTSCIgp*qu@+& z=Q&wJF3pY$np~by<-62ZX+mS&*BKjpU~U&D=MoGR>yaVXAL0Exc%_DRr|SH_2X@c+TJdk{8+I2DeY@1%q5f)_Q=QRmK+d~Jc8)a$clzU! z?+11nub#TRXAbk&Fi!W zlpV<}Z&&ySN%CaHHCX6P7F|61@*`mpcVEn~h!|KL8GP-0i(-xSyBz|+L@PW@F2TDj zUHYEk+##{jS9?!UKqPSVaF#BcV>D3PDNVB{BcrM9e0`gT1fGz~mb6u)9tdgic=v>Q zXDHT*3ww}Zqi89R@sdFNDA_~}Ag=asQ#2C;BVVf`*BeMaBm?-5-^yOc`QHPw(Aqd% z0d8IuWl5Jgv7_a-1Ey`Jpzln28f#A!2`oH3&!lbJ!a`#LuiX=m1lVtK))JlB%C#DE z{K1y^vd#2_p~BaRu0a9hta>!q*Cs<*&bX^o9~c`Sp%5(m)D_9y7EdLhssx%eY*qtW zk8sR`0kp|7T-a%Leb6)*o7X$WpH{jP2XY8HY1k*_$!rQn=V$r(Am5Cb-S2FQr8{qH+iy)gfDC#@A^EoIsJ9w3qap^9bxnJBrm6FERU#F(TR&L*8wm)-ezwzDVz>i&kwq7~|+V^OKGRish&RgAB=`9NWpoWG0z!|(yJo)0SNibqczz{WF8Mjb-y;wwf^>G*(cc5#`a+D zm7;a7TThk8H9Wxzs9*R-A8R&5*c?c+&bSF=LZ2R~#zwzgI!TfT$SwX>%Pb!LB@8#* zeRziQQ!J=THL9V_s|Vy&%#(B2Eq)o_6bUf7B!Q=9#M*o_@Lp@4HuJ6C1bLh0TkLf@ zjW?pF58n@I?}HMmhx~OrZ|V7JSla8nP>~u93zg`98U$JX{*{`Um z#^(cmIfv+T96xIidGdyNvf$x?sd$nqUqy}(Z!N&kYIW^N5BgRs4Z*oH+^Yx}+i}vTdJ4O7 zza42#E)xhI;^WP~xtLU|Usk~v@OgJyKI%X`g*Zw^k}LaUulr?KEUV_`=lDF<5TQrz zI5Dbz+nh74WhzvA@&xjgrQL1E6%FQR_s?}XWVhT@!TK(g8tZXVG5QTPH5QT?oE~rR ziFJ~3ngBCD4j4^;!MW}|$$oT;)_X6T^KP8YykzTkbI{ps%qQ158@y?w^T-n;yL-7F zb-1Sk9|GqZ?F8hQ6Z6u5u00$*rHFo_YTV|Cfgc>gVDkeFs-XUK1cIg&)1JA%-xY4$ zEZIP%)721*CkVo6)RvM{$W_3mV>l9_yMUVBZQy)WHTYWd)WV5DvV` zvmC=EU3m7y+Zl7^)l}mEs3O$wn~|>%57AjcW+czyCFT|by`r-N0BJ}y)>sR0T+wYF z@*iez`X4f>!-|{rNJePlc=zRZ4E^fc$AU!%P14|_oNqCrbKkz(ok>z;l!Iu_`8fo1 zmR|9>TP?8ev#$3UqOdcFF$M_E$)iKf13bMMp}Fb}=oM|{si8I)ZKN;n+i`{9&GWl- z?re5tYi`!Z1_Q9K&~|2br=_;zhGh+dhlaaCFwzbNT+%KQFdxVYpmsRd+}s!+C*sn# zB2E$wKnH&BHvCn70eb87?XSrNQqledlFK>Kt2#c*O|)7ck11WVmg^{jK?`wD@}+~A z7X|kjQ4YXqewwxMhPmZhG$?>K-!!_*TNuCqs{ZbbN|p&tuna)@gw7~kHhXnST=Svj zHxn>=hOXxA*ealI6yyVwiOFqizBM@)8k)N`9>66E?SCWp|(s~4F+CWdxI>>fche?!h^udeoYN~ z$t8C{*I;Cx?#4=0$wKqHil6qQt`~{?;N~GrjEg=}ILrA?LhS!x58qG~k@qj?fKu78 zCg_;P?;R$!+(9d0__x)%U8YQVvPwV6*6Rs&biP0~Vob$VWeY;xI4KBJ7wVXt#jK(; zjzxn~bcbqro}pF;UV9Tx)abL}-g$Cb`Xo(6K0)jXjAz@@B7A3=JeUysJD zE=#5Ya&$ipbqYAU-AP$NdAEzcwI-86Ndr?`;406t(ugI`P5YG=SX1antjuAkmdAso zr}|bSMqII-zL<}z=sN(V1?F83P21hooyLe#QKBE4`5$e%?r@G0e3b1V;a~wwAVUJ2 zi~i+`x>|kgFRh1;w)lQicz=Pu(zEwJR9)5UbNoytDo#9#{@0lAK^r%snO3$kGR^dK z&}y}Gp2EdD%rg(Iia9ga$}gydV~K+)_QJAHc6@8!sgTIpEsfo;6vQkDelD12TRD*b z(ZGFAoD#vdvQvGtWazR7exJK=Yf!vKAjFVmsk;|_k(Xh1{Bk90-`a?R<}r)d$=s*^ z)#_kmcif9?b#dsJNP%Y2K)50hkjV~ZQLntA;uvqg9w3<`blPA~L&%T3^iIoCs5|aW zveRU?4`k>%@7R;T)dci=h0Bm+cf6{GB8gL|G zzVweTETaUk&D4}gl7ib<>CFhAXU4+nif%nlFGxohAn>w?N;7M zP=7G5zuvJk=$O<$1+eITv~M;P0P#i5keE+;rxnx#9-rhQ4)5cuOcyf3$GU4;-KNrmgf>G$`n(k^ zgbZuDy|LqY9fJk=IwcU}#-q~4t&oK+?O?Fn$ffRq1)ngV3hCs*Bp>HwO+bRt6$`o9 zcvr${ST+c7%K)+kjmeZOIr1JN_!_;+xdS(~Yb=eCi zox6g1$kjI36&`{a#bpPU1dlaUHPIjvw1dt zC!l{*Svm1e_wsIN==BXk9zI}ImjJ8v*sPU4%0SN2&pqV}tj#ZY0#sUFgHKx@_&w?# z8{qM|_gOdP(>5!~?O)})zoqDZl`bJ`S(hdI!w9pv4z{e?Dd%&$bvS_9$E^1=@Q|+2 z@wfr+Q@AN|AEBrs5+vA#)M1{T z94CZmeyW5_YW|4{_#S2t{ZdT&r9)_q-8wZqMmF)i zce-TK>dn>DKShHcxh(vFX80=uAXnV^DG7XwGSKIk4E|rLT>n4xkN?in{@dz)>tX)) zbNC17#oxF2w}$V3wU2**c>I5}DN0nQpY}eB1Evlf&rP7mFAD~-goQ%BM_G;?0pN9^ zwm*8c|GUnSY|>z{|JIMM%^;$@ppWk2$`7b^vq0)|Pt(lzqG>a=umBST*qRVQhS6kn#9k9|CA``f%_zwk|=I zvd%|Arjcj!V~vAEuzSacSV2KKsO%tOyk<0!1*h-1T9D7K*LCYdktD^~Zk>yv8BIXH z6X7C1*RRDJcGwS}{8jq<6In1VSbu_@g|8?1bZE=d$b!-EfE{$!VUfTXJt^p*N`PUE ziT=yk4vE%oEf)GOP9YCK<^}tQXe!D&tP9Qys{!Y7f;xF^d2#}?ZKr`&b>X@9_Sh5=UIudC8R3Kx zY%n?_fPwz*NktPDZ&RZf_M}?Q00h%7TWMyv9l}WQg%=fJvd$ZnpMZgf=vh?^77kBnx(y?UqZoD@VYj{dw zq;Vqf*5_0j%7@{$<5(?}FapNRdi7^d@grr}Kl+c*Du-g*B(f7}0={FUw(hYz{|Gb(?;lT97-G(yrk}&Ho)|WYg zzZ&Zj6nV!!_a{>EAK`m{`G8opqnw~6rG3TkpUTq_?&=-fLsQLT6{+WN?zm(1Q-h_g zuN!w;GA0`nGjuAZj{FQU&Au^-ILt)jg#wIOmVs@V!m0#1v1zlJzv3Mn25y^bMB}TE zL_1#%-J7G*%3{Dd>UKlOdh3>3d|GAhei_Ix!7{%(SL1NBx(46KQe)}pS&2&)tvNjC z*#(~79iUzR8{|sN{yDc6z-Jlm=2KR_c^ac{mi_32va;8s)ms9mtyLH#cwdvTr{20- z*(yfynu?d16bcvZd>T)f-DyK1>rkJ-3qDF3DOg&`uiRS#wRB2a8_BdGDN7W4W06%TzPpI|2jC>|O_8 z)0+d-ZJS-W!qkCgdZXuS1(PwFp|Ypt&G__&LtWxQudZ!a67;q`oRLgiIP!r#RYu<%e8;mE})Nb4kFxWev_K9V!7T~X^Ov}oN7@^ z+Rc~SI6bE0{GnH%S4(9$QM+>(q()D zN790K5f5e1;7z2!h>CP(`TVKe!wucobHzAEk0yZ%6ccDu(w3aKPB27BC}ov+y+n@g z_#qTStn=Er=bn{FJuHFr6)s}KLX}hdyy}GJDte9w{U4V75N3u`cxrmyK z`U2%KAs|5@#Q-yKJQ~o71FuGcop1gij3M?4O>jVs7uE$)ko^6+46Zdl)1y>=7Msa5kmzX)qKnf9;Wd9 zG4v`d(>wZpTjnQB{^6;Glj6)g9VA||FPUa+SxnY$y-hhS$l75&=B*A&Q=7dE@M^~z z->hTo1U~yMkD#3IAS*&Iruu)hVnqAv8ISJzfx$s#g>$gz1>qlF7l^b8?}m~RVTb3+ zL6#BSpNZE;VN7E(pTK!Ms7cMjS)+DS*F>+@6XC zYdsJkAvycsZvVhjWo6*iR#BE%^VnV)0OiBBIUGmoxs0wHo(7jE&<$C)4)Noe zFT7<*$jYs&yHe~kn)O}S3lW0%B!j=~9iC21qmlOzBv@zxhb_Y-Q|ni5shFY2sN?Ifc%_H`rOL1E>u78+S{NxfSSz8pGhR2XMr5Xjyay*DXJ_Y`)Q$G{9& z0$|=fVVtbhIi&OCFc?r1jjdaQKOLm(eX-H|rMDdV)FUWZ0p1(AQtSA{6w@wzoli71 z_L>^_aUZP*%$`#fKCz-Ns*SI^njUbMBY+MV^i}@QMYH@hOi`t_QOr|2Vt3z`a)^n@ zy1Rd~Yfg07l7xg-<)1t{xevX=uVoRKIzL^6UWRg;3;IVakY9dFr^iT-tgsBTbP_b? zi)CB^&TI$k!+M?8tAC8=dX2&rbXuiT9kt$6JRF?-R7Lq35j$KyDmsy?QwKChK4Pz+ zJ<6heIlI9Ma<@rssatev>+fy|>kovqPH`4Iz-sdj3tyJPAqmKYo2TV0}l(@e+{k=R-Iw^KCBa(HV4u=YoO-3A}I9 z_*?l%ePi{3vX}E~#WPpVhK0Ig;~U&L&-M)pF-J2u*i}~w_D1&N#ITNip5QzSs58`} z;)>W;g5%33nlD^u^i-&jQn;ig6z4$SoNlD-T%Bf#daG8gEHdie{(|t7JK{Svp~WV8 z#!;GcfgK6*Q4^VGZE^?6e*uU-kG5LB;^pxNmEY!}_+*sVhHbXx*zQyC!=zCUKpD^j zooMsx=51^PPWF~qA_lf=lP^2K?54YqaA(R zyakX?QQ{}TcZTtSD|0U$#kcL-cI6pm3^6`4gzrt_7}Try@p&a51Iy_I@F?ogS=Jbg zhVNQ>D+4dt#&J8RG zwNinKxkIQ3m`xwW+WN!@1^~NyvHR+kOeRiAi+1t&rW>ELJBOtg%)TIb`f60cuhH9& zHol?0r8f*kbZb<=zZx~8_8QZhwas-1PmCdIW4r967}B6J*Jg|QT?=m;Hv9x}@2qnMi{Jce8 z{pK^%)LZI?-e%hCCtFJ`X?M07n!YKM^6PP6>z@*t>FP`y3nyI=Uv@%Dpq6Wj@B^r{ zc6|?4rFI^xGAn88rPJLOTNvn7dLvch;}U`!X+m#MO!xqmb}}S`KdE;%k*|$Y9b-E5 zAHrfAUGnBF%`d$#+Stw!{(>6skKLR8?$x|4?`0ZHM5ou-UL#6KMevhi!dT%rG`7w? zuljU~%n*^h*fkNr2vbu? z+@zQIECRKjVscF-M-IhHmn)Kd06qIfZAt$i^dO#cvNq=gsQuxTfnW=nRUZBTnLOc~ z1wJrf)u6R)3Y*ys^b53~4qsu#OH|Zrm`P5E$)Sb4X> zYR>qXTl#9;s2O=J35oOhU)8f$AB&DYjjo73QPe*&L|En<^&6KhMe7G zOe=cx`5}7p#Ze8~0rx6DXN+kfdxwM1!)CmzT8j>=vk($b5Nn;M%elbvZ$!e8oI^?W zX{b)vp~gzF_;olr8g(3=9*$e+5|zin@u8T1yW0GlBLc(G8{D zHVhZkA9S>=a827N!Pm(bA`p`l;o6|s{#hxj|&hd~FB?dB&z@B?IPn@QV5 z44a5?@dHNpG>~(Z;mz=gr~;qz2bL2}pT0KTgYvN*Hi}>G_MQqnXQ(la(|ZnFm&N2sI}Jr zEcW%6`uzELRQIZw$9?wm?^2zgGKT|^Nx zoNGYd94@||YqGa>F|LB6>XM%;m zdaz1hb3cm``!)j_ufb1UaMvZ{B4vn6wpVjH8NS zsWnKdPxdq6E5*f7N%qy5$EMYgjtp%RSjY*j(B9 zdaSaled+IR_cyv=4k=vWasQkWuuYZ-^g=z)a_|$U%3IeN?=6MR+cnP`2tp>P3V)bc z7McoaF$SiQjvWM-n682P^cZbSLq1K)@bNM}%foA=S!?18P$AhH{g3?}ztm^XqTLp9 zy{9)n&vZbUE4ubfkiq^!0$N<3MGll9SDd&J72}tn*_@W7N8wmj2gFyYaYlCi zr1kE+7MKI^Pc^wbk&z zB&oK#G^3I%u5P4NX3i>L4|=!QLD84;zO%j$fR>5NSb8uLa|ZGzo8$sosLrUtOHj;j zS*H4P%-B%q#wr>`#}2erR>MB3BB_79V}Ew=gt37y^~H;ewf|~g93dMSAswLILFM?_ zJD>TD9HNTcNAz!AgtngwNH=|pMM&i8DzC8iEIx(CnI`F1KFXkx=A+tP=%{iBt-Er# zQw^nj2$eLxH^DgAseoe1Q~2a7-UVkp6Z;L%4W*r*9D{<|3}R=3{cQ}j16u=wLYIWc z>hm+-tAMknEL}P3AQMe48;y@S8x>F;q`S$+*s#Z9&MF>aqN|;5gRlIIV*7;`_D~*z zrJ69EEBai}P5d(a>Cm0&q;;r9E9Z+5-*e-whzS`nF1s&Wo#=C@$-u+YUaY0on_zKW zsK414_cW9w`2Fr|{ zicyHzVP8vOzRqZ1s8jA41#?#NylirB;v|{#$U}jcCdblga7Yx!zi|Q9V9{RHO4U^U zs+=F$QH6>xJg1apWZq`O1dexfL`O2!jgva~-1BaichYnWENkD2oK7r^u2rzm6D;Fw zJY7tL{fPmZ8EI83%na~ZmIB#m|StE-AXzJAlVy3M`#65{q};~AzO$^8d0 zIKp96pQGnCcY5BUJYolm;cDf|g&|XFK*WHR(egRL7xGvyTPDkHY#p&Q8a2nB2AK%k zeqTAK*LD^zKVu=gmzk~>^lci!0z5ZWP8^-}&9Io&IzIK(YtWLYx!o75K9#Ma?2f?B znV~zu%Tvk3ls9p?0Yce}b_uc-v6$F1 z@nUh;v)u8T?fRNrJ8-|ssC?X7DDJCgcTbfIl-x_Bt9PJkm-UWuZl?6C--sKCE1@4m zlyR7kAULt45MPsm^+X0=j5HLW@2xy9QHQnNSQbaX+~Ioorq-BJO@swbM#kkOPBT}* z*$X@k;cJbN)n+$MbMy!}2N+tfFBUFew~ngZw6Yv3R-8$Wz!0~R^J5nGRwiBNB!J6_#5yeqU^vFWj+e*3n}Tw7gyWRiOK>@#h$P_IrJz3__dk$}n=F>!Ndw zowI_mS`c6N$0gKix=UK|lZt9g=b>KLRal`C`%fKl zmX!5EL^fQWn3pRSUbnQkI&Ep3FduDg6qClZnVb+I`BuT8t6F9^PJN8XPU=(nMvlwmrFy_?}@E^m&)8JAZkkt_%;IG zICN8y3Aqo1&mJmojWIr;bA1_}5ZoX@m-^STw@0yQ%}Pm$s4eJ< z3E{JhMkUF@36?nBjdPk85dHBB!^RsEoh7HmTy-)A4sNTe-uxpSWTW8}BA+t_b9q!; zvT!`S`g(kI29?TRb7*m-=;`}Y+Sex>FEb)tbz^mmm7&UO@K8sj#ICb-4%Yw~$LDx7 z$hTTUF|U)>LVdtc${3T(b2 z;l^AA@N(2~rls8`^x*00tAJTHqibwB(z^qw1sJ=6Ujnq|TR$gO1Q%XyZv>H)SHX+o~MpMn!wpld}_|@LhJRiDF)5wrsG>Db5(Z>Zb zG`x$$q&&m#HGxR2XSXdJ`9LZ`Z6%nDc1NJje~bE06yRS?3kpiF=Ml4^Zg)@dW5wSu zNgy3R<-A1u|W9|>}g+3lX$*=(7m!sRDL1sgBsNDJNtzuaB6;nT9@072j zEI|ZO_*FNqe4jV#!8cyHH54?$7=1^I``+ZRXet?f!j+**X8T1gSKAeI_+$kUU5WRy z#?d9yZ#huOO*HuOON60?dByq*3t6&@+^^>{d%8G){gwF>q4ZZH_jrFugJ}oyPXy1Q zPK%v*Ez~f4nsE%Y=@p=@3jwzZ+2@9e=33jB-pG}?x*GN272_e_+0f$deHisg_G&FX z;};gj1(;$kPoVpag2RXWu9cFa5)>H@#T@4%X~N=d}8R~ z^aHL@r&z?*5b4}=DZ7ktV@QEsctLVH+%+B}Yc?2YtN&i~^#GT-&0Lx!%&7tMzWf(q9--N=2ow?h2 z)#MiM8DpuAAGJhAdESM3D0cVxIi~*4b(bjAm+_aJmrg51@k+fR3zSYrecpzq-ZPs?t6ngOo7(VpDPvD-IRJZN?IEbcRYphLJ zuk9C*<=?+iN0}udo<3Zg-sS^O6iw8(5RO(jLz&v_$t*C?I# zygj>~XA5XBBrH)99N!3O6mptqTypXuJ~5?XV(1N%M@lqg$GGT}4_$c=uldBA}*Zg%?DMNj9KX^ zqJj{^L>-m++j{7gcj|?(3szf~)=w3bANo#Jm?*++I<8(0^`aVqt|E%Z%LFbZqo)1% zm4dVbEHqm|sn2x7sO;3>G_|%8pmX)Ofs{%}3W=q;MLVGbs?L?hZFW3r`usnA_OYyC zChZS;v7NWp?>Z^KCnzEqX-7J!t^B5?71oABd3J_XBxXiL zXcKjVS+&d!2OMZWGOJP+tr0KO-{BnH(Cq4d)%RXrVy4J!^uCNXE4H!sV7M>3^EwaS z_j+XWB|owuLHUZ!yhFEA#Dr|b=@dxpT1gf&rokDgDm%WAF4;{;6Ad9AOGEr$y~5{= zWWCo;DH{N7iLb^7TzK<6^>8V(TNlq7aXtKxNM|?ZU*-Ra?=N>$)HFISdKb`zg9lnrb(~OF*)I#oyeyF>g&Q;CN zV`90g(W_TpAna?QB&{WXmHA1O#Kf69#J5$i;W`AZ!2q1h8TTI76h;L=EdAO2KV8;I z{lsNGLBfm*HE6jM#SuD z)JhLWq?;CC(Xu+5QKba404`5E6^Sq&9?d(=i_-5BP^+Q(8A-YPy|dZCj%EXL#=oP& zZ&96E1}29BOkR>L03&qo_W6cMMW4qbo18xpS^+11A>NF>b@d>r#xqcMno~~%B#%~m zknz1RqTK^HGHNknoCzLLB3M3M1s#4Bf5VPg4Rhg~^`9hK3iiSf)}j;S+Rl80D_eJP zK`l02?{VPZW8j5vbC_Os*mHLPwsbsu(HH2VU&)2(-`h8L-A%_k-?8;6Ki0E(HrxIS zA;;L5%##CNytQS+d6_pKqki1)Zz=2G8}7Iz4afi;ll|oYNfEU%tMpR?E_Q^Aaij9e z4id3c#|wGJ^9v()EX0Hcgnpc#{|60b#r~fwe0&!;|4`ZKxOnv(<#>1Vk#%RP+?T$L zQBHmOx#Cl-?b4UqS=}y?Q<5617pEey4yn~eZD#}q$ zLKA9v=-Mb0lvkot$Ue2gck3a}Z~XQMPKDKE@u-MD#7 z_vO?lq+s^(g7@8>yI5BZd-)Chf|-K`!uQCEg;)#<2NOxF$K#8Z)q8P;#ck<=cL?A1 zE~DqsGeD%L`tjZWwXwhskdXXGUC5qSfhd?MktIz?!M9Bx@__f0%=?*ma=sa{Ty(E! zrVXL;+zBH^epO<3vhO$}?^AN8d^GHQYSZ3_J2gQ+{C%vLP06{wXmqkXY*doE{Lbe~ zeXs`^T3IB7m^pS?s+(LP7;^2MCYw=D=4u6h$MujUIi=<|j)uKoC%!i1!`G=M^WiBC-09}=Zf4%dDx`f&E|d3 zs?88=kDNYzEgWS|Zs`uzWg!d4*Y9VmvVEmR^T|BXZI#!{vcbD-B|opKmH@@sMu67d zSz@Gg5am+oA~W51uN`l%32fYQI5Ay`Q%mGs{04o66vFra`95TCR_l6ZcW$j0q`Eb-wXhWvonk z2L=W;J5Gbv>+a&^=VFdGUzG9o-+k`F?^0I(S8b}3_I7m}HCbewGKZOc$4R}&Tj5lp zQ_qwiWHuYPJi5f>OVLSx_bacd%d(Lbpk|-QgjAu^lRY!U@}R;+DyETJ=Rp>J2r9(O zfNhf)>_qntJoG~sTdJUi8zme(*)Ok^3L{hbkClbI4&1_|k%-DBt)uWCDj zDXja6JoI?O-5jj`=VAVDw;Cus{;e=HcZZLSuj)1rwHnt!u?k7!mUfna8Mn&mR`_@) zubb;dk7&vFK84iJr{acn_jnxC7o!ZdUr&pVnCYrV zIO<&xM22|iiat~rJZ+cTl#k@ec{I2>SV0h;*+j14>m+CNAFQ~N*l)G6wED=5#_8JY z+lfZp#s4%G+yiD(6K1}mvlPYvDDzpwAEI8jzxG%v8k2`?aD+NN`V5zt2vz z-ypGT(SrKNbd#}PCB`1tJmj%yd(BH809Z9YLe1PAE(B80IaA%CCLI87HJq>BL(=9(6nxB&hc)oLp z2~aFys!sZy=5P_#(QWn~-{snOF24Le==z&wkaC6A|2V*Uu!)R-lB{|2Qq@bXiue*u z-P?AVy>Kh@f&$y!SH@*d9<~U<-UAl|3a^z`gcSx6o(TUpbI$yJFx)2oaBpcs_r4^s zfvP+_IKrsHS@Q9g6-ZFJ*j}xv9S8lJWi_50g!=gp>qs8UqU}y%q2%Ol&bAp>_;VJf z&y?N%4G<26dk-X%sHQKkW(IHoNuQvo%gjCyv5-6(`IX;vyx34aF~^-ej0~n*a1;4D zOeT)Z!vujX19 zuD4#i>-nokrU4v!E6#wPt(OFnB=UxAQ@KUkf|~#+iMcNxHGs00i|8s4p|~~<-mWA( z&RWMcagK}UDrl|5p|a{{Dh0u&i}FC50~y=<9yH+fiNGy) zWgQ+2IgScGw$lCPwRm`Ix%l3t5oROehBsN%G&I-VNi3r>H-z1Y2~bWIwf2X69?kiW3deXrJSt))=jjVnpl-NF-{i{Ib3?gMq* zBJ;d^?Ohfl`^!71;kStB`K^86G2N#5g^OD>GXc{z)!={RZmJvySE>&}e+$q6;qx;p z6x2*}_9|?8Upp60vL7;YF+`p`yWoe->bw zB#Fu|8F129-z1Jqp2puODvMoH4~m)LN~E*Qlj+2Qza)XJUfIYgtF}@h{^FOb6{T!|?Xk^1hLYw#B+Nzm*~($y;-!Ub)Z8L4A2@@J&L(H=Lr z-8k!FL+^81|5D0HzVMqJObsdSSho6)&jesvyTY$;1ffyubUdU{XMLn{KSB8WxlfWh z_;O3R@R?;#@aIsfE(Ktq`u%UG^nYZ@79@kan3k!`{=^rM!kUK(oQ<#dp2!Tb3 zWr70eNmNNdMS09+FO71;wq386i2Mnyoe=+l{0g#2E?e2!=0%W($!}9IW?DL=r1hRPQ!;= zv>$uO=sUP080~1EziMJ{(RNv*iRZ?Yo zTsE-|a14Q2p;pm>^Ea?tJDW5WrUnuDEyXmQ5)V}g?0SmdI)rl5d1_G)zNtvX%)!=k zFmj5AR*KqYZDwr@_lL{dVe2V0j6e=XB1HCYiBAmxpmR~V{CqTCV5{WBc}->@R6l!h z<_*pJ&{c7sh;n|bM>&dt!`v!LKkC#H&y(M<8=p({PoppoUEXUN17W@Bv6{W0T}kY@ zBX*Ax6=fAKjov7{?pr#vhbobK^-ie)(*?B5h30b|-+IzlrxTsB8X(`zGSlC9UocaN z@gg}EJ7zxhC*Jh8ct%_KE;r9vp|jyKX3`G2YE-gDfwUhnf;2*&&sq1UFZR7qp{a3j z(Q4HSy^Qpjt=}5r2adjR;II(3ukfyu**O4)!E&o>VGg&NY=N&UvFx@+km=-tV~V-X z1Yf^wzoehDYp6PGXIQZ*KRH@tV!6MaaG2Htx!bl;51}Ll&x-<6woZ_IVE(5=Upc94 zz(Na1x=V1^wzkfOhgt*?k5PFZgTLdVfRrfCcnj028C@_p5oUS?Kvb*py!b()!c_da zg1?%b&Y#a2v#Fx&khP~9QDJ%CSwG0qQr?pe`qV{5`S{bL@Iub|Ekkrz6=wnarP+vL z>w#~?5UA1-;rxu?S!s>>Z9bKag-po4tPR5D%ffb+c>0+@pi!t|+F-|MIqw z4~Ma0&sV=q`7g3rR7fVCRd<@p==a@9DOpNfSAn^2;rB#uAG#S^wW|+n;I0arh4W0ZIVB zS^V!2{4e<~)#G`W|B__=FInifZTb`Te$}(HlvC;v_33iCmI8JtTmowZzK>W9bgz3q z|I)avbOTYqb`@7R(=#K2^4Y}(DREiJC1TT!#n~2YbGm>3G{$D^)?JP>FIBDzh}CkPq6@e{Ki#=CVnZDk>Z&W9D0&9av-;Yf%==Si z)9BwbF{2fL;`FBZ#w!FA(*cY>b_K{b+rU`LL8sl~)U&*Xl7P_pDwO zN1u&Vbal_Wc7v(uJ(L{l-hFu$uD#n()wKC=Rw7p-QXvkm-^~xRkdxdH2hT?=<95Qv z$>l#Lqlo9<@^lwN+aA|g8{em?Qk>TwY3uM+=tb?cL?BT&Yzg>gc=fBN`@Kfk!RHUq zH9Q1`QDm7@$q-GKfN7?``)PE+r$=}FxJC8x^?zx)E4dMH?D~J$y#mFAtw$5xga8aaA{`+#DUkxJW z`yDAl+V*E_+8iO+ImOTOs1_OXv@E z6S|+Sj>F?^KVk17CO2X`=&HH$ygzmqbv-{@r_b8YQ%NtHxcmSeuT4B?a2d7k_-@dZBa(TlS2)CjTl~FxVOz8H#|F5Uk?@U$ zmNNvq$`+)T`4c*N`la}O;=RV>`%fkK|6F{imOZon{FFKP_1yK4lhtzkPTUyw(G;?a zZPqg`Mu#xt*cO7U+AfhYr6R= zC8h(c>&=QJCKi{@uGPWQKjZ7RK;QT3@@$&Y&c9fFdAbmrQuj9dW(S#nrXC;aX<#S= zjLeK3-2!U(V7&L9FDjzJL(uX@{q>zfF82C{z8_0NjnA=zwefKFv?7pW<)*ZH(R|;;a_88A3`Q@XU~g0xRTpOF#x%G6FS+<1eKuG(d|M-qC^}1f9L|agg~qfdAQ7VX zAn2(56QX&Ipc_HTw+FTgOf04KU)-1ZoF^icua|RuTFMZ)!AzL$Nz(heD{cWGvAt3m zg{JusttjT0hg)|n(&fwoe<>A;Uy1Q{x&l9WAjZK;sHHiWvVBINZ{`V18Ya}>QIqp;$e>=f0%7wZ+L}|QpOt=J~(Cvlu_=NeYR1pkRG z4z~C#PPZF8KDNW8{}UZbBl^MLXVICuY}+NfS-l4~E%8s4pVH00HzyFWe&mCs+4U!C zb%eKz#Uv{8HkqGcIqF*p?n3M4yrB*?h>X;frqugSnl06qO*pc;8feaNrj$2(O6urr z=D4v49xLIEI*B$4DVI1U6H{|8n~67O$bF2^Iw9Ol_bcai?hcc%O;a}96CoJ<`r|gT zj=8k-g9^W0tW!-bUxpPI-`5Lmm`p=KQn<%U@=xMck3{Gk@uI&~$uVdSEeGhj95Z2W zXI(R?4Jwx^qu8dE+1O5QM;0d$m}OrS-F32@*cy3-fwB%j@e z4wi|*UEbA95PzQ=@s%_Be$YTF4JDUe`NfSllNxvVuW~L4<`ERTpPyD`;BE=)I3HLx zYrGU16eD!w98!Ej3H0@AyR5~V3uty^q9hf*V;8b+cN>n|=MS z_h7Q>g-%)Zzin|WR}3ztN3h748kv(#_b2x5Vj7K9<%2Mc6a?Cus|p430mQ9#xhp}p z;F@u+;42f7<9Tj^*=7UPEl!evJ5QJS73FE9ftrO26gvaB50SBL!o%R|q=?XiYR>vw zN{7KQIKpI#N|C|q6^t4 zSWR!FwF#gil`m;oNvgY*ar3>RepB2CbrJ?Gi6owPheA!%$I8BoSE5QNjTYL$ z-O(NHUv&q31$kEaZz-ikqBIj})8a~LVN5YYT7hw4T~(M8UOZC5>g-{w=DLggtF#6R zM#_8YJ3pt_OU>o$#W5R*+dKZ^A@@rc(pH$gFi4yYYSn_Kq`bQ4Fr(OE1Uj=I&M`k$ zmiK=5y;YL4!@d;*PP1pv0?9s1mbp5|PL;zOOx6}ig|Y0LCcWUc8Ic{GSx0drxB3Ho z6oxJmRk6>Q*rYwy>Zi;!-yd-eD~NJ>*eCmJ?c1}iG`htx;+^YSDZEqpckHcEV`1ow z8V}DeQXK69{lg-m=R4gX#_{~yI=8W{-W;}?L&ookUOU3~Ch|Cd4z2pF-{5JJCr%Oc z{czY$lctsP;qP`U)RXl+5?I58JW^x7!8<9#lY%>GTTlA1l{6e1H~|;Qd zI-_5Q9}gbT+jrp1iRm_}Vfwh%gv^%1OKz(2-!LhSzLDd=k)W~A{gKXJ+yXq@=r^PQ z4o+9z3ewKMcn<>CU;eJ3uV>pt()z^00aN>Ng(j>cp6GLg?6ac*W!5cr$X>5^m}?!f zb5Z%=q}riItzx$9!R(Mho8q}zsbGVZ^A7M_Pi*Z8<*=giXRqp4Vyqp zAxIc;X^SORRG7FcK4k^{HFbuGAHc==G%7t`x^TEu2z=`Q_ZK+p#j z+&p5o#}??VipI@$_ePxk?&knMLHuN=EBxkj4}Z;s)Iy^xy2+l-<_v|DiZ*<{a!7;x`xNmrTUM0C;X#}eTB7N z0<>=18z8zM0mBovJA-=jsh1A5F`e|qwy<-u@a1Ozk9xW5g8`m!EW`rx$*=Tz*PZdKW`>nSJG0P_I!LSpR{mmkbct;=( zNHlx^0lcDrdGEHoZBKkS?cjBN5#h?@@Ni&gKPnuy)(yEVlY?L{IssLK1Dk+3;ojEc zZxX^)EQ3&NcrCjg%|DE!xG6QCqKJyGK0j|ZAic(@{w z07T8>maxNo-|u?@d#3YuAfE^WptXWmI?xCDDn3jNY`u_TmVmO1QhR))x$aGJu@&?dMdG<}*k*^;xh23Lv%s;M|H)IC|k=bpytQ_3-v`SunDY`VsU=vr4LNn;&U9>0r-k7R(bQ z!MKDlKxp2Wyel>JB7W?s2Ydkz2ON@bXGH(H^T63``WJ7?y|+>#Kcm0;inx!dH^LHS zWSX9VM4nXP1qZ#?bXw_iwi*7I3f|Zvy18n7p^R%M;`qKDiI)Ah=_<_f7ngpoVq5u z<7=qMAFCQx)6yB$dy)ovq6#LyH$Is7#A)>ztl*o4OnQyC_Gn%OcY8k{r^bt7pR){2 zM%>ysV%fNl-eW$Cszv_bMba0VC0)mydcUtbXb`)jay>Y1tg@K&mZ~}+I=N@$-G@9lpjjC(U5HS!dUm{ep*%O#Y-9yWH@FnUh&dTx?OhJk?i?P+Vhg4s9S?kppEQ_l| z4hFxSaa@q1+fGgews!l_eAi(CvE^VZ=`IkNs&9KsQ43^%_yG(K8@m?^-A~hY-!Z0IY$GxG<2-=E*SjFeIK#wXu zswe_#KCtHbq|rRoNPp|y7I&o8RxkOMyZ!^(TD2;t>TEvHX{QRCZkdH-y~!TP4Gz41 zGsWIkS2O>Of{)UaMT(3l;Zjz+nw!JL1!yL<{kESZ$v3otx$4H2Aq^A)j;se$!LwLx zw=I+wgtO{K$qm_EM0J4*Al>xcnR{tf8Y2=}d54b9=G^w8_H*jN3tF9ayE>FTy{sJV zJ_cQ3Rr};SzyFf*d39(vEg~avASi7OF^QDMiQQzpT9g zayNVb7unf{ozm>w4vg%mojo^yC?b8EMV%qbzUobef+10~h_p;1GoOYupDj`RuFn6t z;nD*S+lH?eX>Y>SCAJn|e1T>R(P3S(Tu4@R_GD{5RRTI@jfg}WkH(u!?w|Yy4S~XS z-}U;#%LKC~D{4$09ySy5a6g@Q{{r_OmR5#*DecyR`d0B}R)62)hyE8{JSxevYx8mV zp*K`_T1BJ7oVpAh4>0P-SXegN1DZS@?(WTC z)~|Wi?Wo3u0b1y7ALjnpjL2Qw*N%W-ws`H4?~{ z!->+Ws!47x%4V-xV{m)LN8~(B%fSnX!0h}kHnXPPO_heUDSn|jP?RFkTa%-ilmVG; z7N`SSiI&40sBF2^}%{EU`Cbge%B@QCBI zjing1XDcXJ^m)yv`KNAz@;Y+-?DT=$fGd*@;4$BCZq6S!H3VjF-&SIq!M`9fY(0ke z6!jbFbKCEGv}#Vers7Sry=0z`uic@{;@Z=`ygo;v0yN~)^W`0$(^?Ved(c5rynauEp4FmNon*0!VcM%Fi2 zJgfX**feJsNqarJb0ui9)#|I<9R6#jqtW-iLIIZfYw4tE%s_`jx%B?9AoDNMH?*rs zjZ9bCE8nZ6yjRQiEFbcn?uF~KFMOJ(L~lTITZPd>xYw8%dyW$W%_hf|-NCA7NiSG= z40p%&4KU9v5V9Lj(tb_F{PT7gRtM~^j2FvT*{x>-RcflV`5`EZn0y*ielJTtxoO+< z!#x@b_@n^^@c^PMKdqG6_VY*0`ZwQt=R=AGax4ey+)LC~jqhi={b$Ntx@jIRar0d- zXATTsvMdj_YbDnLeA0lKxJkxx17~c>D{Be?VW`~y>P6MwBzqiaZ{Iej@ZqKPC@q>WRp5@=gpx{1grwFk zL=V>RoEF^I{n6y!2SHA^Q=A8y$|>YrMhGwx73LoxP*;FYwvG2@FD%TW?R85mZ&RN( zI`h+$D#7lk%TIm_vcX9qof9V@!&MED+6t!ehDve8cbZ;(cq-|p6gR@Dh$DL9JOi%# zdRk$EGg3|GS;rLW-p1q`KZOa5k5rqV96U7GD)b$2V9>sn66#6{Wj*#rJB~x;yT#_> zryHy_&%K{eeHFYrTXf~h+2fTr7MyO}_YLh=C{=X31QF;vR(EDW=-}pC?l=#e`2i8v zx@|O)1S2gXe4McES4^m4^XU2Z?)*oGMP>pRG@jLu{=piwGz;*!0QS#j?Lja8nCPfD zxGk1voP+<_IES+U7WiWTguK-?{wH zcJ4ovpc|%eR!`4Dqq8_7+;gfqO>pNJmq&66`I~M0+yDGG^Z9>2@(36F39MUed zbY%CHv(ve9zN2h&3HYzb@p;65EEtbKmqz3v%5DNiv{+dmVF8cVfUz>rP^3rhhi4*N z$Gv1^Ac$!IE{`vWVR_SV=gykxGk>7_YecoR?TvgKxe;16|Li(VV@Yg$)1QcKo$+Ja z4DTA3HGb&y)IS;G~N1vFcN2 zk*&<4kY8_@@ZjGpxZ_@zFT9>Oiu|Iz)9*s!ovHpxG3Y0i7)Z*VB+y4bBR?dp}rVOWn7-ITrPHLXYv z#3%qNQKQZme9|vDc~}MHdDOK|;EW$J*g=<0AS`TP?s>=mXA6&wGRoNl_*M+r=@&!r zW47mg)X`DuF1q#>9uY4+%|rP22z4LZICsMnLln0#-A3y`S;6E$+}Bu(5w5dQ^nDX* zUi-z-#H9<0b*1R%`g%nii#2ZnmX#QbO^}xdxLUzAD~E=D=vG9zw{qE3z+(Y?md+Zy;9|g5%HeaoTH#AGNWCis zif{4z(FVX60d2c=O|EjP?vj|zs!Y{a{kO_Wx~}Ny z)m*55H$1E>(Ehg3?ew&yi}~tttiMB($cOp3W#~7SJUK6OZ&dij3_?SH7FKLz|Aqh= z{P!RA&xtW2*MnH?N=YBH?gMQ}&&v&~t+!ii<^90O>+3!7usr2)*s(x5*UmG<(6GAY zaI@^0VyB0h^+#`3wg^7{WTc`7SZ|=m8L)ZF;PWIYl(hYJtOzTJLjGYd)B ztaKp@_tyVrw;+E8}^oH2p&3hL1qBRs%_|p!HNI2z5%iz(2Fx9wXAAab>ZwUvncZK zYx0kCjREimqZMRwbni@kHf+KTl>BPldG@SHv0v-hFfu`eSoNOM$mRz&ur-He zHWmI0XxO0aUv}>-{7+2J<^9eV-hgl@VTzN|5zzzID6NWIQ~Lu}Kjm%J3H zEf;y8mG=u6n|D7w(cbkJN|k@V^Vx7{=wm2+V^nO@_j3Hf#ef~D(!chs$TJmSIqOYs z1RPZF-4Po{vXM6pT$@QDVfZ&*=CLGQJB&@)w7f+|M&5_;{WssQA2xc$P|coqcT1iv zHa^9g{rHNBvorFXWBC&ji`QUT9gWR*s&dFUU&5!A(kQc@CzjRqu+zvI?YbUj){dU8 zcFV5TpbBwf5DbOiOu}>*&e{#uEvy&dalt4YDXFiJ*>WpsumDOw=jR+h0Ag5KH#_*+ z9+*+1BIv0*nSrlBuKTdJNJm*!D8=s;71KojE7~k(l_tvFN_da=qc*+0+aF)uxPC*q zEPMd`*SU5sbDgMOdev73SvEhcB^_POyDb7g=;mEIHe|lfv+rKNMM0x(kBh~Qq3<0# zsckEg@czt*lkJBWJQ_t`iC+2^F(IIL?rB?fj-bc&3$Ch@_hlF4dj{mk6(mxUTrE@E zJg*$fyEIlUbDV;FMrx&y;_3-OM-ZV$zA?fOjmjVj^;xJ26_x$nDar%rgw7C<$?0;O z0a`x2+mjRe-)f0fWPR;BCg}D&p}$pESN8Q&S?0kqRM@Q_Zyj#TS9Ui(GQ0wxEXJGP zpq8t44cnD}ZCUKH(2yob(mnjRZlMT1!h2lvkFjSnadrb=Fp*0uGeCWIGcbbZV~f3a z;mt{2>Oyio_ZP~!#n(UHLQ+pJ_~@4_hI5ZH)J8?;x{t~}YKg45t)rizyj6s6=diAv ztbN&@W!*jaYc&j?oISE8en~kwQ;S&-Q2bE1+*Ew=Yq!`Z@BmB;*!UiDj1kV^Xru&z z$v(u6BkFX^vZP2@U%XR)jRTGr# z4K_E9v0tL8qHJ2+PDRcAO>K%dqstXuIVEImJm}~?g36$9?lj0OqAhGp-5(prcsS$* zHrJYY844|@BQm@Ul!Z>ebz`rt%;?$^ZYQf~L(+1}&NqZO5*S8pH-C_xe6u$nL+~ z6!%lBDQ2yL>Jdn^_gisTC0JF_WH-}-?4BeCSvoZ)3il&u*AOjZH2u8hce-(7osRB?=hywmZnW7n6A6 z{haTNl)3SoZ?l2NuK48xRP1s}hqZ?WrndbicK`UXZjf#3F1v$?5+oO+PpV1Y^27(Z z@viPIMVV`$C}cFvtfcg|aRXgX`vLwD_zQb16PVNp4rS|B#QlU#cKq-gak56Z+ci24 z=+sT*b>#co)Si(*HdII>5vNS?DkV=I#ftO$8u5Qp{lvBiG2$OYxZ164Zjqz^3V}iVBY!l}?Xe&aHP#9NJw{y=P zA(=(-(|qTS0cqaUFvuU&2K?)bAGQP~Tp$K{sJmS4Y{bZM-)jwHs{V%v#{=I9Ko~=pi^hBIkgs+He_wjjA|pw8 z&Mtdx83R?SZIkD_?8F2J2qM`7xQGozpTCB%B% zjvWp~EDOS#kdZN{0enx|W={o9DDlza($k=Xp8Axr}4r0FS z)7R^C%&YqmSZ>8`H09{Hom>2!oElgNnm*&^d;2v+V(_zHy-~x6ie-p&jD_7^>U{i; znbZ}cCm|HwP}=*%D_&a;YmlkJUQfMSmPK`H4=iGJx$W7r)ZbQC@$|eYFC5iez4Ysn zss4c3jRT6MeSQeZ?@urMOyw6i!@igkTBYMr+0i@fo@eFh7{^F)I8iFNSTZqwxqftO zsmE<0iT)(P!4j!!!|y)_yUv8uMTr;9htGaIt46ChRNXNkv0yAQ)vhCk#nkB^ieFfx z&CIPGVE1w?ZB%+vv?w*iv&e37H%QtS zYVJbPF!J%}a=W-zqgmXW5&#I4ijoDwz)OC%z(kOx?Pe+PeiM2W& zFsl2|(3x%ZN6%DfMho^cust8LN83YT((z7!CTEjual<7-p66Q{1FA5aNKfce83TYJdqAY1WQ-%}%IH+T;5)c1(})*%jae zK98q&mSV<2!4e&&lE|99fbz-zv+|Lx7|$5{X4=5}Y$WtGXZBTtm>GPB1(PGqlCl zV#5kC?K0MP7;MRH=q?t7P!Fu4jCTH#8}q$ zJCjMl`RY+*dQ`}_j+W8K3T4kdYSDL3pa;YUHEi?`j;IQf6VQo`9<#K{#mKd;xRB-E zpY}E>HRjpzD6e8o(Y>S(;-YZ(W~W+A5t|-54t;Gwmab{voK|ropsoz_prp6L%>(Z|8NBlhu|8SVYDvsz0TkCqo{wW1*QSz@@5_VW{ zI`&%0#;!L?Auk^lK-bdU?MF*Z_87E#@Vb}t7Ep7!XjcQM zA8(j?7L$?sc_Gqy$cv9CF>fxWcPQhOy2sG*=vdP`9h0Z?q&y}*D5#HK(mW$kp|4p{ zVitTfR1y>1;OaiZ4wkH9275_AXd>v2)Ef|SIr|6;Wvc3JP-qa`iFjJYJ(cA4v#})B z+b&PnatAbH9nss3yIVk{<~5hCfh@_`Z9RaUhYe+GmZC5Pis~ylV#ap6GlMKk zYH~Yd`QKk079Viyj5qMcug%0QOJyC(uz@(@0*OV9!KJF&y4f>T-&P1JWAtQLZ5;6= z)Br$><8SKNwfP1wq?!FH%HEKt{>@-*_YE3OBefxKCs1pnir)E@A304`dZ#hDhlX70 zP^e=ux2xdDOdCc}U{n= z@Rb++K;}anxPIHttMXz){{Q)91x!=j8Zb7j{*JZb_q36fs%>`1L$i&CHJ44F8L5Q> z?OuChfD5WC!rCt4i7W38Kk&CZ{9nQAp78(skqw#n!?*eWXkXOURu}R^mS&-h6Y=ne z*p|Shwvgoor6$;Za3j@|SxE(*?aRvZV%!MezVxQ-g9ZgmhWUA_N;fvW7vrC9C;wFP zCVgvGOJctw{~%F*uSjBmCg`NwNc<#obP}aWyB?_of8|tbvJ9>KK zuTE1GJ~w*RaPA1m+i>o+QqT*4c@QwEFMMTkh$$PnjBhGC0ae*D%nO;%{D}UAZ|eL6 zaj15ijEwuMgnr+Uo`jpL%<6;$lzN#t3g#ufNb)Cg^!3nLmb|VjmH$QD)Y3c_&<_on zrgn)kyQP07V2cjF04auTxy<(o?rP{SAMVt{N;E$NWg{oB{9BYv$t%P!VsFpJyQT;! zeMdi%UeOKeJZe424LFubAUZ!#_%b^GvmJW3MaKOIM$lG3SL(>ymsl0`K08#8e|_r# zyE9#Nv)c2N{p4OI4$nv!c8bZcA({(+s?jbz7677~;_Y5rN_)L;Iq*U_Mk>lZ(|FC- zt_Y-5Hqv-1eqM_-Bgha!Pt>g4gAusqQ;9WA%5}W_rp8g=@)o$wOCVK$_&-EpYx%8% zyTngG6J&&*hi|$fAi9recjA}_2mRsFuJ-Vd8Jg0w!<+Y+ul0%Li)~ibe|FRh`_>m& zwrSQ$NhKR_q~1!!ieKY<_xHQ4!y%xhq` z?KKGr$yxmvg8|BebvtYG@=vYr#o}|N2BLDaec1c#oMTL;IQ=8cs)GwB7WNTRlZZIy zm=OQ#eT3^lEe?h!z+PoxfJ4e#p~pW<(cGpf7`c|!r3ih;>TAN!x30faWFbpQ`8vmB zzlZ&%^+86Bseey@>z3*xk6uQ(KBcoOt^J?)KeI^E+UcDj?2x^iK{~_j6rZxTQ0(}!BKF3+-i}NMUoFuG zg)#A}m^I&|c-Dk(QW>T@Vb*Vya0+SS@xqOek35Q~yV(&Mv?RV?qzh*$m0@GRThFNM zKcAR?8SDFDQGr)ztNYILa=T_Sm_}}Ps51OlDpIK9tiHgF+hCXD*{Gnwh}|oyw6EP_ zxqKe-UD^D;&cyMPn?+2YL@iFqpS|VcBw!crU*uNFa{CxF@NkNjI*-f(m3$U&@tahYVJ?yE$getf~yE_I>1vr+Xwa}F*;o%n9dFv=B zn8~fh)Fq{0o(vNM+4cz=^*>SG_4D7gGbfA0`8^fz8iRVEND8#(clMmz6?-@ybl$+IRT<9@UA8u!^?HDFA zpMKjo=*G3PM^!T7Z8Q;|nhTa{7EB1*+h59?@$C$1r`a`JBUS*psXF+%9uJ4U7{4?| zuUOPr&6^J04D=}1eWHy1!fs5+f}3W^cr=@{g(rvC31&Y=W9`=&G`ula|WEk z4esv1O2A7!dvk+|Y4hJaFs;XRkS5y4!;GDJ3TMB!)|O@X1Q~Wttuiv$`fonm+h|Dv_Ri2FU2 z+wY)UFQljYNR=!{($~BFhn_0@6n6v3fAR}%&dt$XV}3@yBv+7n5vSJ#zGJuPADmxb z_myp>V8>%E5;>UPUa%~?y0PJt@0S{zD4vzz4c*|yWdCH%asl07$aN&cp5Ck!2>yN{ z?2DLt0%LX4Gk%ru^XH}F_#A%=J90axTfig1HZHg49`WYoU5)a5$vw4$*q2c{KfRUo zgw62}z~5Cb;bWD9^)+Or!@g*Ej;R)9X=Uc2&m>JR6!jUq#*3XMq+1OuPVSyw)Hx^K z^%)2wo|BW8Qz3ReQ3L*yvu8oy(sQNUvS-$N!DjR6%&Nj!5KMPXQV{)sX&d=W8FW0^ggu=a|%N4Q6*E`LKgV$Q(L4r|ozi{>NiLci@NrO&U4ooPSQC=_(? zZiDyr$R$2(jKhjug09&$eLX@&NzY=oja_L(>%HHy=YOt4+XF42Mjfpf9mK|2o#367 zecEO{E!ppz^-N`Q_6b-1!ZHMfhjV&SYwG%joorwX`Z2lfZ#R>$weU1uQ=jjyb6)Sh zKy&fenw2qQ^Ca?5&%#pPcS{HZxkbK8-o0O1vJ}lhGk_fV$!;w7%MC^!mpMr>K)|G^ zT8NUVI!Y5du$lCei_UKnB1{hlEmH8}q=q24`^OI$FbNdQEuL&bNuc6}aJ!wBtH5)u zj8de(7xx_&twe#61?#p`o1%y%mjRGT0;0O`IZ(u>)uHt6AXniBl)ZNSKYz%` zkoGo~4KJED^6D>3?m)Wu$>D22xYOVDPERin89-g9qnG-Y>*;{^jS9d+X@X6H;o;el z7}$C*h2Oo*s}G`?K@-uP-T%3V-pTn>gu&fKBLzlCmf?`L|0n3JbnR(L9SJ zwDTkLyKPNflNX1Ao^(37V`UlBWDH-oVbrE3ilWwAD1(7KG}}$H06Y$mr~ISJC3OL? zn}<7j+2HD*D>6ARjp3}UrN$}9`i=bqt7}e^2B;ux3%kQ@%nylLGh)Qz*9%yS#HGg0 zTw@U?f$ERakYwpGri|N?4uZKRj9%;zG;vfWCn|+d(Uu~_Nw2NNKVl&2!Itz%3MJa> z5%LoO*|Ovhe9>BcG4w!UE9aA&*CQ`(SIDFg_#{}TQF=+r!n z4L4?lPU&(I1DXP+LZS6jkioi{Vm#B9QShRViDj%+NuzKrSsip*T@!bWy9AfQa2FV? zFV{fqUWg>pWwjcZ0OB%*2m_fV*BK|jF9mui7`PIb37);-OVa3hnj$QZH7$+qo)m|3 zNt2?H74`G2oK_p0v5BMow2E2-h~woIz-4d;134t_XyzwM6Z}rHl6o|-pAUP5MaF^L z`5y(o_``sD3ET6cM2iM{!vYehOHX4=u3>$hK;cAx%bX@DxQi$T$nV}Rj`}^- z(iSP7$%vi|FfHE+tXB;f`sEdzHpwdqsOxn?Q#vB#s`DR zX04*2v{@n|0RDsFFn27p(&&LXZ$r3fh%}PhP4&|{zLJMhfi-ryad8GqbNhg0r!Qww zLXK!JkAYhx^o{O7sI6-uOOe@ywXJk8S{O@(`U*rf(>xgin2C>od~-ir9}s_uXnsvI zW~DHdVH|J96htb1KSD1t;@VHx-L>znO$RA^T494N%UC?AmDL3Ahgz`r29UzT^FztB z!Z%5a1k=|+<0@)RAlOeEM#S{JgY+&?!*ehdKvFFa;@j!55#IBx+cSednKvsf*_9=H z%Jj<>Vz&K>BSI%1fw?&cSH0y0clyDX*c4K`H%$&*30A$k=4-#r^y90m5=Y1~Iaok| z0W~S)e-z4bVf+aAX?#GuyH{Tw3CL?>yxo1mvN3$9MGk!7a_G0{-B0RSD~R2R*zfGs zdKOI`?OVycWH=X1{q%646O9*vnry7|6g;ly8vze9fR5jomq4wTExpI)&weq{hHMX> z;?$gjuvh1dDM4(~BqMYZ5fG1J56cg!#*s}^#yBy($oRr?GmgtmI^rl$)_r^iXdMBv zKYzdx={yD8lk*9@N{92z7nvL#omYB#dqX+;dlo-$5s22;)BQR4v=Xd&oI8}dbW&dp zSSNs1gJfq(e&SctoiT*%>J6HHg#oYGGAN4OU|q+xV6gv~G%_RgngO|a*UI`Z)e(_=6x{M&GHP5r z=!LReYG8-Oh=FSVoh%3-nWF%N2hTwB{6W9~ALRqw+^y!koWv1_2a3g} zaHvy$)Mj(e$zuoaYaElk&A;XIkA6jtBT4#FFf*ZF8&!(Cb&}EEG(Fl(ko#;elb{ZV zYDv-nYlsGbkr1={PN;AEeVldkxxE|q#w7do(PXWW9 zPAh;o>(4o4CEQ$cTBr;fAl$S(YHX5!|D(!CHRzP>;e%4A+JI@IP32eqH%WU9SN^>E zTx}8~)yQZ)$Cy_PdOZC$;U>dm&FK>$Vzu7~A7z7NZ%68!B4U9e>nvqCzKPoMI!78; zm~fN4H?Pw8({qI0viRk@8bBeR0uouvR7p&WF9AZqCKwA5JYW zh6I7F=-CM2K989n(pQn&;jv*N9&r>=*vV+Hs@nSJ;^So}WM4LtLwGF5_Oro}ZdsG7g$SL)HaV z4~!v(#_#jqyF^v?ZMzKv4qrcaLi%@}PFkPe(8;!lS+JOfmkkXZlZpks%Q+MveUF$c zT`&E@voVCRoax6Yr`s_cxmyC6Nq$YP z@!Mjj;ULRi(~;M~j*d{k`3fxyAPfc;`h@{@&Y0)U|LYC|_EHf<=6nrkRod>GiJbHk zg73j}_lI$YUK&%HB)ky1o(T^^k3d097Ij3TtDA{f*G;l6u7 zjK@A$GlfM>L8%b*;Z8{C1e9t9k;brxsgcGH7l5s^Cvb-;ESEmXQ>nfdh6M(&SBVe# zUaI~)x`*yFwT=WYf;i^8+E?tj2H*t>H(e~s=HM>YG(_t?}Cftmd;(mPv#`99!SOC#)qg8^I@jDy*b-9GPJ(s%Ua_z zndYT$A7pZ(=bqmC)byZ9ij~IeMYrZ0kD0TTIE9`%*YcVXKQ`g2{-ZY@h~S1 z8wQP%ffDta1C}4kY@rlkN2kS*^HCrNQSB-%B*A?rKVM(e`QgP2Ww9bPm9EuF?QKJG>5Cfvjra5I?u z#7#y-K-|wzMs(#3RroX@Am1TIzM&)WWKu`XR9#aO3Pg7Sv=%t5OENNSm%A;>4|#qt z4gmP$x%4-Tyk3F*g``)&3HIop(pu|6$G$OtybqMpyMNR=ubUx*7+<>yJD}HDhMK29 zl4NA?LP4yGvALVOApQrVIRw@hh}vUWF%PU%95NqlJZz-y_bUHzGbTSM8BgL9QggZ z!wZt1MmOU|OL1u0Jlu3_Y&E?0y{13WI%utNewdBvdRP>6x(~?mf2;b_ZKtqbB$%z( zG6|(w;z^vZPD{soF<>%mH2_GpoedYTA+`SK#?Q5~LJ%{Er$;!lqvpUde_P{uWR2&H zxy!WMsf4QdMrvzlQ;6=ivo;}lAuO-;{<>v=GWH1uMs1p$6vc~?$mpN3ic=94x2q%0 zgGeA|Zxh91&zm52G;sH^}Vtro(;_C zZ&Ldu;B$vLU+ZA~P}Zaui(AuYT&&hiy#D0r5y=F#YW*O}j;FSXI#Y>WMJX8+yR_-3 z^l47LZE1I+!*`Fflq(rixs%B!J72~dtjiRS6UVVF?XwLFiC79_5x^`cs93>@X0Z8Z z^9$^n<%-Q*dkk=^-AX&TrPv&*ar65*OC_6`)$p}_aAVJ<$UIrlJ@>(x`Yi5so zFp?n5tBkAw)F5>28iWoMiiZUyT50_kJu4c>Ne**@Ge29)8+2=O z^iWmfoGGmx1Dcc7NZ5?!O5=s}(YmqVN^UYX=D>1dgy1aE@l@s;>LAgtX%-EZPRu2b z1-Y%S=r+RXfj5w-2BlhD4u;53uOj!KZ3UwI)q1X#u_Yau%4FihBCl3*B4f<2+s`g8 ztsmrt^adQj=JG%Jt~XJ0#t5lpASRB76G@XKHl|#r|3?I+1@XJ@L(OlYp`7`6z_&`B z%}sgux%S8MY7k>S9#5K*$}Us^|JpZ^U;FmwmKD~;oMRbHv|h4zp0&ptQ6kVCoL|Gr z`8ahkMS|^r_Pl40nR?C4<))f>&lBq1?G9!qHOphM1iK)|4UYWv!)i>o3^J!);9h6tTkpKd@ki?N>QjXLdUJm& zS-%7&udUe4civutvP`p|x!bp&kVy-Hd3FVfQ;WB6`rXPN?JzmJN>bVhiJ~X@F>Bv} zGRSurt6HCQ(=LZZ3@c?@8+7QXSLYQd`aTel3p?uTW)xRfpHJ_((?|5%1r2c{xDR_& zn+>a?w*OS_sV(&1?*__123q^a2gixeemYl-c5Q#vu$$Ss4^!xlbPcJNKOJMIwA+Pk zPYk~_32aiJ$)D0m6|ey$yw7iH!!Z?C=pwMxT@`}iG}ffkl8*ZmG=+0UHL3-|MfV&5 zzyqXyC1L)OneGN%=^T|ciz5=YS5)lL@if9^^bh*Il^z<*ub-I1sIzx`%5TAc9g$eD za2uQ~pO8l~g353CPdOD!1Kk+G0{VFLl0t{38*Y7Kbotz%nzW6WzUXInn;xZ8X!`<4 zl>Dij=w?8W9>jKrB&WwNxu@~Ahwxoh=b&VorSEjJjZoEZqQyqV2bW#wW;edOdZ-$N_Vli8-4BSmBr-^}gp>>S;sC7!cH`csT7 zNfKp)7p9<-YBP!DVM`JqUEofNgCWkK5a_2bo0 zL_aoBZF=T1s^#RNr#qK|YvvKwi3`^FAsJA67_qC==bezeP8@N21jV5wi^RJMc*Fx-j6kV3>u#=NDi(a4I3&3q!>kxJW zYUKjzoZims&;L?c%?$noR(;2MP?80_I!y2;|1I^(Zb9wW&(_Pq+z+-#O%P)1Pd#3TdF1Zwo`Ch-i^L;~8xoKYC%CS7JC{lM6lQpUL00{^K%s!L zARMmqeOD&;64a)sFDd58$LrAzDNdeJqKRYX;jT@mgI=MgzdZ0Zha?RGjnj5F>rBx@ zfF~R-4eM8|&$uB|Q2tV`2l2LuQhti6*(A6RMuGJsE*AKm9+D(IWNX#|FK~J3{wvI_ zLiVJiuL<4kv#&oOkkqFer3n)^yXvo7$kQ1R|7 z>ex5G6xX7H_=bmk_q+mw92}5y?UiOoyhEkjGEAv)vySR{O-8LCR4%-pZP741uHB zRH$y|X091qd@OnJey3Pwl%Q7UajWMo65JDxU;8+GJ%0ML71cM$!Y zzLHMN(*iy%+O81&$>8_IjBx-yi1#k~w8IC2=XjALWWYO@u=hF;2b<3s#Qi0n2qO9s#|hDDfgyY#>a7G?0r} z&1X1?2O60(tL%K%%k6z}wR#k$>$tnusDDEXnGRIn{{XB1^~?h=gn1vPCdx)vdPcNr zhKI~es1U97^k9oLUS5A)WTkzbPrNM%r|dng763+n_OtU_FP~EEbngr8G2sB3Rg^a> z``_V?zkT;jfrP_{L(XbzhlD7JiDmBA({n~{Y?hyF`w@NrO(6_tK$GxInZ;Mq);~s8 zd5S0tghNZY-+Qn}clfFHf^eCJK1D5FquU+pJ>3(nT zCq*tjCd5ovAr21ZjEAoKk8AnWx!P5_ua0K5>k%VpZ@;*9(W>0TOJ?Gx*RuS|80hiT z+2Mgi_SrF0J6rsEDL0im2rbki)t8Q3#nihoy+#RM1?@qXA_Ke_gQeJdH7Eb|=ApC< zr(wp(cEm8%zmFK+(tGr>uNQ+sj#7F zQ0)Rtx#e}G1jfdVBgc^CYp64f6NxbitCWy69+pqM)p2gzaVT^6vfy0&g2>zhsKOJu zAkDNx$Va3xpz2JWPkF7>orkcbjMRg(8CD$*YnIXQ_N0#qNG`~kep8$5D;Mziu=-}| zV~oyt&d{|E*tEGJ64pL&fK)CYGpNY0IMD~++mPU(Lakj1Lts9PUVD;B;#o+nlOOpgTLt*GSSuzswKvChfZ2Us|eTSCycgt(k zCYFOAU2}B+*Oh#Qyrz+OaOyeUXe@ClP%vULu)A=LF(J}#ckXS|znlDj%Nq@om8SBI zevJ)_(yrU2T(Lxe!mM9$o7K#}7|Z8j1cX&Ddz;hf<;m0;(6eN(-tr!vGMql7sXv)k zF}CVz?KD(>TVt8@{8inIVlX#+7HhP9&prQ+umC|Nv~e9hNf6mf(#K>aDMJcL3nvJT z0B({dWiD#Wg4v2HKpsO2GOghY?O-~)FrJ)7P94=npzE_Mo$6(K<{0}49bK>@#TdHc zv|3v~ae-7o#|7_+9d14w9P&gqfoSf|A`l^{6od_hnRPyr9;{gF2P#Tx<4 z8X!8`22j%v@mn$;Xa_+SLn)4H#?s9**2vzjadHen=~H9fgs%g|!>h@7OiP588qyS& zf346Jo&}5V96MWe%jy<@xJ-=YkNFQzCv{?wgTC*kZZ%=F4q@8+ED@uWMN876y z{iTjy`~5{@a+Mxdzao_-4X>rlGUusog<_+G`SsJ@Zs2@6cy{nH>oK9tvuA3+D@J26 ze-xs&+6%&-SiuRGF;vQQU*Y(UOm6yj59~~=){>h6pVbqA+to}h52W2N_8WPBF%?d4 zaT|FOm+E)5;be{y^U00pWu>4oM74b7#cUBHZCA*Fljo)yk9o-8b} z(~BA=li!`mAdXoMvzF#8V<@t1MlAK&!R2Y}>^kTvL-Ga5ZKQ&fR3a-~#{tzI^>8cvM5kTn|T;8LLGe;(cBSY2X~ z76o#-@otzg^@-<2rihy1YNB~FXFx1lR5-u;Ep!C@+ev= zT8Z>p@qVPKR+wCxdyZ*Q)L;oz5F1v1aE>xTs+EnGFIBRjb=KC{2VhIgrhEaZMU`7Z zCWoy+k{g116}2I^6~f!8<%Z`JQgxrTPIZy4O)~}#6Ft)c1`!|n3t268ca1_iuaQ6c zuQ+6vyeirU*tZP^+}2wTzvzX9T>a(cLEh30Hq43>YSl?c?^BKz>mUkuySTBhilc*omgBG6z} z*VhTD@xg=H1*6feQJrm$&h@`pxd*(;%UWn}f!G`5Ik@Nudkd7f$9GN!cG%af5mp)R zFms(`h2esst6v{+TL~9-hID4;=5LIoIr1`fUqJ`%3B+caf9rWIr*<^i3M@-kY^qGA zzE!QQlMFth@bxLj`*jQ#4$T)t-B<{mkCU^lb{h}R9LFPD6rNI9zdF3EQmev+xvbB7 z=Q1#+0i~-K9Bf{V-84nv(azIgc)ECJfHpdtP;C+ka4Kgov*zt-v(S_o3yT_wTVo)s zy}f4J5{7WUt_uj)j{$yp|BH^MIS9J9)4ojBji=49x^~T6mpp&`wqWMy{d!bR-S*Li8`%F4N8t%l?WRYJ^zq$kw32*r`92Ee)D z4z!>1Eta_2upZRDR_oi6No%V9dHU;b%b2bydmU->@?!bf7_=h$s~2e^45pbw=7;g3pgCZoA5&lm z9RmmRO#7=8;KKaZdl(0%c=4gOJGASR!XYj;HxbPxsYtDO>cNGQIh+*n1vl#5-ckp1v;cEizrb89j?IxZrHvr$HF}$XBP4+H1nX@3+>pWLDKD!sFag7 zaf!Kxh_?aH@Yd0wH<#qba*e#A!$=Y{$METGx2HamlU*vtZXcSO9UeJNNM2`@d=)cq znyqNp?7IJgagDWOCO+PVrtU>0w##&aL&u7AYT{akXWCm!r55o#oM`GCyvPz% z0LdYC&pClB7|CZk@D?zeIlnoqB&i087Io^D9bFF`(GjkWoJN+vo-v}fnwuq;G+5Z+ ztwEH8A|sN2-TFO$8`-%W$|@L*vLT$N>Gzk9G0LuqGG|r!94_T^BV!y&GCryikR)6v z$yl`Cycv3vK2wbYDk&nD0rcgB-bH}FlK{MU=II^92lI~sMOY+!k>>x6oMoH2s!J(T zE!h-Eb*ozVvQi43sI4U*B^jAiX`O8@+sNJn_5&fwuAU7R#4#Yj26t@A-v$MEJlDt5m~|Q7 zifkK+GyrFA&S3y@RQ;^* zDCdV=Nxo(p^{6TIF=8h(X6aM!yZ1XPYz*jtlx*lj!Ij;ls_`nay}aSekM@| z#^JGP1n$H0ck;>iW32*v)MR#NX?WAz)4j$i6GQ$iYlz;vB&fFbQUk~J#}5ui21M5$ z+T9RICVY4RbXXdZ!b7eez8|~y4P`2$1`fRRzYzAm>t``KHg@cd(3Zo4VMM77)?yUU z!ri5W=Y4bFI+9HP+@+;6c5%Di)jIc4gwImCcaH?Wg;-{MF>rOCH7cE!g#J#1_%7mk zW$};2!6ygN4xT)yl=CRTVEBksCs^zm&GO!{LTLi7G&VNlXaK|lXI52G=UbSn?etr| z=pfQ>&}wME&%(Al+gECy9Xotxo zQ&q~+CxH8nYBXQ51~sQ^j1#l4k|=lV(t%IlG#as=BRV|5t?OG!;g2}WcRg*a*U_Fn zezTr_VudLA+oK1}LNSiXAx~DH@^A2b?&+5!did0ZV94c9FRGLXaR``465A!Hq;I0R$K5BD@v>~`myiwSwM_6D|YR+t@ypisMCLXX!=TnZ9}a&bnXEyWqCBcSny zK}~=;N~NLv!w+uC6hA9=$8kgf{*!AL;cZj4?RA{v7Z>Q-1B#qFKD~FIe~zE55TnV5 z1Me`5Je!*PrlWF`)C`=Tv@~`DKA2RocSO~v5X-;^6*}k=xUVDyw7Gb8jui42K z7I3`36F3v^;M?a*_yobzo~&df`=dVHxwIq`$xrU~ue|X3hDyTNgHumFM!)sBDUgy@ znVqwfGi%j+(+l7HEE$fZ7EPV+am*hHUYBDN0RE@cgt?g5Zyt8HkEqCRHq6=aJrkpxF_)r80amA1XqibFar>@*X!sVeJIM;a)e zQ0Wu9ccf`%o`Ukf3&m14;Xi#U@;&Us|-C`VQnaT%WW2h$BWt1(J3|R{GlKV5( zY}{(g0?Jo2w)wh^P`rNf2>5tkS#1yCD(JL!AIKL{5{oB%ZrxTWOo$m!DdL~GHK|s$ z$vT}@Srs^E%|_)YwO-6=JRe;Ev#lxL{L7Yjr!lcDXUfJ=VYDmfY;$l99b09a10>Un zkfNY5BZdueu83-En>T!GRy*<)=bCT3LSmk!edrA|hB@vT*R!T>vX3K#fG zy==bG;fNHmr%QE8bWO(-?XSkgh7ONu(Iq&hi9*c*Wka}&IR!$Mt}J2mvbo69Vk|5dvg9nBWWX@6^)S&QT0p^`E_W^Y=*S>OVy~KfHIu_aG$e z&q2svz4z~j0}7;n2yey2Z%h4=vo8Ig5Ka3}AsUdpng5ZyYL)-pv;j55)f+t5rOhvH zRH(yZ-ve7Wf4Gx>I`cn-J#Ho6!=C-Xd;IsX=bwbWkplf+6CBS$UPjEQRy9j<)Q&Ex zNUlb$pv-B)?c?kKPYpMKjVu~P31-jN!J<;#$pNF}L)eV6J#gq%3{4!>5KtsbszW)Z zr!KL-To|=+ZTDzE%->tbqI@Me4*d7)n^e;HX3kf#;NrZdv)xRTcUMO3aUA!G>tS&dV1-rv$8PkjV)|}Q z)%Fi~Ki;owWNZC_-*3H9;vb^w1o%W!dce!r_hrMC$Bl%ia(1p*Tj{+NToXJd{yIZi zEE9uP_%>2elJm#^`kU59OofH4kC6`%v7O}C`-ImG;?%uqdr)>Es>^S5OK!P^KubSF z?UFOmLOiGQU1{--9!j0XZ*P!Mfaa;#`u?it+?rE+##Vha=hvMQRM@LM=Lg
TYvf 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œ^ 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

oJBg_cb_m- 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 by8*MGb38qF{ii$7&RCwZJ^AYR