├── .bandit ├── .env.example ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── integration-tests.yml │ ├── mcp-integration.yml │ ├── publish-docs.yml │ ├── publish.yml │ └── security.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .python-version ├── CLAUDE.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.cn.md ├── README.md ├── SECURITY.md ├── assets ├── assets │ └── icon.png ├── compie-wide.png ├── compie.png ├── demo.gif ├── init.gif ├── logo.png ├── plugins.gif ├── speedrun.gif └── star.gif ├── docker └── docs │ ├── Dockerfile-docs │ └── nginx.conf ├── docs ├── advanced │ ├── push-notifications.md │ └── streaming.md ├── agentup-development │ ├── configuration.md │ ├── customization.md │ ├── index.md │ ├── release.md │ └── template-system.md ├── getting-started │ ├── ai-agent.md │ ├── core-concepts.md │ ├── first-agent.md │ ├── index.md │ ├── installation.md │ └── iterative-agent.md ├── images │ ├── compie-docs.png │ ├── compie.png │ ├── favicon.ico │ ├── icon.png │ └── next.png ├── index.md ├── integrations │ └── crewai.md ├── mcp │ └── mcp-integration.md ├── middleware │ ├── a2a-protocol.md │ ├── cache-management.md │ ├── index.md │ ├── logging.md │ ├── rate-limiting.md │ └── state-management.md ├── plugin-development │ ├── ai-functions.md │ ├── avatar-generation.md │ ├── cli-reference.md │ ├── development.md │ ├── getting-started.md │ ├── index.md │ ├── logging.md │ ├── plugin-system-prompts.md │ ├── plugins-as-tools-explanation.md │ ├── scopes-and-security.md │ └── testing.md ├── reference │ └── configuration.md ├── security │ ├── api-keys.md │ ├── github_oauth2_setup.md │ ├── index.md │ ├── jwt-tokens.md │ ├── oauth2-provider-configuration.md │ ├── oauth2-tokens.md │ └── scope-based-authorization.md └── stylesheets │ └── extra.css ├── mkdocs.yml ├── pyproject.toml ├── pytest.ini ├── scripts ├── load_test.sh ├── mcp-stream-client.py ├── mcp │ ├── README.md │ └── weather_server.py ├── mcp_usage_example.py ├── placeholder_orchestrator.py ├── release.py ├── simple_mcp_test.py ├── test_rate_limit.sh ├── test_retry.sh ├── test_streaming.py ├── test_streaming.sh └── webhook_listener.py ├── src ├── __init__.py └── agent │ ├── __init__.py │ ├── a2a │ ├── __init__.py │ └── agentcard.py │ ├── api │ ├── __init__.py │ ├── app.py │ ├── rate_limiting.py │ ├── request_logging.py │ ├── routes.py │ └── streaming.py │ ├── capabilities │ ├── __init__.py │ └── manager.py │ ├── cli │ ├── __init__.py │ ├── __main__.py │ ├── cli_utils.py │ ├── commands │ │ ├── __init__.py │ │ ├── deploy.py │ │ ├── init.py │ │ ├── init_agent.py │ │ ├── mcp.py │ │ ├── plugin.py │ │ ├── plugin_info.py │ │ ├── plugin_init.py │ │ ├── plugin_manage.py │ │ ├── run.py │ │ └── validate.py │ ├── main.py │ └── style.py │ ├── config │ ├── __init__.py │ ├── a2a.py │ ├── constants.py │ ├── intent.py │ ├── loader.py │ ├── logging.py │ ├── model.py │ ├── plugin_resolver.py │ ├── settings.py │ └── yaml_source.py │ ├── core │ ├── __init__.py │ ├── base.py │ ├── dispatcher.py │ ├── executor.py │ ├── function_executor.py │ ├── memory_integration.py │ ├── model.py │ ├── models │ │ ├── __init__.py │ │ ├── configuration.py │ │ ├── iteration.py │ │ └── memory.py │ └── strategies │ │ ├── __init__.py │ │ ├── iterative.py │ │ └── reactive.py │ ├── generator.py │ ├── integrations │ ├── README.md │ ├── __init__.py │ ├── config.py │ ├── crewai │ │ ├── __init__.py │ │ ├── a2a_client.py │ │ ├── agentup_tool.py │ │ ├── discovery.py │ │ └── models.py │ └── examples │ │ ├── __init__.py │ │ ├── basic_crew.py │ │ ├── multi_agent_flow.py │ │ └── streaming_example.py │ ├── llm_providers │ ├── __init__.py │ ├── anthropic.py │ ├── base.py │ ├── model.py │ ├── ollama.py │ └── openai.py │ ├── mcp_support │ ├── __init__.py │ ├── mcp_client.py │ ├── mcp_http_server.py │ ├── mcp_integration.py │ ├── mcp_server.py │ └── model.py │ ├── middleware │ ├── __init__.py │ ├── implementation.py │ └── model.py │ ├── plugins │ ├── __init__.py │ ├── adapter.py │ ├── base.py │ ├── decorators.py │ ├── example_plugin.py │ ├── integration.py │ ├── manager.py │ ├── models.py │ └── registry.py │ ├── push │ ├── __init__.py │ ├── notifier.py │ └── types.py │ ├── security │ ├── __init__.py │ ├── audit_logger.py │ ├── authenticators │ │ ├── __init__.py │ │ ├── api_key.py │ │ ├── base.py │ │ ├── bearer.py │ │ └── oauth2.py │ ├── base.py │ ├── context.py │ ├── decorators.py │ ├── exceptions.py │ ├── manager.py │ ├── model.py │ ├── scope_service.py │ ├── unified_auth.py │ ├── utils.py │ ├── validators.py │ └── weak.txt │ ├── services │ ├── __init__.py │ ├── agent_registration.py │ ├── base.py │ ├── bootstrap.py │ ├── builtin_capabilities.py │ ├── config.py │ ├── llm │ │ ├── __init__.py │ │ └── manager.py │ ├── mcp.py │ ├── mcp_config_mapper.py │ ├── mcp_package_installer.py │ ├── mcp_registry.py │ ├── middleware.py │ ├── model.py │ ├── multimodal.py │ ├── push.py │ ├── registry.py │ ├── security.py │ └── state.py │ ├── state │ ├── __init__.py │ ├── context.py │ ├── conversation.py │ ├── decorators.py │ └── model.py │ ├── templates │ ├── .env.j2 │ ├── .gitignore.j2 │ ├── Dockerfile.j2 │ ├── README.md.j2 │ ├── __init__.py │ ├── config │ │ └── agentup.yml.j2 │ ├── docker-compose.yml.j2 │ ├── helm │ │ ├── Chart.yaml.j2 │ │ ├── templates │ │ │ ├── _helpers.tpl.j2 │ │ │ ├── deployment.yaml.j2 │ │ │ └── service.yaml.j2 │ │ └── values.yaml.j2 │ ├── plugins │ │ ├── .cursor │ │ │ └── rules │ │ │ │ └── agentup_plugin.mdc.j2 │ │ ├── .github │ │ │ ├── dependabot.yml.j2 │ │ │ └── workflows │ │ │ │ ├── ci.yml.j2 │ │ │ │ └── security.yml.j2 │ │ ├── .gitignore.j2 │ │ ├── CLAUDE.md.j2 │ │ ├── Makefile.j2 │ │ ├── README.md.j2 │ │ ├── __init__.py.j2 │ │ ├── plugin.py.j2 │ │ ├── pyproject.toml.j2 │ │ ├── static │ │ │ └── logo.png │ │ └── test_plugin.py.j2 │ ├── pyproject.toml.j2 │ └── system_prompt.txt.j2 │ ├── types.py │ └── utils │ ├── __init__.py │ ├── config_sync.py │ ├── git_utils.py │ ├── helpers.py │ ├── mcp_demo_weather_server.py │ ├── messages.py │ ├── multimodal.py │ ├── validation.py │ └── version.py ├── tests ├── __init__.py ├── conftest.py ├── fixtures │ └── __init__.py ├── integration │ ├── __init__.py │ ├── configs │ │ ├── mcp_sse_config.yml │ │ ├── mcp_stdio_config.yml │ │ └── mcp_streamable_config.yml │ ├── conftest.py │ ├── mocks │ │ ├── __init__.py │ │ └── mock_llm_provider.py │ ├── run_mcp_tests.sh │ ├── test_mcp_integration.py │ └── utils │ │ ├── __init__.py │ │ └── mcp_test_utils.py ├── test_agent_registration.py ├── test_ai_function_integration.py ├── test_cli │ ├── __init__.py │ ├── test_create_agent.py │ └── test_plugin_list.py ├── test_config_models.py ├── test_core │ ├── __init__.py │ ├── test_api.py │ ├── test_generator.py │ ├── test_handlers.py │ ├── test_llm_anthropic.py │ ├── test_llm_base.py │ ├── test_llm_ollama.py │ ├── test_llm_openai.py │ ├── test_middleware.py │ ├── test_services.py │ └── test_streaming.py ├── test_core_models.py ├── test_foundation.py ├── test_llm_providers_models.py ├── test_mcp_client_caching.py ├── test_mcp_models.py ├── test_middleware_models.py ├── test_plugin_base.py ├── test_plugin_decorators.py ├── test_plugin_integration.py ├── test_plugin_registry.py ├── test_plugins_models.py ├── test_scope_hierarchy.py ├── test_security_models.py ├── test_services_models.py ├── test_state │ ├── __init__.py │ ├── test_auto_application.py │ ├── test_context_backends.py │ └── test_decorators.py ├── test_state_models.py ├── test_types.py ├── test_validation_framework.py ├── test_version_management.py ├── thirdparty │ ├── __init__.py │ ├── test_a2a_client.py │ ├── test_agentup_tool.py │ └── test_discovery.py └── utils │ ├── __init__.py │ ├── mock_services.py │ ├── plugin_testing.py │ └── test_helpers.py └── uv.lock /.bandit: -------------------------------------------------------------------------------- 1 | # .bandit - Bandit configuration file 2 | # https://bandit.readthedocs.io/en/latest/config.html 3 | 4 | [bandit] 5 | # Skip test files and virtual environments 6 | exclude_dirs: 7 | - /tests/ 8 | - /.venv/ 9 | - /venv/ 10 | - /build/ 11 | - /dist/ 12 | - /.git/ 13 | - /__pycache__/ 14 | - /.pytest_cache/ 15 | - /htmlcov/ 16 | - /test-agents/ 17 | - /test-render/ 18 | - /examples/ 19 | 20 | # Tests to skip 21 | skips: 22 | - B101 # assert_used - We use asserts in our code for validation 23 | - B601 # paramiko_calls - False positives on parameter names 24 | - B602 # subprocess_popen_with_shell_equals_true - Used safely in integration tests 25 | - B110 # try_except_pass - We use try/except for ignore cases 26 | # Report only issues with high confidence 27 | confidence: HIGH 28 | 29 | # Report issues of medium severity and above 30 | severity: MEDIUM -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | API_KEY=alice-secret-key 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: RedDotRocket 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Create a report to help us improve AgentUp 4 | title: "[Bug]: " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 🐛 Bug Description 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | --- 15 | 16 | ## ✅ Steps to Reproduce 17 | 18 | Steps to reproduce the behavior: 19 | 20 | 1. Go to '...' 21 | 2. Run command '...' 22 | 3. See error 23 | 24 | --- 25 | 26 | ## 📄 Relevant `agentup.yml` Section 27 | 28 | Paste the **relevant part of your `agentup.yml`**, especially if this relates to plugin configuration. 29 | 30 | ```yaml 31 | # Example: 32 | plugin: 33 | name: some-plugin 34 | version: 1.2.3 35 | config: 36 | key: value 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ✨ Feature Request 3 | about: Suggest an idea or improvement for AgentUp 4 | title: "[Feature]: " 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## ✨ Feature Description 11 | 12 | A clear and concise description of the feature or improvement you'd like to see in AgentUp. 13 | 14 | --- 15 | 16 | ## 🚀 Motivation 17 | 18 | Why is this feature important? What problem does it solve or what new capability does it add? 19 | 20 | --- 21 | 22 | ## 🛠️ Suggested Implementation (Optional) 23 | 24 | Do you have an idea of how this could be implemented? 25 | 26 | --- 27 | 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🛠️ Pull Request 3 | about: Submit a code contribution to AgentUp 4 | title: "[PR]: " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 📝 Description 11 | 12 | Briefly describe what this pull request does and why it is needed. 13 | 14 | --- 15 | 16 | ## 🔍 Related Issues 17 | 18 | Closes #issue_number or references related issues. 19 | 20 | --- 21 | 22 | ## 🧪 Testing 23 | 24 | Explain how this has been tested or provide reproduction steps. 25 | 26 | - [ ] Security Tests pass (Bandit) 27 | - [ ] Unit tests pass 28 | - [ ] Manual testing completed 29 | 30 | --- 31 | 32 | ## 🧾 Changes Checklist 33 | 34 | - [ ] I’ve read the [contributing guidelines](../CONTRIBUTING.md) 35 | - [ ] My code follows the AgentUp code style 36 | - [ ] I’ve added tests (if applicable) 37 | - [ ] I’ve updated documentation (if applicable) 38 | - [ ] I've added information on any changes to `agentup.yml` required 39 | 40 | --- 41 | 42 | ## 🧩 Additional Notes 43 | 44 | Anything else the reviewer should know. 45 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "uv" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.github/workflows/publish-docs.yml: -------------------------------------------------------------------------------- 1 | name: Build Docs Image 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - 'docs/**' 8 | - 'mkdocs.yml' 9 | - 'docker/docs/**' 10 | - '.github/workflows/publish-docs.yml' 11 | workflow_dispatch: 12 | 13 | env: 14 | REGISTRY: ghcr.io 15 | IMAGE_NAME: ${{ github.repository }} 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | 21 | permissions: 22 | contents: read 23 | packages: write 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4 28 | 29 | - name: Setup Python 30 | uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v4 31 | with: 32 | python-version: '3.x' 33 | 34 | - name: Install dependencies 35 | run: | 36 | pip install mkdocs mkdocs-material mkdocs-glightbox mkdocs-minify-plugin 37 | # Add other mkdocs plugins you use 38 | 39 | - name: Build docs 40 | run: mkdocs build 41 | 42 | - name: Set up Docker Buildx 43 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3 44 | 45 | - name: Login to GitHub Container Registry 46 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 47 | with: 48 | registry: ${{ env.REGISTRY }} 49 | username: ${{ github.actor }} 50 | password: ${{ secrets.GITHUB_TOKEN }} 51 | 52 | - name: Extract metadata 53 | id: meta 54 | uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5 55 | with: 56 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 57 | tags: | 58 | type=raw,value=docs-latest,enable={{is_default_branch}} 59 | type=raw,value=docs-{{sha}} 60 | type=raw,value=docs-{{branch}}-{{sha}} 61 | type=ref,event=branch,prefix=docs- 62 | 63 | - name: Build and push 64 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v5 65 | with: 66 | context: . 67 | file: ./docker/docs/Dockerfile-docs 68 | push: true 69 | tags: ${{ steps.meta.outputs.tags }} 70 | labels: ${{ steps.meta.outputs.labels }} 71 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish PyPi 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | pypi-publish: 10 | name: Upload release to PyPI 11 | runs-on: ubuntu-latest 12 | permissions: 13 | id-token: write 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v5 20 | with: 21 | python-version: '3.x' 22 | 23 | - name: Install build tools 24 | run: | 25 | python -m pip install --upgrade build 26 | 27 | - name: Build distributions 28 | run: | 29 | python -m build 30 | 31 | - name: Publish package distributions to PyPI 32 | uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1 33 | with: 34 | attestations: true 35 | 36 | github-releases-to-discord: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v3 41 | - name: GitHub Releases to Discord 42 | uses: SethCohen/github-releases-to-discord@b96a33520f8ad5e6dcdecee6f1212bdf88b16550 # v1 43 | with: 44 | webhook_url: ${{ secrets.WEBHOOK_URL }} 45 | color: "2105893" 46 | username: "Release Changelog" 47 | content: "||@everyone||" 48 | footer_title: "Changelog" 49 | reduce_headings: true 50 | 51 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: Security Scan 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | paths: 7 | - 'src/**' 8 | - 'pyproject.toml' 9 | - 'uv.lock' 10 | - 'Makefile' 11 | - '.github/workflows/security.yml' 12 | pull_request: 13 | branches: [ main, develop ] 14 | paths: 15 | - 'src/**' 16 | - 'pyproject.toml' 17 | - 'uv.lock' 18 | - 'Makefile' 19 | - '.github/workflows/security.yml' 20 | schedule: 21 | # Run security scan weekly on Monday at 3 AM UTC 22 | - cron: '0 3 * * 1' 23 | workflow_dispatch: # Allow manual trigger 24 | 25 | jobs: 26 | bandit: 27 | name: Bandit Security Scan 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4 32 | 33 | - name: Install uv 34 | uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v4 35 | with: 36 | enable-cache: true 37 | 38 | - name: Set up Python 39 | run: | 40 | uv python install 3.11 41 | uv python pin 3.11 42 | 43 | - name: Install dependencies 44 | run: | 45 | uv sync --extra dev 46 | 47 | - name: Run Bandit security scan 48 | run: | 49 | uv run bandit -r src/ -f json -o bandit-report.json || true 50 | uv run bandit -r src/ -f txt 51 | 52 | - name: Upload Bandit report 53 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 54 | if: always() 55 | with: 56 | name: bandit-report 57 | path: bandit-report.json 58 | 59 | dependency-check: 60 | name: Dependency Vulnerability Check 61 | runs-on: ubuntu-latest 62 | 63 | steps: 64 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4 65 | 66 | - name: Install uv 67 | uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v4 68 | with: 69 | enable-cache: true 70 | 71 | - name: Set up Python 72 | run: | 73 | uv python install 3.11 74 | uv python pin 3.11 75 | 76 | - name: Install dependencies 77 | run: | 78 | make install-dev 79 | 80 | - name: Check for vulnerable dependencies 81 | run: | 82 | uv pip check || true 83 | # Export current dependencies for scanning 84 | uv pip freeze > requirements.txt 85 | 86 | - name: Run pip-audit 87 | run: | 88 | uv pip install pip-audit 89 | uv run pip-audit --desc || true 90 | 91 | - name: Upload dependency report 92 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 93 | if: always() 94 | with: 95 | name: dependency-report 96 | path: requirements.txt 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | env/ 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Virtual Environment 24 | .venv/ 25 | venv/ 26 | ENV/ 27 | env/ 28 | 29 | # Environment variables 30 | .env 31 | 32 | # IDE 33 | .idea/ 34 | .vscode/ 35 | *.swp 36 | *.swo 37 | .tool-versions 38 | .claude/ 39 | .serena/ 40 | 41 | # Logs 42 | logs/ 43 | *.log 44 | 45 | # OS specific 46 | .DS_Store 47 | .DS_Store? 48 | ._* 49 | .Spotlight-V100 50 | .Trashes 51 | ehthumbs.db 52 | Thumbs.db 53 | 54 | # AgentUp 55 | dump.rdb 56 | .planning/ 57 | 58 | # Testing and CI artifacts 59 | .coverage 60 | coverage.xml 61 | htmlcov/ 62 | .pytest_cache/ 63 | pytest_cache/ 64 | *.coverage 65 | .coverage.* 66 | test_*/ 67 | .mypy_cache/ 68 | .ruff_cache/ 69 | bandit-report.json 70 | requirements-ci.txt 71 | monkeytype.sqlite3 72 | 73 | # Test agents and renders 74 | test-agents/ 75 | test-render/ 76 | example-agent/ 77 | examples/chatbot/ 78 | memory/ 79 | 80 | # Temporary files 81 | *.tmp 82 | *.bak 83 | *.orig 84 | *~ 85 | 86 | # mkdocs 87 | site/ 88 | 89 | # misc 90 | .foam/ 91 | 92 | # agentup stuff 93 | agentup.yml 94 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: make-checks 5 | name: Run make format-check, lint, security, test-unit-fast 6 | entry: bash -c "make format-check && make lint && make security && make test-unit" 7 | language: system 8 | pass_filenames: false 9 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.11 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | :tada: **First off, thank you for considering contributing to our project!** :tada: 4 | 5 | This is a community-driven project, so it's people like you that make it useful and 6 | successful. These are some of the many ways to contribute: 7 | 8 | * :bug: Submitting bug reports and feature requests 9 | * :memo: Writing tutorials or examples 10 | * :mag: Fixing typos and improving the documentation 11 | * :bulb: Writing code for everyone to use 12 | * :people_holding_hands: Community engagement and outreach 13 | 14 | If you get stuck at any point you can create an 15 | [issue](https://github.com/RedDotRocket/AgentUp/issues) on GitHub 16 | or jump on [Discord](https://discord.com/invite/pPcjYzGvbS) 17 | 18 | For more information on contributing to open source projects, 19 | [GitHub's own guide](https://opensource.guide/how-to-contribute) 20 | is a great starting point if you are new to version control. Also, checkout the 21 | [Zen of Scientific Software Maintenance](https://jrleeman.github.io/ScientificSoftwareMaintenance/) 22 | for some guiding principles on how to create high quality scientific software 23 | contributions. 24 | 25 | ## Ground Rules 26 | 27 | The goal is to maintain a diverse community that's pleasant for everyone. 28 | **Please be considerate and respectful of others**. Everyone must abide by our 29 | [Code of Conduct](https://github.com/RedDotRocket/AgentUp/blob/main/CODE_OF_CONDUCT.md) 30 | and we encourage all to read it carefully. 31 | 32 | ## Developer Certificate Of Origin 33 | 34 | By submitting a pull request or contribution to this project, I certify that: 35 | 36 | 37 | (a) The contribution was created in whole or in part by me and I 38 | have the right to submit it under the open source license 39 | indicated in the file; or 40 | 41 | (b) The contribution is based upon previous work that, to the best 42 | of my knowledge, is covered under an appropriate open source 43 | license and I have the right under that license to submit that 44 | work with modifications, whether created in whole or in part 45 | by me, under the same open source license (unless I am 46 | permitted to submit under a different license), as indicated 47 | in the file; or 48 | 49 | (c) The contribution was provided directly to me by some other 50 | person who certified (a), (b) or (c) and I have not modified 51 | it. 52 | 53 | (d) I understand and agree that this project and the contribution 54 | are public and that a record of the contribution (including all 55 | personal information I submit with it, including my sign-off) is 56 | maintained indefinitely and may be redistributed consistent with 57 | this project or the open source license(s) involved. 58 | 59 | ## How to Agree to DCO 60 | 61 | - Use `git commit -s` for automatic sign-off, OR 62 | - Comment "I agree to the DCO" in your pull request, OR 63 | - Simply submitting a pull request constitutes agreement to the above DCO terms 64 | -------------------------------------------------------------------------------- /README.cn.md: -------------------------------------------------------------------------------- 1 |
2 | English | 简体中文 3 |
4 | 5 |
6 | AgentUp Logo 7 |

为AI智能体带来Docker对容器的革命性改变

8 |
9 | 10 | 11 |

12 | 13 | 新手友好问题 14 | 15 |   16 | 17 | 加入Discord 18 | 19 |

20 | 21 | 22 |

23 | 24 | 许可证 25 | 26 | 27 | CI状态 28 | 29 | 30 | PyPI版本 31 | 32 | 33 | 下载量 34 | 35 | 36 | Discord 37 | 38 |

39 |
40 |
41 | 42 | 43 |
44 | 45 | 46 | 52 | 53 |
47 | 🚀 积极开发中 48 |
49 | 🏃‍♂️ 我们进展很快,可能会有变化! 50 |
51 |
54 |
55 | 56 |
57 | 58 | ## 为什么选择AgentUp? 59 | 60 | 正如Docker让应用程序变得不可变、可重现且运维友好,**AgentUp**为AI智能体带来了同样的革命。通过配置定义您的智能体,它可以在任何地方一致运行。与团队成员分享智能体,他们可以克隆/分叉并立即运行。部署时确信您的智能体在开发、测试和生产环境中都会表现一致。 61 | 62 | AgentUp由拥有丰富经验的工程师构建,他们曾为**Google、GitHub、Nvidia、Red Hat、Shopify等公司**的关键任务系统创建开源解决方案。我们深知构建稳定、安全、可扩展软件的要求,并将这些原则应用于让AI智能体真正做到生产就绪、安全可靠。 63 | 64 | ## AgentUp:开发者优先的智能体框架 65 | 66 | AgentUp提供企业级智能体基础架构,专为需要强大功能与简洁性的专业开发者而设计。 67 | 68 | **开发者优先的操作**:由了解现实约束的开发者构建。每个智能体都存在于自己的代码库中,仅需一个AgentUp配置文件。克隆、运行`agentup run`,所有依赖项在初始化期间解决——不再有环境设置的烦恼。 69 | 70 | **安全设计**:内置基于范围的细粒度访问控制,支持OAuth2、JWT和API密钥认证,防止未授权的工具/MCP访问,确保数据保护。安全不是事后考虑——它是AgentUp的基础架构。 71 | 72 | **配置驱动架构**:通过声明式配置定义复杂的智能体行为、数据源和工作流。跳过数周的样板代码和框架争夺。您的智能体成为可移植、可版本化的资产,具有清晰的契约定义其能力和交互。 73 | 74 | **可扩展的定制生态系统**:需要RAG、图像处理、自定义API逻辑?没问题。利用社区插件或构建自动继承AgentUp中间件、安全和操作功能的自定义扩展。独立的插件版本控制与现有CI/CD管道无缝集成,确保核心平台更新不会破坏您的实现。使用AgentUp,您可以获得运行智能体的即时反馈,以及框架的可扩展性。 75 | 76 | **智能体到智能体发现**:自动A2A智能体卡生成向生态系统中的其他智能体公开您的智能体能力,实现无缝的智能体间通信和编排。 77 | 78 | **异步任务架构**:消息驱动的任务管理支持基于回调通知的长时间运行操作。非常适合研究智能体、数据处理工作流和事件驱动自动化。跨Redis和其他后端的状态持久化确保大规模可靠性。 79 | 80 | ## 面向生产的先进架构 81 | 82 | AgentUp在设计时考虑了生产部署,具备随着框架成熟而扩展的架构模式。虽然目前仍在alpha阶段,但核心安全和可扩展性功能已经为构建严肃的AI智能体提供了坚实的基础。 83 | 84 | ## 保持更新 85 | 86 | AgentUp 开发进展很快 🏃‍♂️,要跟进项目动态并第一时间收到新版本通知,请给仓库点星。 87 | 88 | 89 | 90 | ## 几分钟内开始使用 91 | 92 | ### 安装 93 | 94 | 使用您首选的Python包管理器安装AgentUp: 95 | 96 | ```bash 97 | pip install agentup 98 | ``` 99 | 100 | ### 创建您的第一个智能体 101 | 102 | 通过交互式配置生成新的智能体项目: 103 | 104 | ```bash 105 | agentup init 106 | ``` 107 | 108 | 从可用选项中选择,并通过交互式提示配置您的智能体能力、认证和AI提供商设置。 109 | 110 | ### 启动您的智能体 111 | 112 | 启动开发服务器并开始构建: 113 | 114 | ```bash 115 | agentup run 116 | ``` 117 | 118 | 您的智能体现在运行在`http://localhost:8000`,具有完整的A2A兼容JSON RPC API、安全中间件和所有配置的可用能力。 119 | 120 | ### 下一步 121 | 122 | 探索全面的[文档](https://docs.agentup.dev)以了解高级功能、教程、API参考和现实世界示例,帮助您快速构建智能体。 123 | 124 | ### 当前集成 125 | 126 | AgentUp智能体能够将自己作为工具呈现给不同的框架,这带来了确保所有工具使用一致且安全、被跟踪和可追溯的优势。 127 | 128 | - [CrewAI](https://crewai.com),详见[文档](docs/integrations/crewai.md)。 129 | 130 | ## 开源和社区驱动 131 | 132 | AgentUp采用Apache 2.0许可证,基于开放标准构建。该框架实现了A2A(智能体到智能体)规范以实现互操作性,并遵循MCP(模型上下文协议)与更广泛的AI工具生态系统集成。 133 | 134 | **贡献** - 无论您是修复错误、添加功能还是改进文档,都欢迎贡献。加入不断增长的开发者社区,共同构建AI智能体基础设施的未来。 135 | 136 | **社区支持** - 通过[GitHub Issues](https://github.com/RedDotRocket/AgentUp/issues)报告问题、请求功能和获取帮助。在[Discord](https://discord.gg/pPcjYzGvbS)上参与实时讨论并与其他开发者联系。 137 | 138 | ## 什么是DCO Bot? 139 | 140 | 我们使用开发者原创证书(DCO)来保持项目的法律健全性并保护我们的社区。这在开源项目中很常见(Linux内核、Kubernetes、Docker)。 141 | 142 | DCO防止意外包含专有代码等问题,并确保所有贡献者都有权提交他们的更改。 143 | 144 | 这保护了项目的贡献者和用户。 145 | 146 | ### 如何签署提交 147 | 在提交时简单地添加`-s`标志: 148 | 149 | ```bash 150 | git commit -s -m "添加很棒的新功能" 151 | ``` 152 | 153 | 这会添加一行"Signed-off-by",证明您编写了代码或有权限在Apache 2.0下贡献它。您保留对贡献的所有权——无需文书工作! 154 | 155 | ## 表达您的支持 ⭐ 156 | 157 | 如果AgentUp正在帮助您构建更好的AI智能体,或者您想鼓励开发,请考虑给它一个星标,帮助其他人发现这个项目,也让我知道值得继续投入时间到这个框架中! 158 | 159 | [![GitHub stars](https://img.shields.io/github/stars/RedDotRocket/AgentUp.svg?style=social&label=Star)](https://github.com/RedDotRocket/AgentUp) 160 | 161 | --- 162 | 163 | **许可证** - Apache 2.0 164 | 165 | 166 | [badge-discord-img]: https://img.shields.io/discord/1384081906773131274?label=Discord&logo=discord 167 | [badge-discord-url]: https://discord.gg/pPcjYzGvbS -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please do not report a security vulnerability in the open, instead contact "luke@rdrocket.com" , so if need be the issue can be managed under [responsible disclosure](https://en.wikipedia.org/wiki/Coordinated_vulnerability_disclosure). 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/assets/assets/icon.png -------------------------------------------------------------------------------- /assets/compie-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/assets/compie-wide.png -------------------------------------------------------------------------------- /assets/compie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/assets/compie.png -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/assets/demo.gif -------------------------------------------------------------------------------- /assets/init.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/assets/init.gif -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/assets/logo.png -------------------------------------------------------------------------------- /assets/plugins.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/assets/plugins.gif -------------------------------------------------------------------------------- /assets/speedrun.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/assets/speedrun.gif -------------------------------------------------------------------------------- /assets/star.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/assets/star.gif -------------------------------------------------------------------------------- /docker/docs/Dockerfile-docs: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | 3 | # Copy the built MkDocs site (from repo root) 4 | COPY site/ /usr/share/nginx/html/ 5 | 6 | # Copy nginx config from same directory 7 | COPY docker/docs/nginx.conf /etc/nginx/nginx.conf 8 | 9 | EXPOSE 8080 10 | 11 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /docker/docs/nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1024; 3 | } 4 | 5 | http { 6 | include /etc/nginx/mime.types; 7 | default_type application/octet-stream; 8 | 9 | gzip on; 10 | gzip_vary on; 11 | gzip_min_length 1024; 12 | gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/xhtml+xml font/opentype font/truetype image/svg+xml; 13 | 14 | server { 15 | listen 8080; 16 | server_name _; 17 | root /usr/share/nginx/html; 18 | index index.html; 19 | 20 | # Security headers 21 | add_header X-Frame-Options "SAMEORIGIN" always; 22 | add_header X-Content-Type-Options "nosniff" always; 23 | add_header X-XSS-Protection "1; mode=block" always; 24 | add_header Referrer-Policy "no-referrer-when-downgrade" always; 25 | 26 | # Cache control 27 | location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2)$ { 28 | expires 1y; 29 | add_header Cache-Control "public, immutable"; 30 | } 31 | 32 | location / { 33 | try_files $uri $uri/ $uri.html =404; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /docs/getting-started/core-concepts.md: -------------------------------------------------------------------------------- 1 | # Core Concepts 2 | 3 | Let's revisit the core concepts of AgentUp to understand how it works and what makes it unique. 4 | 5 | ## Key Architecture Principles 6 | 7 | ### 1. Plugin-Based Architecture 8 | 9 | ``` 10 | ┌─────────────────┐ ┌──────────────────┐ 11 | │ Your Agent │ │ AgentUp Package │ 12 | │ (Configuration) │───▶│ (Framework) │ 13 | │ │ │ │ 14 | │ • agentup.yml │ │ • Core Runtime │ 15 | │ • No Code │ │ • Plugin System │ 16 | │ • Just YAML │ │ • A2A Protocol │ 17 | └─────────────────┘ └──────────────────┘ 18 | ``` 19 | 20 | **What this means:** 21 | 22 | - **Agents contain only configuration** - No source code to maintain 23 | - **Framework provides all functionality** - Runtime, protocols, plugins 24 | - **Easy updates** - Update framework core, keep your config and plugins pinned to versions 25 | 26 | ### 2. Configuration-Driven Design 27 | 28 | Everything in AgentUp is controlled through `agentup.yml`: 29 | 30 | ```yaml 31 | name: "My Agent" 32 | description: "What this agent does" 33 | version: "1.0.0" 34 | 35 | # Enable plugins for functionality 36 | plugins: 37 | - plugin_id: system_tools 38 | - plugin_id: web_search 39 | 40 | # Auto-applied middleware 41 | middleware: 42 | - name: rate_limiting 43 | config: 44 | requests_per_minute: 60 45 | ``` 46 | 47 | ### 3. Plugin Capabilities 48 | 49 | Plugins provide capabilities to your agent: 50 | 51 | ``` 52 | Agent Config ──┐ 53 | │ 54 | ▼ 55 | Plugin Loader 56 | │ 57 | ▼ 58 | ┌──────────────────┐ 59 | │ Capabilities │ 60 | │ │ 61 | │ • read_file │ 62 | │ • write_file │ 63 | │ • web_search │ 64 | │ • send_email │ 65 | └──────────────────┘ 66 | ``` 67 | 68 | ### 4. Scopes and Security 69 | 70 | Scopes define what capabilities an agent can access: 71 | 72 | ``` 73 | Agent Config ──┐ 74 | │ 75 | ▼ 76 | Plugin Loader 77 | │ 78 | ▼ 79 | ┌──────────────────┐ 80 | │ Capabilities │ 81 | │ │ 82 | │ • read_file │ 83 | │ • write_file │ 84 | │ • web_search │ 85 | │ • send_email │ 86 | └──────────────────┘ 87 | Scope Check 88 | │ 89 | ▼ 90 | ┌──────────────────┐ 91 | │ Scopes │ 92 | │ │ 93 | │ • files:read │ 94 | │ • files:write │ 95 | │ • web:search │ 96 | │ • email:send │ 97 | └──────────────────┘ 98 | ``` 99 | 100 | 101 | **Key concepts:** 102 | 103 | - **Plugins** = Python packages with capabilities 104 | - **Capabilities** = Individual functions (read_file, web_search) 105 | - **Scopes** = How capabilities are grouped and applied policy 106 | 107 | ## AgentUp Taxonomy 108 | 109 | ### Framework Components 110 | 111 | 112 | #### Plugin Capabilities 113 | 114 | Provided by plugins, configured in your agent: 115 | 116 | - File operations (`read_file`, `write_file`) 117 | - System commands (`execute_command`) 118 | - Web requests (`http_request`) 119 | - Custom capabilities (your plugins) 120 | 121 | ### Communication Protocols 122 | 123 | #### A2A (Agent-to-Agent) 124 | - **Purpose**: Agent discovery and inter-agent communication 125 | - **Format**: JSON-RPC 2.0 over HTTP 126 | - **Features**: Capability discovery, secure communication, standardized errors 127 | 128 | #### MCP (Model Context Protocol) 129 | - **Purpose**: Pluggable tools for AI models 130 | - **Integration**: Works with AgentUp plugins 131 | - **Benefit**: Standardized tool interfaces 132 | 133 | ## Auto-Application Pattern 134 | 135 | AgentUp globally applies cross-cutting concerns: 136 | 137 | ```yaml 138 | # Global settings applied everywhere 139 | middleware: 140 | - name: rate_limiting 141 | - name: authentication 142 | 143 | state_management: 144 | enabled: true 145 | ``` 146 | 147 | Per-plugin overrides possible 148 | 149 | ```yaml 150 | plugins: 151 | - plugin_id: expensive_api 152 | middleware_override: 153 | - name: rate_limiting 154 | config: 155 | requests_per_minute: 10 # Slower for this plugin 156 | ``` 157 | -------------------------------------------------------------------------------- /docs/getting-started/index.md: -------------------------------------------------------------------------------- 1 | # Getting Started with AgentUp 2 | 3 | Let's get you up and running with AgentUp! 4 | 5 | This section will guide you through your first steps with the framework. 6 | 7 | !!! Prerequisites 8 | Before diving in, ensure you have the following: 9 | 10 | * Python 3.10 or higher installed 11 | * Familiarity with YAML configuration files 12 | * A text editor or IDE for coding 13 | * curl or an API client for testing endpoints 14 | 15 | ### What You'll Learn 16 | 17 | By the end of this section, you'll: 18 | 19 | - Have AgentUp installed and configured 20 | - Understand the core concepts and architecture 21 | - Have created and tested your first agent 22 | - Know the difference between reactive and iterative agents 23 | - Understand how to configure iterative agents for complex goals 24 | - Know how to customize and extend your agents 25 | 26 | ## Getting Started Guide 27 | 28 | Follow these steps to get up and running with AgentUp: 29 | 30 | 1. **[Installation](installation.md)** - Install AgentUp and its dependencies 31 | 2. **[Core Concepts](core-concepts.md)** - Learn the fundamental concepts 32 | 3. **[Create Your First Agent](first-agent.md)** - Build and test a basic agent 33 | 4. **[Iterative Agents](iterative-agent.md)** - Deep dive into self-directed, goal-based agents 34 | 5. **[AI Agent Integration](ai-agent.md)** - Connect with AI providers 35 | 36 | ## Quick Start 37 | 38 | For the impatient, here's the fastest way to get started: 39 | 40 | ```bash 41 | # Install AgentUp 42 | pip install agentup 43 | 44 | # Create a new agent project 45 | agentup init my-agent 46 | 47 | # Follow the prompts and choose "Iterative" for complex goal handling 48 | # Start the agent 49 | cd my-agent 50 | uv sync 51 | agentup run 52 | ``` 53 | 54 | Your agent will be running at `http://localhost:8000` and ready to handle complex, multi-step goals through its iterative execution engine. 55 | 56 | -------------------------------------------------------------------------------- /docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | # Installation Guide 2 | 3 | Get AgentUp installed and configured on your system. 4 | 5 | !!! Prerequisites 6 | - **Python 3.10 or higher** - Check with `python --version` 7 | - **pip** package manager - Usually included with Python 8 | - **uv** - If you're building from source, plan to contribute etc 9 | - **Git** (recommended) - For cloning examples and contributing 10 | 11 | ### Supported Platforms 12 | - **Linux** (Ubuntu 20.04+, CentOS 8+, others) 13 | - **macOS** (10.15+) 14 | - **Windows** (10, 11) 15 | 16 | ### Installation Methods 17 | 18 | === "pipx install" 19 | 20 | ```bash 21 | pipx install agentup 22 | ``` 23 | === "git clone (uv)" 24 | 25 | ```bash 26 | git clone https://github.com/RedDotRocket/AgentUp.git 27 | cd AgentUp 28 | 29 | # Create virtual environment 30 | uv sync 31 | 32 | # Install in development mode (omit `e` for fixed install) 33 | uv add --editable /path/to/AgentUp 34 | ``` 35 | 36 | ### Verify Installation 37 | 38 | ```bash 39 | # Check AgentUp version 40 | agentup --version 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/images/compie-docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/docs/images/compie-docs.png -------------------------------------------------------------------------------- /docs/images/compie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/docs/images/compie.png -------------------------------------------------------------------------------- /docs/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/docs/images/favicon.ico -------------------------------------------------------------------------------- /docs/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/docs/images/icon.png -------------------------------------------------------------------------------- /docs/images/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/docs/images/next.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # AgentUp Documentation 2 | 3 | 9 | 10 |

11 | AgentUp Logo 12 |

13 | 14 |

15 | License: Apache 2.0 16 | | 17 | CI 18 | | 19 | Discord 20 | | 21 | PyPI Version 22 | | 23 | Downloads 24 |

25 | 26 |

27 | The Operating System for AI Agents. Designed with security, scalability, and extensibility at its foundation. Build Agents at blistering speed, with safety builtin. 28 |

29 | 30 | !!! warning 31 | Development is moving fast, and this document may not reflect the latest changes. Once updated, we will remove this warning. 32 | 33 | ## Welcome to the AgentUp documentation! 34 | 35 | You'll find everything you need here to get started with AgentUp, from installation to advanced configuration and troubleshooting. AgentUp streamlines blistering fast development through a configuration-driven architecture, yet with the ability to extend as much as you need via a rich plugin ecosystem. Ensuring your agents are portable, maintainable and revision controlled. 36 | 37 | ## How This Guide is Organized 38 | 39 | ### Progressive disclosure 40 | 41 | This documentation follows a [progressive disclosure](https://en.wikipedia.org/wiki/Progressive_disclosure) approach: 42 | 43 | drawing 44 | 45 | 1. **Quick Start sections** get you up and running immediately 46 | 2. **Detailed guides** provide comprehensive coverage of each topic 47 | 3. **Reference materials** offer complete technical specifications 48 | 4. **Troubleshooting** helps solve specific problems 49 | 50 | Each section starts with a preequisites list. No asumptions are made about your prior knowledge. We intend for all to come on this journey, so we will start with the basics and build up from there. 51 | 52 | !!! Prerequisites 53 | What you need before starting, e.g.: 54 | 55 | * Python version 56 | * Libraries 57 | * Snacks 58 | * Hat and sun protector 59 | 60 | We attempt to narrow in on the essentials: 61 | 62 | - Code blocks for commands and code snippets 63 | - Highlighted lines for key parts of code examples 64 | 65 | ``` py hl_lines="2 3" 66 | def bubble_sort(items): 67 | for i in range(len(items)): 68 | for j in range(len(items) - 1 - i): 69 | if items[j] > items[j + 1]: 70 | items[j], items[j + 1] = items[j + 1], items[j] 71 | ``` 72 | 73 | #### Helpful Tips 74 | 75 | We attempt to teach as we go along, so you can learn the concepts behind the commands. You should see lots of **tips** at various intervals, there to help you understand the underlying principles of AgentUp. 76 | 77 | !!! tip 78 | **AgentUp** is designed to be **extensible**. You can create custom plugins for reuse or share with the community. 79 | 80 | ## Human Curated Documentation 81 | 82 | Time has been taken to ensure clarity and accuracy, so you can trust the information provided here. You won't find a sea of emojis or mermaid diagrams galore. We believe in quality over quantity, and we hope you appreciate the effort that has been invested in creating this documentation. 83 | 84 | --- 85 | 86 | ## Support and Community 87 | 88 | Should you need help or want to connect with other users, we have several options: 89 | 90 | - **Discord**: Jump on [Discord](https://discord.gg/pPcjYzGvbS), we would love to have you! 91 | - **GitHub Issues**: [Report bugs and request features](https://github.com/rdrocket-projects/AgentUp/issues) 92 | 93 | --- 94 | 95 | ## Contributing 96 | 97 | We welcome contributions to improve this documentation, code, and overall experience! 98 | -------------------------------------------------------------------------------- /docs/middleware/rate-limiting.md: -------------------------------------------------------------------------------- 1 | # AgentUp Rate-Limiting 2 | 3 | Rate limiting in AgentUp is managed through **middleware**. 4 | You can apply it globally (network-level) or override for specific plugins. 5 | Root-level configuration is no longer supported — use middleware only. 6 | 7 | --- 8 | 9 | ## Network-Level Rate Limiting (FastAPI Middleware) 10 | 11 | Rate limiting on AgentUp's FastAPI middleware is exposed via `agentup.yml`: 12 | 13 | ```yaml 14 | rate_limiting: 15 | enabled: true 16 | endpoint_limits: 17 | "/": {"rpm": 100, "burst": 120} 18 | "/mcp": {"rpm": 60, "burst": 150} 19 | ``` 20 | 21 | **Details:** 22 | 23 | | Aspect | Description | 24 | |------------|--------------------------------------------| 25 | | Scope | All HTTP requests to specific endpoints | 26 | | Applied | Before requests reach any plugin code | 27 | | Purpose | Network-level protection | 28 | 29 | The applied Rate Limiting can be seen when starting an Agent in **DEBUG** mode, for example: 30 | 31 | ```text 32 | 2025-07-28 19:43:02 [DEBUG] Network rate limiting middleware initialized endpoint_limits={'/': {'rpm': 100, 'burst': 120}, '/mcp': {'rpm': 50, 'burst': 60}, '/health': {'rpm': 200, 'burst': 240}, '/status': {'rpm': 60, 'burst': 72}, 'default': {'rpm': 60, 'burst': 72}} 33 | ``` 34 | 35 | --- 36 | 37 | ## Plugin-Specific Override (Per-Plugin Middleware) 38 | 39 | You can override the global middleware for a specific plugin by adding a plugin-level override in `agentup.yml`: 40 | 41 | ```yaml 42 | plugins: 43 | - plugin_id: name 44 | middleware_override: 45 | - name: rate_limited 46 | params: 47 | requests_per_minute: 10 48 | ``` 49 | 50 | **Details:** 51 | 52 | | Aspect | Description | 53 | |------------|--------------------------------------------| 54 | | Scope | ONLY that specific plugin | 55 | | Applied | Replaces global middleware for that plugin | 56 | | Purpose | Fine-tuned control per plugin | 57 | 58 | --- 59 | 60 | ## ⚠️ Note on Root-Level Configuration 61 | 62 | Previous versions of AgentUp allowed **root-level rate limiting config**. 63 | This is no longer supported — all configuration must now be done through **middleware**. 64 | -------------------------------------------------------------------------------- /docs/plugin-development/avatar-generation.md: -------------------------------------------------------------------------------- 1 | # How to Create a 'Compie' Image for your Plugin. 2 | 3 | To generate an image of 'Compie' for use in the AgentUp plugin registry, you can use the following prompt with an image generation model. I found so far ChatGPT works best. 4 | 5 | ```plaintext 6 | Create a high-quality 2D digital illustration in a modern rubber hose + retro cartoon style, featuring an anthropomorphic vintage computer (CRT monitor with a keyboard as body and gloved hands/feet). 7 | 8 | The character should have a friendly expression and be interacting with a themed object (e.g., holding a camera, map, wrench, etc.) depending on the topic. 9 | 10 | Use a limited vintage-inspired color palette: muted teal, beige, off-white, faded red, and dark outlines. 11 | 12 | Include bold, distressed sans-serif lettering above and below the character, with the top word(s) indicating the app or brand (e.g., "AGENTUP") and the bottom word(s) indicating the tool name (e.g., "IMAGE", "SYS TOOLS", or “MAPS", “SLACK”). 13 | 14 | The background should be transparent or a light textured paper tone if transparency isn’t possible. 15 | 16 | The overall style should be playful, bold, clean, and ideal for branding, badges, or banners. 17 | 18 | The image dimensions must be 400x400, with an alpha channel to provide a transparent background 19 | The theme for the image should be: 20 | 21 | [Description of Compie] 22 | ``` 23 | 24 | You will often find the dimensions get ignored, if this is the case you can resize to 400x400 in an image editing application 25 | such as Gimp, or Photoshop. 26 | 27 | Once oyou have your image, move it into the `static` folder in your plugin directory and name it `logo.png` 28 | 29 | ``` 30 | . 31 | ├── static 32 | │ └── logo.png 33 | ``` -------------------------------------------------------------------------------- /docs/plugin-development/index.md: -------------------------------------------------------------------------------- 1 | # AgentUp Plugin System 2 | 3 | The AgentUp plugin system is for extending AI agent capabilities and provides a clean, 4 | type-safe, and extensible way to create, distribute, and manage agent functionality. 5 | 6 | ## What Are Plugins? 7 | 8 | Plugins are independent Python packages that extend your agent's capabilities. 9 | They can be developed, tested, and distributed separately from the main agent codebase. This carries several benefits: 10 | - **Modular** - each plugin encapsulates specific functionality 11 | - **Reusable** - plugins can be shared across different agents 12 | - **Versioned** - plugins can be versioned independently 13 | - **Discoverable** - plugins are automatically discovered by the AgentUp framework 14 | 15 | ## Quick Start 16 | 17 | ### 1. Create Your First Plugin 18 | 19 | Plugins can be created anywhere - you don't need to be inside an agent project: 20 | 21 | ```bash 22 | # Create a new plugin with interactive prompts (run from any directory) 23 | agentup plugin init 24 | 25 | # Or specify details directly 26 | agentup plugin init weather-plugin --template ai 27 | 28 | # This creates a new directory with your plugin 29 | cd weather-plugin/ 30 | ``` 31 | 32 | ### 2. Develop and Test 33 | 34 | ```bash 35 | # Install your plugin in development mode 36 | pip install -e . 37 | ``` 38 | 39 | ### 3. Use in Your Agent 40 | 41 | Plugins are discovered automatically through two methods: 42 | 43 | **a) Development Mode** (Recommended for plugin development) 44 | ```bash 45 | # Navigate to your plugin directory 46 | cd /path/to/weather-plugin 47 | 48 | # Install in development mode 49 | pip install -e . 50 | ``` 51 | 52 | **b) Production Mode** (For published packages) 53 | ```bash 54 | # Install from PyPI or other sources 55 | pip install agentup-weather-plugin 56 | ``` 57 | 58 | ## Plugin Types 59 | 60 | ### Basic Plugins 61 | Perfect for simple direct routed functions, where an LLM is not required. 62 | 63 | 64 | ### AI Plugins 65 | Provide LLM-callable functions for agent interactions. 66 | 67 | ## Documentation Sections 68 | 69 | 1. **[Getting Started](getting-started.md)** - Create your first plugin in 5 minutes 70 | 2. **[Plugin Development](development.md)** - Comprehensive development guide 71 | 3. **[AI Function Integration](ai-functions.md)** - Build LLM-callable functions 72 | 4. **[Scopes and Security](scopes-and-security.md)** - Plugin security and access control 73 | 5. **[System Prompts](plugin-system-prompts.md)** - Customize AI behavior with capability-specific system prompts 74 | 6. **[Testing Plugins](testing.md)** - Test your plugins thoroughly 75 | 7. **[CLI Reference](cli-reference.md)** - Complete CLI command documentation 76 | 77 | ## Architecture Overview 78 | 79 | ``` 80 | ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ 81 | │ Your Plugin │ │ AgentUp Core │ │ LLM Service │ 82 | │ │ │ │ │ │ 83 | │ ┌─────────────┐ │ │ ┌──────────────┐ │ │ ┌─────────────┐ │ 84 | │ │ Skill Logic │◄┼────┼►│ Plugin Mgr │◄┼────┼►│ Function │ │ 85 | │ └─────────────┘ │ │ └──────────────┘ │ │ │ Calling │ │ 86 | │ ┌─────────────┐ │ │ ┌──────────────┐ │ │ └─────────────┘ │ 87 | │ │AI Functions │◄┼────┼►│ Function Reg │ │ │ │ 88 | │ └─────────────┘ │ │ └──────────────┘ │ │ │ 89 | └─────────────────┘ └──────────────────┘ └─────────────────┘ 90 | ``` 91 | 92 | The plugin system provides clean interfaces between your code and the agent infrastructure, 93 | making plugin development straightforward and maintainable, and best of all, 94 | sharable with the community. 95 | -------------------------------------------------------------------------------- /docs/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | :root > * { 2 | --md-primary-fg-color: #46604fff; 3 | } 4 | 5 | .highlight pre { 6 | white-space: pre-wrap !important; 7 | word-wrap: break-word !important; 8 | } -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: AgentUp Documentation 2 | site_description: Documentation for AgentUp, a framework for building AI agents. 3 | site_url: https://github.com/RedDotRocket/agentup 4 | repo_url: https://github.com/RedDotRocket/agentup 5 | repo_name: RedDotRocket/agentup 6 | theme: 7 | name: material 8 | font: 9 | text: Nunito 10 | code: Fira Code 11 | logo: images/icon.png 12 | favicon: images/favicon.ico 13 | features: 14 | # # Content 15 | # # - navigation.tabs # Enables tabbed navigation 16 | - content.tooltips 17 | - content.action.edit 18 | - content.action.view 19 | - content.code.annotate 20 | # - content.code.copy 21 | - content.tabs.link 22 | - content.code.select 23 | # # # Nav 24 | # # - navigation.instant # Enables mobile hamburger menu 25 | # # - navigation.expand # Expand all sections by default 26 | # # - navigation.path 27 | # # - navigation.tabs 28 | # # - navigation.sections 29 | - navigation.footer 30 | # # - navigation.indexes 31 | # # - navigation.top 32 | # # - navigation.tracking 33 | # # # Search 34 | - search.highlight 35 | - search.share 36 | - search.suggest 37 | # # # Table of Contents 38 | - toc.follow 39 | 40 | palette: 41 | # Light Mode (Retro Comic Theme) 42 | - scheme: default 43 | toggle: 44 | icon: material/weather-night 45 | name: Switch to dark mode 46 | primary: teal 47 | accent: custom 48 | 49 | # Dark Mode (Retro Comic Dark) 50 | - scheme: slate 51 | toggle: 52 | icon: material/weather-sunny 53 | name: Switch to light mode 54 | primary: custom 55 | accent: custom 56 | 57 | extra_css: 58 | - stylesheets/extra.css 59 | 60 | markdown_extensions: 61 | - attr_list 62 | - md_in_html 63 | - pymdownx.emoji: 64 | emoji_index: !!python/name:material.extensions.emoji.twemoji 65 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 66 | - pymdownx.highlight: 67 | anchor_linenums: true 68 | line_spans: __span 69 | pygments_lang_class: true 70 | - pymdownx.inlinehilite 71 | - pymdownx.snippets 72 | - pymdownx.superfences: 73 | custom_fences: 74 | - name: mermaid 75 | class: mermaid 76 | format: !!python/name:pymdownx.superfences.fence_code_format 77 | - pymdownx.tabbed: 78 | alternate_style: true 79 | - admonition 80 | - pymdownx.details 81 | copyright: Copyright © 2025 Red Dot Rocket 82 | 83 | nav: 84 | - Home: index.md 85 | - Getting Started: 86 | - Overview: getting-started/index.md 87 | - Installation: getting-started/installation.md 88 | - First Agent: getting-started/first-agent.md 89 | - AI Agent: getting-started/ai-agent.md 90 | - Goal Orientated Agents: getting-started/iterative-agent.md 91 | - Core Concepts: getting-started/core-concepts.md 92 | - Middleware: 93 | - Middleware: middleware/index.md 94 | - A2A Protocol: middleware/a2a-protocol.md 95 | - Logging: middleware/logging.md 96 | - State Management: middleware/state-management.md 97 | - Cache Management: middleware/cache-management.md 98 | - Rate Limiting: middleware/rate-limiting.md 99 | - MCP Integration: mcp/mcp-integration.md 100 | - Security: 101 | - Security: security/index.md 102 | - API Keys: security/api-keys.md 103 | - Bearer Tokens: security/jwt-tokens.md 104 | - OAuth2: security/oauth2-tokens.md 105 | - OAuth2 Provider Configuration: security/oauth2-provider-configuration.md 106 | - GitHub OAuth2 Setup: security/github_oauth2_setup.md 107 | - Scope-based Authorization: security/scope-based-authorization.md 108 | - Integrations: 109 | - CrewAI: integrations/crewai.md 110 | - Plugin Development: 111 | - Plugin Development: plugin-development/index.md 112 | - Getting Started: plugin-development/getting-started.md 113 | - AI Functions: plugin-development/ai-functions.md 114 | - CLI Reference: plugin-development/cli-reference.md 115 | - Plugin System Prompts: plugin-development/plugin-system-prompts.md 116 | - Plugins as Tools: plugin-development/plugins-as-tools-explanation.md 117 | - Scopes and Security: plugin-development/scopes-and-security.md 118 | - Logging: plugin-development/logging.md 119 | - Testing: plugin-development/testing.md 120 | - Development: plugin-development/development.md 121 | - Avatar Generation: plugin-development/avatar-generation.md 122 | - AgentUp Development: 123 | - AgentUp Development: agentup-development/index.md 124 | - Configuration: agentup-development/configuration.md 125 | - Customization: agentup-development/customization.md 126 | - Template System: agentup-development/template-system.md 127 | - Release Management: agentup-development/release.md 128 | - Advanced: 129 | - Push Notifications: advanced/push-notifications.md 130 | - Streaming: advanced/streaming.md 131 | - Reference: 132 | - Configuration: reference/configuration.md 133 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "agentup" 3 | version = "0.7.5" 4 | description = "Create AI agents with all the trappings, out of the box." 5 | readme = "README.md" 6 | requires-python = ">=3.11" 7 | authors = [{ name = "Luke Hinds", email = "luke@rdrocket.com" }] 8 | dependencies = [ 9 | "click>=8.1.0", 10 | "questionary>=2.0.1", 11 | "pyyaml>=6.0.1", 12 | "jinja2>=3.1.0", 13 | "httpx>=0.28.1", 14 | "a2a-sdk[sql]>=0.3.0", 15 | "uvicorn>=0.34.3", 16 | "fastapi[standard]>=0.115.12", 17 | "pytest>=8.4.0", 18 | "pytest-asyncio>=1.0.0", 19 | "numpy>=1.26.4", 20 | "pillow>=11.3.0", 21 | "asyncio>=3.4.3", 22 | "fastmcp>=2.8.1", 23 | "mcp>=1.0.0", 24 | "pydantic>=2.11.5", 25 | "pydantic-settings>=2.7.0", 26 | "python-dotenv>=1.1.0", 27 | "authlib>=1.6.0", 28 | "psutil>=7.0.0", 29 | "valkey>=6.0.0", 30 | "pyjwt>=2.10.1", 31 | "structlog>=25.4.0,<26.0.0", 32 | "asgi-correlation-id>=4.3.4", 33 | "redis>=6.2.0", 34 | "aiohttp>=3.12.15", 35 | "semver>=3.0.4", 36 | "packaging>=23.0", 37 | "importlib-metadata>=8.7.0", 38 | ] 39 | 40 | [project.scripts] 41 | agentup = "agent.cli.main:cli" 42 | 43 | [project.optional-dependencies] 44 | agent = ["pytest>=8.0.0", "pytest-asyncio>=0.23.0"] 45 | crewai = ["crewai>=0.157.0"] 46 | dev = [ 47 | "ruff>=0.8.0", 48 | "mypy>=1.13.0", 49 | "bandit[toml]>=1.8.0", 50 | "pytest-cov>=6.0.0", 51 | "pytest-watch>=4.2.0", 52 | "twine>=6.0.0", 53 | "types-pyyaml>=6.0.0", 54 | "types-requests>=2.32.0", 55 | "pre-commit>=4.2.0", 56 | ] 57 | docs = [ 58 | "mkdocs>=1.6.1", 59 | "mkdocs-material>=9.6.16", 60 | "mkdocs-mermaid2-plugin>=1.1.1", 61 | "mkdocs-include-markdown-plugin>=6.2.5", 62 | "mkdocs-minify-plugin>=0.8.0", 63 | "pymdown-extensions>=10.12.1", 64 | "mkdocs-git-committers-plugin>=0.2.3", 65 | "mkdocs-git-revision-date-localized-plugin>=1.4.7", 66 | "mkdocs-glightbox>=0.4.0", 67 | "mkdocs-minify-plugin>=0.8.0", 68 | ] 69 | 70 | [build-system] 71 | requires = ["hatchling"] 72 | build-backend = "hatchling.build" 73 | 74 | [tool.hatch.build.targets.wheel] 75 | packages = ["src/agent"] 76 | 77 | [tool.pytest.ini_options] 78 | testpaths = ["tests"] 79 | python_files = ["test_*.py", "*_test.py"] 80 | python_classes = ["Test*"] 81 | python_functions = ["test_*"] 82 | addopts = "--verbose --tb=short" 83 | filterwarnings = ["ignore::DeprecationWarning"] 84 | 85 | [tool.uv] 86 | package = true 87 | 88 | [tool.ruff] 89 | target-version = "py310" 90 | line-length = 120 91 | exclude = [".git", "__pycache__", "build", "dist", ".venv", "venv"] 92 | 93 | [tool.ruff.lint] 94 | select = [ 95 | "E", # pycodestyle errors 96 | "W", # pycodestyle warnings 97 | "F", # pyflakes 98 | "I", # isort 99 | "B", # flake8-bugbear 100 | "UP", # pyupgrade 101 | ] 102 | ignore = [ 103 | "E501", # line too long (handled by formatter) 104 | "B008", # do not perform function calls in argument defaults 105 | ] 106 | 107 | [tool.ruff.format] 108 | quote-style = "double" 109 | indent-style = "space" 110 | 111 | [tool.mypy] 112 | python_version = "3.11" 113 | warn_return_any = true 114 | warn_unused_configs = true 115 | disallow_untyped_defs = false 116 | disallow_incomplete_defs = false 117 | check_untyped_defs = true 118 | disallow_untyped_decorators = false 119 | no_implicit_optional = true 120 | warn_redundant_casts = true 121 | warn_unused_ignores = true 122 | warn_no_return = true 123 | warn_unreachable = true 124 | strict_equality = true 125 | 126 | [[tool.mypy.overrides]] 127 | module = ["questionary.*", "a2a.*", "fastmcp.*", "authlib.*"] 128 | ignore_missing_imports = true 129 | 130 | [tool.bandit] 131 | exclude_dirs = ["tests", ".venv", "venv", "build", "dist"] 132 | skips = ["B101"] # assert_used 133 | 134 | [tool.pyright] 135 | venvPath = "." 136 | venv = ".venv" 137 | 138 | [dependency-groups] 139 | dev = ["coverage>=7.9.1", "mypy-extensions>=1.1.0", "pathspec>=0.12.1"] 140 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | # Pytest configuration for AgentUp testing 3 | 4 | # Test discovery 5 | testpaths = tests 6 | python_files = test_*.py 7 | python_classes = Test* 8 | python_functions = test_* 9 | 10 | # Markers for test categorization 11 | markers = 12 | unit: Unit tests (fast, no external dependencies) 13 | integration: Integration tests (slower, may use external services) 14 | e2e: End-to-end tests (full system tests) 15 | performance: Performance and load tests 16 | security: Security-focused tests 17 | mcp: MCP (Model Context Protocol) tests 18 | a2a: A2A specification compliance tests 19 | slow: Slow running tests 20 | smoke: Quick smoke tests for basic functionality 21 | fast: Fast-running tests (under 1 second) 22 | stress: Stress and load tests 23 | 24 | # Output settings 25 | addopts = 26 | --strict-markers 27 | --strict-config 28 | --verbose 29 | --tb=short 30 | 31 | # Asyncio settings for async tests 32 | asyncio_mode = auto 33 | 34 | # Warnings 35 | filterwarnings = 36 | ignore::DeprecationWarning 37 | ignore::PendingDeprecationWarning 38 | ignore::UserWarning:httpx.* 39 | ignore::pytest.PytestUnknownMarkWarning -------------------------------------------------------------------------------- /scripts/load_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Usage: ./load_test.sh [request_type] [total_requests] [duration_secs] 4 | # request_type: one | two | both 5 | # total_requests: Number of requests to send 6 | # duration_secs: Duration to spread requests over 7 | 8 | # Check for GNU parallel 9 | if ! command -v parallel &> /dev/null; then 10 | echo "GNU parallel is required. Install it with: sudo apt install parallel" 11 | exit 1 12 | fi 13 | 14 | # Validate input 15 | if [ $# -ne 3 ]; then 16 | echo "Usage: $0 [one|two|both] [total_requests] [duration_secs]" 17 | exit 1 18 | fi 19 | 20 | REQ_TYPE="$1" 21 | TOTAL_REQUESTS="$2" 22 | DURATION="$3" 23 | DELAY=$(awk "BEGIN {print $DURATION / $TOTAL_REQUESTS}") 24 | 25 | URL="http://localhost:8000/" 26 | 27 | BODY_ONE='{ 28 | "jsonrpc": "2.0", 29 | "method": "message/send", 30 | "params": { 31 | "message": { 32 | "role": "user", 33 | "parts": [{ 34 | "kind": "text", 35 | "text": "one" 36 | }], 37 | "message_id": "msg-one", 38 | "kind": "message" 39 | } 40 | }, 41 | "id": "req-one" 42 | }' 43 | 44 | BODY_TWO='{ 45 | "jsonrpc": "2.0", 46 | "method": "message/send", 47 | "params": { 48 | "message": { 49 | "role": "user", 50 | "parts": [{ 51 | "kind": "text", 52 | "text": "two" 53 | }], 54 | "message_id": "msg-one", 55 | "kind": "message" 56 | } 57 | }, 58 | "id": "req-one" 59 | }' 60 | 61 | send_request() { 62 | local TYPE=$1 63 | local BODY="" 64 | if [ "$TYPE" == "one" ]; then 65 | BODY="$BODY_ONE" 66 | elif [ "$TYPE" == "two" ]; then 67 | BODY="$BODY_TWO" 68 | else 69 | echo "Unknown request type: $TYPE" 70 | return 1 71 | fi 72 | 73 | curl -s -X POST "$URL" \ 74 | -H "Content-Type: application/json" \ 75 | -d "$BODY" > /dev/null 76 | } 77 | 78 | export -f send_request 79 | export URL BODY_ONE BODY_TWO 80 | 81 | # Generate request plan 82 | generate_requests() { 83 | for ((i=1; i<=TOTAL_REQUESTS; i++)); do 84 | case "$REQ_TYPE" in 85 | one) echo "send_request one" ;; 86 | two) echo "send_request two" ;; 87 | both) 88 | if (( RANDOM % 2 )); then 89 | echo "send_request one" 90 | else 91 | echo "send_request two" 92 | fi 93 | ;; 94 | *) 95 | echo "Invalid request type: $REQ_TYPE" 96 | exit 1 97 | ;; 98 | esac 99 | sleep "$DELAY" 100 | done 101 | } 102 | 103 | # Run requests with parallel 104 | generate_requests | parallel -j 50 --line-buffer 105 | -------------------------------------------------------------------------------- /scripts/mcp-stream-client.py: -------------------------------------------------------------------------------- 1 | from mcp import ClientSession 2 | from mcp.client.streamable_http import streamablehttp_client 3 | 4 | 5 | async def main(): 6 | # Connect to a streamable HTTP server 7 | print("Connecting to MCP streamable HTTP server...") 8 | async with streamablehttp_client("/mcp") as ( 9 | read_stream, 10 | write_stream, 11 | _, 12 | ): 13 | # Create a session using the client streams 14 | async with ClientSession(read_stream, write_stream) as session: 15 | await session.initialize() 16 | # Call a tool 17 | tool_result = await session.call_tool("echo", {"message": "hello"}) 18 | print(f"Tool result: {tool_result}") 19 | 20 | # List available resources 21 | resources = await session.list_resources() 22 | print(f"Available resources: {resources}") 23 | 24 | 25 | if __name__ == "__main__": 26 | import asyncio 27 | 28 | try: 29 | asyncio.run(main()) 30 | except KeyboardInterrupt: 31 | print("Interrupted by user, exiting...") 32 | except Exception as e: 33 | print(f"An error occurred: {e}") 34 | -------------------------------------------------------------------------------- /scripts/mcp/README.md: -------------------------------------------------------------------------------- 1 | # AgentUp MCP Test Servers 2 | 3 | This directory contains MCP (Model Context Protocol) test servers for integration testing with AgentUp. 4 | 5 | ## Available Servers 6 | 7 | ### Weather Server (`weather_server.py`) 8 | 9 | A unified MCP server that provides weather tools using the National Weather Service API. Supports all three MCP transport types. 10 | 11 | **Features:** 12 | - Weather alerts by US state 13 | - Weather forecasts by coordinates 14 | - Authentication token support for testing config expansion 15 | - Comprehensive error handling and validation 16 | 17 | **Transport Support:** 18 | - `stdio` - Standard input/output transport for subprocess execution 19 | - `sse` - Server-Sent Events transport over HTTP 20 | - `streamable_http` - Streamable HTTP transport 21 | 22 | **Tools:** 23 | - `get_alerts(state: str)` - Get weather alerts for a US state code 24 | - `get_forecast(latitude: float, longitude: float)` - Get weather forecast for coordinates 25 | 26 | ## Usage Examples 27 | 28 | ### Command Line 29 | 30 | ```bash 31 | # Run with stdio transport (for AgentUp subprocess execution) 32 | python weather_server.py --transport stdio 33 | 34 | # Run with SSE transport on custom port 35 | python weather_server.py --transport sse --port 8123 36 | 37 | # Run with streamable HTTP and authentication 38 | python weather_server.py --transport streamable_http --port 8123 --auth-token test-token-123 39 | 40 | # Test environment variable expansion 41 | export WEATHER_TOKEN=my-secret-token 42 | python weather_server.py --transport sse --auth-token $WEATHER_TOKEN 43 | ``` 44 | 45 | ### AgentUp Configuration 46 | 47 | Add to your `agentup.yml`: 48 | 49 | ```yaml 50 | mcp: 51 | enabled: true 52 | client_enabled: true 53 | servers: 54 | # stdio transport 55 | - name: "weather" 56 | transport: "stdio" 57 | command: "python" 58 | args: ["scripts/mcp/weather_server.py", "--transport", "stdio"] 59 | tool_scopes: 60 | get_alerts: ["weather:read"] 61 | get_forecast: ["weather:read"] 62 | 63 | # SSE transport with authentication 64 | - name: "weather" 65 | transport: "sse" 66 | url: "http://localhost:8123/sse" 67 | headers: 68 | Authorization: "Bearer ${WEATHER_TOKEN}" 69 | tool_scopes: 70 | get_alerts: ["weather:read"] 71 | get_forecast: ["weather:read"] 72 | 73 | # Streamable HTTP transport 74 | - name: "weather" 75 | transport: "streamable_http" 76 | url: "http://localhost:8123/mcp" 77 | headers: 78 | Authorization: "Bearer ${WEATHER_TOKEN}" 79 | tool_scopes: 80 | get_alerts: ["weather:read"] 81 | get_forecast: ["weather:read"] 82 | ``` 83 | 84 | ## Testing Integration 85 | 86 | 1. **Start the server:** 87 | ```bash 88 | python scripts/mcp/weather_server.py --transport sse --auth-token test-token-123 89 | ``` 90 | 91 | 2. **Configure AgentUp** with the appropriate transport settings 92 | 93 | 3. **Test via AgentUp API:** 94 | ```bash 95 | curl -X POST http://localhost:8000/ \ 96 | -H "Content-Type: application/json" \ 97 | -H "X-API-Key: admin-key-123" \ 98 | -d '{ 99 | "jsonrpc": "2.0", 100 | "method": "message/send", 101 | "params": { 102 | "message": { 103 | "role": "user", 104 | "parts": [{"kind": "text", "text": "Get weather alerts for California"}], 105 | "message_id": "msg-001", 106 | "kind": "message" 107 | } 108 | }, 109 | "id": "req-001" 110 | }' 111 | ``` 112 | 113 | ## Authentication 114 | 115 | For HTTP-based transports (SSE and streamable_http), the server supports Bearer token authentication: 116 | 117 | - **Header Format:** `Authorization: Bearer ` 118 | - **Environment Variable Testing:** Use `${WEATHER_TOKEN}` in AgentUp config to test variable expansion 119 | - **Token Validation:** Server validates the token and returns 401/403 for invalid/missing tokens 120 | 121 | ## Dependencies 122 | 123 | The weather server requires: 124 | - `mcp>=1.0.0` - Official MCP SDK 125 | - `httpx` - HTTP client for NWS API requests 126 | - `uvicorn` - ASGI server for HTTP transports 127 | - `starlette` - Web framework for middleware 128 | 129 | Install with: 130 | ```bash 131 | uv add "mcp>=1.0.0" httpx uvicorn starlette 132 | ``` 133 | 134 | ## Development 135 | 136 | To create additional MCP test servers: 137 | 138 | 1. Import `FastMCP` from `mcp.server.fastmcp` 139 | 2. Define tools using the `@mcp.tool()` decorator 140 | 3. Support multiple transports using the patterns in `weather_server.py` 141 | 4. Include authentication middleware for HTTP transports 142 | 5. Add comprehensive error handling and validation 143 | 6. Update this README with new server documentation -------------------------------------------------------------------------------- /scripts/webhook_listener.py: -------------------------------------------------------------------------------- 1 | import http.server 2 | import socketserver 3 | 4 | 5 | class WebhookHandler(http.server.BaseHTTPRequestHandler): 6 | def do_POST(self): 7 | content_length = int(self.headers["Content-Length"]) 8 | post_data = self.rfile.read(content_length) 9 | 10 | print("=== WEBHOOK RECEIVED ===") 11 | print(f"Headers: {dict(self.headers)}") 12 | print(f"Body: {post_data.decode()}") 13 | print("========================") 14 | 15 | self.send_response(200) 16 | self.send_header("Content-type", "application/json") 17 | self.end_headers() 18 | self.wfile.write(b'{"status": "received"}') 19 | 20 | 21 | with socketserver.TCPServer(("", 3001), WebhookHandler) as httpd: 22 | print("Webhook server running on http://localhost:3001") 23 | httpd.serve_forever() 24 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | # Empty file to make src a package 2 | -------------------------------------------------------------------------------- /src/agent/__init__.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from .utils.version import get_version 4 | 5 | # Suppress a2a-sdk deprecation warnings for camelCase field aliases 6 | warnings.filterwarnings( 7 | "ignore", 8 | category=DeprecationWarning, 9 | message=r"Setting field .* via its camelCase alias is deprecated.*", 10 | module=r"a2a\..*", 11 | ) 12 | 13 | __version__ = get_version() 14 | 15 | # Lazy imports to avoid loading config when using CLI 16 | # Import these explicitly when needed: 17 | # from agent.api.app import app, create_app, main 18 | # from agent.config import Config 19 | # from agent.core import AgentExecutor, FunctionDispatcher, FunctionExecutor 20 | # from agent.services import get_services, initialize_services 21 | # from agent.state import ConversationManager, get_context_manager 22 | 23 | __all__ = [ 24 | # Main app 25 | "app", 26 | "create_app", 27 | "main", 28 | # Core 29 | "AgentExecutor", 30 | "FunctionDispatcher", 31 | "FunctionExecutor", 32 | # Config 33 | "Config", 34 | # Services 35 | "get_services", 36 | "initialize_services", 37 | # State 38 | "ConversationManager", 39 | "get_context_manager", 40 | ] 41 | 42 | 43 | def __getattr__(name): 44 | if name == "app": 45 | from .api.app import app 46 | 47 | return app 48 | elif name == "create_app": 49 | from .api.app import create_app 50 | 51 | return create_app 52 | elif name == "main": 53 | from .api.app import main 54 | 55 | return main 56 | elif name == "Config": 57 | from .config import Config 58 | 59 | return Config 60 | elif name == "AgentExecutor": 61 | from .core import AgentUpExecutor 62 | 63 | return AgentUpExecutor 64 | elif name == "FunctionDispatcher": 65 | from .core import FunctionDispatcher 66 | 67 | return FunctionDispatcher 68 | elif name == "FunctionExecutor": 69 | from .core import FunctionExecutor 70 | 71 | return FunctionExecutor 72 | elif name == "get_services": 73 | from .services import get_services 74 | 75 | return get_services 76 | elif name == "initialize_services": 77 | from .services import initialize_services 78 | 79 | return initialize_services 80 | elif name == "ConversationManager": 81 | from .state import ConversationManager 82 | 83 | return ConversationManager 84 | elif name == "get_context_manager": 85 | from .state import get_context_manager 86 | 87 | return get_context_manager 88 | raise AttributeError(f"module {__name__!r} has no attribute {name!r}") 89 | -------------------------------------------------------------------------------- /src/agent/a2a/__init__.py: -------------------------------------------------------------------------------- 1 | from .agentcard import clear_agent_card_cache, create_agent_card 2 | 3 | __all__ = [ 4 | "create_agent_card", 5 | "clear_agent_card_cache", 6 | ] 7 | -------------------------------------------------------------------------------- /src/agent/api/__init__.py: -------------------------------------------------------------------------------- 1 | # Import commonly used functions for backwards compatibility 2 | from agent.config import Config 3 | from agent.security.decorators import protected 4 | 5 | from .app import app, create_app, main 6 | from .routes import ( 7 | create_agent_card, 8 | get_request_handler, 9 | router, 10 | set_request_handler_instance, 11 | sse_generator, 12 | ) 13 | 14 | __all__ = [ 15 | "app", 16 | "create_app", 17 | "main", 18 | "create_agent_card", 19 | "get_request_handler", 20 | "router", 21 | "set_request_handler_instance", 22 | "sse_generator", 23 | "Config", 24 | "protected", 25 | ] 26 | -------------------------------------------------------------------------------- /src/agent/capabilities/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | from pathlib import Path 3 | 4 | import structlog 5 | 6 | logger = structlog.get_logger(__name__) 7 | 8 | from .manager import ( # noqa: E402 9 | execute_capabilities, 10 | execute_status, 11 | get_all_capabilities, 12 | get_capability_executor, 13 | list_capabilities, 14 | register_capability, 15 | ) 16 | 17 | 18 | # Dynamic capability discovery and import 19 | def discover_and_import_capabilities(): 20 | capabilities_dir = Path(__file__).parent 21 | discovered_modules = [] 22 | failed_imports = [] 23 | 24 | logger.debug("Starting dynamic capability discovery") 25 | 26 | # TODO: I expect there is a better way to do this, 27 | # this will dynamically import all Python files in the capabilities directory 28 | # except __init__.py and executors.py (core files) 29 | for py_file in capabilities_dir.glob("*.py"): 30 | # Skip __init__.py and executors.py (core files) 31 | if py_file.name in ["__init__.py", "executors.py"]: 32 | continue 33 | 34 | module_name = py_file.stem 35 | 36 | try: 37 | # Try to import the module 38 | importlib.import_module(f".{module_name}", package=__name__) 39 | discovered_modules.append(module_name) 40 | 41 | except ImportError as e: 42 | failed_imports.append((module_name, f"ImportError: {e}")) 43 | logger.error(f"Failed to import capability module {module_name}: {e}") 44 | except SyntaxError as e: 45 | failed_imports.append((module_name, f"SyntaxError: {e}")) 46 | logger.error(f"Syntax error in capability module {module_name}: {e}") 47 | except Exception as e: 48 | failed_imports.append((module_name, f"Exception: {e}")) 49 | logger.error(f"Unexpected error importing capability module {module_name}: {e}", exc_info=True) 50 | 51 | if failed_imports: 52 | logger.warning(f"Failed to import {len(failed_imports)} capability modules:") 53 | for module_name, error in failed_imports: 54 | logger.warning(f" - {module_name}: {error}") 55 | 56 | return discovered_modules, failed_imports 57 | 58 | 59 | # Run dynamic discovery 60 | discovered_modules, failed_imports = discover_and_import_capabilities() 61 | 62 | # Export all public functions and capabilities (core only) 63 | __all__ = [ 64 | # Core capability functions 65 | "get_capability_executor", 66 | "register_capability", 67 | "get_all_capabilities", 68 | "list_capabilities", 69 | # Core capabilities 70 | "execute_status", 71 | "execute_capabilities", 72 | ] 73 | -------------------------------------------------------------------------------- /src/agent/cli/__init__.py: -------------------------------------------------------------------------------- 1 | from .main import cli 2 | 3 | __all__ = ["cli"] 4 | -------------------------------------------------------------------------------- /src/agent/cli/__main__.py: -------------------------------------------------------------------------------- 1 | from .main import cli 2 | 3 | if __name__ == "__main__": 4 | cli() 5 | -------------------------------------------------------------------------------- /src/agent/cli/cli_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tarfile 3 | from collections import OrderedDict 4 | 5 | import click 6 | 7 | 8 | def _is_within_directory(base_dir: str, target_path: str) -> bool: 9 | """ 10 | Return True if the realpath of target_path is inside realpath of base_dir. 11 | """ 12 | base_dir = os.path.abspath(base_dir) 13 | target_path = os.path.abspath(target_path) 14 | return os.path.commonpath([base_dir]) == os.path.commonpath([base_dir, target_path]) 15 | 16 | 17 | def safe_extract(tar: tarfile.TarFile, path: str = ".", members=None) -> None: 18 | """ 19 | Extracts only those members whose final paths stay within `path`. 20 | Raises Exception on any path traversal attempt. 21 | """ 22 | for member in tar.getmembers(): 23 | member_path = os.path.join(path, member.name) 24 | if not _is_within_directory(path, member_path): 25 | raise Exception(f"Path traversal detected in tar member: {member.name!r}") 26 | # Bandit: I am doing this to make you happy! 27 | tar.extractall(path=path, members=members) # nosec 28 | 29 | 30 | class OrderedGroup(click.Group): 31 | def __init__(self, name=None, commands=None, **attrs): 32 | super().__init__(name=name, commands=commands, **attrs) 33 | self.commands = OrderedDict() 34 | 35 | def add_command(self, cmd, name=None): 36 | name = name or cmd.name 37 | self.commands[name] = cmd 38 | 39 | def list_commands(self, ctx): 40 | return self.commands.keys() 41 | -------------------------------------------------------------------------------- /src/agent/cli/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/src/agent/cli/commands/__init__.py -------------------------------------------------------------------------------- /src/agent/cli/commands/init.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | @click.command() 5 | @click.argument("name", required=False) 6 | @click.argument("version", required=False) 7 | @click.option("--quick", "-q", is_flag=True, help="Quick setup with minimal features (non-interactive)") 8 | @click.option("--output-dir", "-o", type=click.Path(), help="Output directory") 9 | @click.option("--config", "-c", type=click.Path(exists=True), help="Use existing agentup.yml as template") 10 | @click.option("--no-git", is_flag=True, help="Skip git repository initialization") 11 | def init(name, version, quick, output_dir, config, no_git): 12 | """Initializes a new agent project.""" 13 | # Import and call the original init_agent functionality 14 | from . import init_agent 15 | 16 | return init_agent.init_agent.callback(name, version, quick, output_dir, config, no_git) 17 | -------------------------------------------------------------------------------- /src/agent/cli/commands/plugin.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from ...utils.version import get_version 4 | from ..cli_utils import OrderedGroup 5 | from .plugin_info import config, info, list_plugins, validate 6 | 7 | # Import subcommands from specialized modules 8 | from .plugin_init import init 9 | from .plugin_manage import add, reload, remove, sync 10 | 11 | # Export all commands and functions 12 | __all__ = [ 13 | "plugin", 14 | "init", 15 | "add", 16 | "remove", 17 | "sync", 18 | "reload", 19 | "list_plugins", 20 | "info", 21 | "config", 22 | "validate", 23 | "get_version", 24 | ] 25 | 26 | 27 | @click.group("plugin", cls=OrderedGroup, help="Manage plugins and their configurations.") 28 | @click.version_option(version=get_version(), prog_name="agentup") 29 | def plugin(): 30 | """Plugin management commands.""" 31 | pass 32 | 33 | 34 | # Register all subcommands 35 | plugin.add_command(init) 36 | plugin.add_command(add) 37 | plugin.add_command(remove) 38 | plugin.add_command(sync) 39 | plugin.add_command(reload) 40 | plugin.add_command(list_plugins) 41 | plugin.add_command(info) 42 | plugin.add_command(config) 43 | plugin.add_command(validate) 44 | -------------------------------------------------------------------------------- /src/agent/cli/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import click 5 | 6 | from ..utils.version import get_version 7 | from .cli_utils import OrderedGroup 8 | from .commands.deploy import deploy 9 | from .commands.init import init 10 | from .commands.mcp import mcp 11 | from .commands.plugin import plugin 12 | from .commands.run import run 13 | from .commands.validate import validate 14 | 15 | 16 | def setup_cli_logging(): 17 | """Sets up unified logging for the CLI using structlog if available.""" 18 | 19 | # Check for explicit log level from environment or default to WARNING 20 | log_level = os.environ.get("AGENTUP_LOG_LEVEL", "WARNING").upper() 21 | is_debug = log_level == "DEBUG" 22 | 23 | try: 24 | from agent.config.logging import setup_logging 25 | from agent.config.model import LogFormat, LoggingConfig, LoggingConsoleConfig 26 | 27 | # In debug mode, allow resolver logs through, otherwise suppress them 28 | resolver_level = "INFO" if is_debug else "CRITICAL" # noqa: F841 29 | cache_level = ( # noqa: F841 30 | "WARNING" if is_debug else "CRITICAL" 31 | ) # Suppress cache debug logs even in debug mode 32 | 33 | # Create logging config 34 | console_config = LoggingConsoleConfig( 35 | enabled=True, 36 | colors=True, 37 | show_time=True, 38 | show_level=True, 39 | ) 40 | cli_logging_config = LoggingConfig( 41 | enabled=True, 42 | level=log_level, 43 | format=LogFormat.TEXT, 44 | console=console_config, 45 | correlation_id=False, 46 | request_logging=False, 47 | structured_data=False, 48 | modules={ 49 | "agent.plugins": "WARNING", # Suppress plugin discovery logs 50 | "agent.plugins.manager": "WARNING", 51 | }, 52 | ) 53 | setup_logging(cli_logging_config) 54 | except (ImportError, Exception): 55 | # Fallback to standard library logging if structlog config fails 56 | logging.basicConfig( 57 | level=getattr(logging, log_level, logging.WARNING), 58 | format="%(message)s", 59 | ) 60 | # Suppress specific noisy loggers in fallback mode 61 | # Suppress specific noisy loggers (but allow them in debug mode) 62 | resolver_log_level = logging.INFO if is_debug else logging.CRITICAL 63 | cache_log_level = logging.WARNING if is_debug else logging.CRITICAL # Suppress cache debug logs 64 | 65 | logging.getLogger("agent.plugins").setLevel(logging.WARNING) 66 | logging.getLogger("agent.plugins.manager").setLevel(logging.WARNING) 67 | logging.getLogger("agent.config.plugin_resolver").setLevel(resolver_log_level) 68 | logging.getLogger("agent.resolver").setLevel(resolver_log_level) 69 | logging.getLogger("agent.resolver.dependency_resolver").setLevel(resolver_log_level) 70 | logging.getLogger("agent.resolver.error_handler").setLevel(resolver_log_level) 71 | logging.getLogger("agent.resolver.reporters").setLevel(resolver_log_level) 72 | logging.getLogger("agent.resolver.providers").setLevel(resolver_log_level) 73 | logging.getLogger("agent.resolver.cache").setLevel(cache_log_level) # Suppress cache debug logs 74 | logging.getLogger("agent.resolver.installer").setLevel(resolver_log_level) 75 | logging.getLogger("agent.resolver.lock_manager").setLevel(resolver_log_level) 76 | logging.getLogger("pluggy").setLevel(logging.WARNING) 77 | 78 | 79 | @click.group( 80 | cls=OrderedGroup, 81 | help="AgentUp CLI - Create and Manage agents and plugins.\n\nUse one of the subcommands below.", 82 | ) 83 | @click.version_option(version=get_version(), prog_name="agentup") 84 | def cli(): 85 | # Set up logging for all CLI commands 86 | setup_cli_logging() 87 | """Main entry point for the AgentUp CLI.""" 88 | pass 89 | 90 | 91 | # Register command groups 92 | cli.add_command(init) 93 | cli.add_command(run) 94 | cli.add_command(deploy) 95 | cli.add_command(validate) 96 | cli.add_command(plugin) 97 | cli.add_command(mcp) 98 | 99 | 100 | if __name__ == "__main__": 101 | cli() 102 | -------------------------------------------------------------------------------- /src/agent/cli/style.py: -------------------------------------------------------------------------------- 1 | """CLI styling and formatting utilities for AgentUp commands.""" 2 | 3 | import click 4 | from questionary import Style 5 | 6 | # Questionary style for interactive prompts 7 | custom_style = Style( 8 | [ 9 | ("qmark", "fg:#5f819d bold"), 10 | ("question", "bold"), 11 | ("answer", "fg:#85678f bold"), 12 | ("pointer", "fg:#5f819d bold"), 13 | ("highlighted", "fg:#5f819d bold"), 14 | ("selected", "fg:#85678f"), 15 | ("separator", "fg:#cc6666"), 16 | ("instruction", "fg:#969896"), 17 | ("text", ""), 18 | ] 19 | ) 20 | 21 | 22 | def print_header(title: str, subtitle: str | None = None) -> None: 23 | """Print a styled header with separator lines.""" 24 | click.secho("─" * 50, fg="white", dim=True) 25 | click.secho(title, fg="cyan", bold=True) 26 | click.secho("─" * 50, fg="white", dim=True) 27 | if subtitle: 28 | click.secho(subtitle + "\n", fg="white") 29 | 30 | 31 | def print_success_footer(message: str, location: str | None = None, docs_url: str | None = None) -> None: 32 | """Print a styled success message with optional location and documentation link.""" 33 | click.secho("\n" + "─" * 50, fg="white", dim=True) 34 | click.secho(message, fg="green", bold=True) 35 | click.secho("─" * 50, fg="white", dim=True) 36 | 37 | if location: 38 | click.secho(f"\nLocation: {location}", fg="cyan") 39 | 40 | if docs_url: 41 | click.secho("\nRead the documentation to get started:", fg="white", bold=True) 42 | click.secho(docs_url, fg="blue", underline=True) 43 | 44 | 45 | def print_error(message: str) -> None: 46 | """Print a styled error message.""" 47 | click.secho(f"Error: {message}", fg="red") 48 | -------------------------------------------------------------------------------- /src/agent/config/__init__.py: -------------------------------------------------------------------------------- 1 | from .a2a import * # noqa: F403 2 | from .constants import * # noqa: F403 3 | from .model import * # noqa: F403 4 | from .plugin_resolver import clear_plugin_resolver, get_plugin_resolver, initialize_plugin_resolver 5 | from .settings import Config, get_config, get_settings 6 | 7 | __all__ = [ 8 | "Config", 9 | "get_config", 10 | "get_settings", 11 | "get_plugin_resolver", 12 | "initialize_plugin_resolver", 13 | "clear_plugin_resolver", 14 | ] 15 | -------------------------------------------------------------------------------- /src/agent/config/a2a.py: -------------------------------------------------------------------------------- 1 | """ 2 | A2A (Agent-to-Agent) Protocol Integration for AgentUp. 3 | 4 | This module provides A2A protocol types, exceptions, and error handling utilities 5 | for JSON-RPC communication between agents. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | # Import official A2A types 11 | from a2a.types import ( 12 | AgentCapabilities, 13 | AgentCard, 14 | AgentCardSignature, 15 | AgentExtension, 16 | AgentProvider, 17 | AgentSkill, 18 | APIKeySecurityScheme, 19 | Artifact, 20 | DataPart, 21 | HTTPAuthSecurityScheme, 22 | In, 23 | JSONRPCMessage, 24 | Message, 25 | Part, 26 | Role, 27 | SecurityScheme, 28 | SendMessageRequest, 29 | Task, 30 | TaskState, 31 | TaskStatus, 32 | TextPart, 33 | ) 34 | 35 | 36 | class TaskNotFoundError(Exception): 37 | pass 38 | 39 | 40 | class TaskNotCancelableError(Exception): 41 | pass 42 | 43 | 44 | class PushNotificationNotSupportedError(Exception): 45 | pass 46 | 47 | 48 | class UnsupportedOperationError(Exception): 49 | pass 50 | 51 | 52 | class ContentTypeNotSupportedError(Exception): 53 | pass 54 | 55 | 56 | class InvalidAgentResponseError(Exception): 57 | pass 58 | 59 | 60 | # A2A Error Code Mapping 61 | A2A_ERROR_CODE_MAP = { 62 | TaskNotFoundError: -32001, 63 | TaskNotCancelableError: -32002, 64 | PushNotificationNotSupportedError: -32003, 65 | UnsupportedOperationError: -32004, 66 | ContentTypeNotSupportedError: -32005, 67 | InvalidAgentResponseError: -32006, 68 | } 69 | 70 | 71 | def get_error_code_for_exception(exception_type: type[Exception]) -> int | None: 72 | """Get the A2A JSON-RPC error code for an exception type. 73 | 74 | Args: 75 | exception_type: The exception class type 76 | 77 | Returns: 78 | The corresponding JSON-RPC error code or None if not found 79 | """ 80 | return A2A_ERROR_CODE_MAP.get(exception_type) 81 | 82 | 83 | # Re-export A2A types and error handling for convenience 84 | __all__ = [ 85 | # A2A protocol types 86 | "AgentCard", 87 | "Artifact", 88 | "DataPart", 89 | "JSONRPCMessage", 90 | "AgentSkill", 91 | "AgentCapabilities", 92 | "AgentExtension", 93 | "AgentProvider", 94 | "AgentCardSignature", 95 | "APIKeySecurityScheme", 96 | "In", 97 | "SecurityScheme", 98 | "HTTPAuthSecurityScheme", 99 | "Message", 100 | "Role", 101 | "SendMessageRequest", 102 | "Task", 103 | "TextPart", 104 | "Part", 105 | "TaskState", 106 | "TaskStatus", 107 | # A2A JSON-RPC exceptions 108 | "TaskNotFoundError", 109 | "TaskNotCancelableError", 110 | "PushNotificationNotSupportedError", 111 | "UnsupportedOperationError", 112 | "ContentTypeNotSupportedError", 113 | "InvalidAgentResponseError", 114 | # A2A error handling utilities 115 | "A2A_ERROR_CODE_MAP", 116 | "get_error_code_for_exception", 117 | ] 118 | -------------------------------------------------------------------------------- /src/agent/config/constants.py: -------------------------------------------------------------------------------- 1 | # General dumping ground for stuff that is going to stay constant across the project. 2 | 3 | # Default model configurations 4 | DEFAULT_MODELS = { 5 | "openai": "gpt-4o-mini", 6 | "anthropic": "claude-3-haiku-20240307", 7 | "ollama": "llama3", 8 | } 9 | 10 | # API Endpoints 11 | DEFAULT_API_ENDPOINTS = { 12 | "openai": "https://api.openai.com/v1", 13 | "anthropic": "https://api.anthropic.com", 14 | "ollama": "http://localhost:11434", 15 | } 16 | 17 | # Database configurations 18 | DEFAULT_DATABASE_URL = "sqlite:///./agent.db" 19 | DEFAULT_VALKEY_URL = "valkey://localhost:6379" 20 | 21 | # Server configuration 22 | # WARNING: "0.0.0.0" binds to all network interfaces, making the agent accessible 23 | # from any network interface. For production, consider: 24 | # - Use "127.0.0.1" for localhost-only access 25 | # - Use specific IP addresses for controlled access 26 | # - Ensure proper firewall rules and authentication are in place 27 | DEFAULT_SERVER_HOST = "0.0.0.0" # nosec B104 - intentional for development 28 | DEFAULT_SERVER_PORT = 8000 29 | 30 | # Timeouts and limits 31 | DEFAULT_HTTP_TIMEOUT = 60.0 32 | DEFAULT_MAX_RETRIES = 3 33 | DEFAULT_CACHE_TTL = 300 34 | 35 | # User agent 36 | DEFAULT_USER_AGENT = "AgentUp-Agent/1.0" 37 | 38 | # Environment variable names 39 | ENV_VARS = { 40 | "OPENAI_API_KEY": "OPENAI_API_KEY", 41 | "ANTHROPIC_API_KEY": "ANTHROPIC_API_KEY", 42 | "OLLAMA_BASE_URL": "OLLAMA_BASE_URL", 43 | "VALKEY_URL": "VALKEY_URL", 44 | "DATABASE_URL": "DATABASE_URL", 45 | "AGENT_CONFIG_PATH": "AGENT_CONFIG_PATH", 46 | "SERVER_HOST": "SERVER_HOST", 47 | "SERVER_PORT": "SERVER_PORT", 48 | } 49 | 50 | 51 | # Security defaults 52 | DEFAULT_JWT_ALGORITHM = "HS256" 53 | DEFAULT_API_KEY_LENGTH = 32 54 | DEFAULT_JWT_SECRET_LENGTH = 64 55 | -------------------------------------------------------------------------------- /src/agent/config/loader.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration loader and saver for AgentUp. 3 | 4 | This module provides functions to load and save AgentConfig instances 5 | from/to YAML configuration files. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | from pathlib import Path 11 | from typing import Any 12 | 13 | import yaml 14 | 15 | from .model import AgentConfig 16 | 17 | 18 | def load_config(file_path: str) -> AgentConfig: 19 | """Load agent configuration from a YAML file.""" 20 | path = Path(file_path) 21 | 22 | if not path.exists(): 23 | # Return default config if file doesn't exist 24 | return AgentConfig() 25 | 26 | with open(path, encoding="utf-8") as f: 27 | data = yaml.safe_load(f) or {} 28 | 29 | # Handle environment variable expansion 30 | data = _expand_env_vars(data) 31 | 32 | return AgentConfig(**data) 33 | 34 | 35 | def save_config(config: AgentConfig, file_path: str) -> None: 36 | """Save agent configuration to a YAML file.""" 37 | path = Path(file_path) 38 | path.parent.mkdir(parents=True, exist_ok=True) 39 | 40 | # Convert config to dict for YAML serialization 41 | # Manually exclude computed fields that shouldn't be saved 42 | data = config.model_dump(exclude_defaults=True, exclude_none=True, by_alias=True) 43 | 44 | # Remove computed fields that cause validation issues when reloading 45 | computed_fields_to_exclude = { 46 | "is_production", 47 | "is_development", 48 | "enabled_services", 49 | "total_service_count", 50 | "security_enabled", 51 | "full_name", 52 | } 53 | for field in computed_fields_to_exclude: 54 | data.pop(field, None) 55 | 56 | with open(path, "w", encoding="utf-8") as f: 57 | yaml.dump(data, f, default_flow_style=False, sort_keys=False, indent=2, allow_unicode=True) 58 | 59 | 60 | def _expand_env_vars(value: Any) -> Any: 61 | """Expand environment variables in configuration values.""" 62 | import os 63 | import re 64 | 65 | if isinstance(value, str): 66 | # Handle ${VAR} and ${VAR:default} patterns 67 | def replace_env_var(match): 68 | var_spec = match.group(1) 69 | if ":" in var_spec: 70 | var_name, default = var_spec.split(":", 1) 71 | else: 72 | var_name, default = var_spec, None 73 | 74 | return os.getenv(var_name, default or match.group(0)) 75 | 76 | return re.sub(r"\$\{([^}]+)\}", replace_env_var, value) 77 | elif isinstance(value, dict): 78 | return {k: _expand_env_vars(v) for k, v in value.items()} 79 | elif isinstance(value, list): 80 | return [_expand_env_vars(item) for item in value] 81 | 82 | return value 83 | -------------------------------------------------------------------------------- /src/agent/config/yaml_source.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from typing import Any 4 | 5 | import yaml 6 | from pydantic.fields import FieldInfo 7 | from pydantic_settings import BaseSettings, PydanticBaseSettingsSource 8 | 9 | from .model import expand_env_vars 10 | 11 | 12 | class YamlConfigSettingsSource(PydanticBaseSettingsSource): 13 | """ 14 | A settings source that reads from a YAML configuration file. 15 | 16 | Supports environment variable substitution using ${VAR_NAME} syntax. 17 | """ 18 | 19 | def __init__( 20 | self, 21 | settings_cls: type[BaseSettings], 22 | yaml_file: Path | str | None = None, 23 | yaml_file_encoding: str | None = None, 24 | ): 25 | super().__init__(settings_cls) 26 | self.yaml_file = Path(yaml_file) if yaml_file else Path("agentup.yml") 27 | self.yaml_file_encoding = yaml_file_encoding or "utf-8" 28 | 29 | def _read_file(self) -> dict[str, Any]: 30 | # Check for config path from environment variable first 31 | env_config_path = os.getenv("AGENT_CONFIG_PATH") 32 | if env_config_path: 33 | self.yaml_file = Path(env_config_path) 34 | 35 | if not self.yaml_file.exists(): 36 | return {} 37 | 38 | try: 39 | with open(self.yaml_file, encoding=self.yaml_file_encoding) as f: 40 | content = yaml.safe_load(f) 41 | if content is None: 42 | return {} 43 | 44 | if "name" in content and "project_name" not in content: 45 | content["project_name"] = content["name"] 46 | # Apply environment variable expansion 47 | expanded_content = expand_env_vars(content) 48 | 49 | return expanded_content if isinstance(expanded_content, dict) else {} 50 | except Exception: 51 | # If there's any error reading the file, return empty dict 52 | return {} 53 | 54 | def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: 55 | # This method should return (None, field_name, False) to indicate 56 | # that this source doesn't have a value for this field 57 | # This allows env variables to take precedence 58 | return None, field_name, False 59 | 60 | def __call__(self) -> dict[str, Any]: 61 | return self._read_file() 62 | -------------------------------------------------------------------------------- /src/agent/core/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import get_current_auth_for_executor, set_current_auth_for_executor 2 | from .dispatcher import FunctionDispatcher 3 | from .executor import AgentUpExecutor 4 | from .function_executor import FunctionExecutor 5 | 6 | __all__ = [ 7 | "AgentUpExecutor", 8 | "FunctionDispatcher", 9 | "FunctionExecutor", 10 | "get_current_auth_for_executor", 11 | "set_current_auth_for_executor", 12 | ] 13 | -------------------------------------------------------------------------------- /src/agent/core/models/__init__.py: -------------------------------------------------------------------------------- 1 | """Core models for AgentUp execution system.""" 2 | 3 | from .configuration import AgentConfiguration, AgentType 4 | from .iteration import ActionResult, FunctionExecutionResult, GoalStatus, IterationState, ReflectionData 5 | from .memory import LearningInsight, LearningType, MemoryContext 6 | 7 | __all__ = [ 8 | "AgentConfiguration", 9 | "AgentType", 10 | "IterationState", 11 | "ReflectionData", 12 | "GoalStatus", 13 | "ActionResult", 14 | "FunctionExecutionResult", 15 | "MemoryContext", 16 | "LearningInsight", 17 | "LearningType", 18 | ] 19 | -------------------------------------------------------------------------------- /src/agent/core/models/configuration.py: -------------------------------------------------------------------------------- 1 | """Configuration models for AgentUp execution system.""" 2 | 3 | from pydantic import BaseModel, Field, field_validator 4 | 5 | from agent.config.model import AgentType, IterativeConfig, MemoryConfig 6 | 7 | 8 | class AgentConfiguration(BaseModel): 9 | """Complete agent configuration model.""" 10 | 11 | model_config = {"use_enum_values": True} 12 | 13 | agent_type: AgentType | str = AgentType.REACTIVE 14 | memory: MemoryConfig = Field(default_factory=MemoryConfig) 15 | iterative: IterativeConfig = Field(default_factory=IterativeConfig) 16 | 17 | @field_validator("agent_type", mode="before") 18 | @classmethod 19 | def validate_agent_type(cls, v): 20 | """Validate and convert agent_type to proper enum value.""" 21 | if isinstance(v, str): 22 | # Handle string values 23 | if v.lower() in ["reactive", "iterative"]: 24 | return v.lower() 25 | else: 26 | raise ValueError(f"Invalid agent_type: {v}. Must be 'reactive' or 'iterative'") 27 | elif isinstance(v, AgentType): 28 | # Handle enum instances 29 | return v.value 30 | else: 31 | # Default to reactive 32 | return AgentType.REACTIVE.value 33 | -------------------------------------------------------------------------------- /src/agent/core/models/iteration.py: -------------------------------------------------------------------------------- 1 | """Iteration state models for self-directed agents.""" 2 | 3 | from datetime import datetime, timezone 4 | from enum import Enum 5 | from typing import Any 6 | 7 | from pydantic import BaseModel, Field 8 | 9 | 10 | class GoalStatus(str, Enum): 11 | """Goal achievement status.""" 12 | 13 | NOT_STARTED = "not_started" 14 | IN_PROGRESS = "in_progress" 15 | PARTIALLY_ACHIEVED = "partially_achieved" 16 | FULLY_ACHIEVED = "fully_achieved" 17 | FAILED = "failed" 18 | REQUIRES_CLARIFICATION = "requires_clarification" 19 | 20 | 21 | class CompletionData(BaseModel): 22 | """Structured completion data from goal completion capability.""" 23 | 24 | summary: str = "Goal completed successfully" 25 | result_content: str = "" # The actual substantive result/answer 26 | confidence: float = 1.0 27 | tasks_completed: list[str] = Field(default_factory=list) 28 | remaining_issues: list[str] = Field(default_factory=list) 29 | 30 | 31 | class ActionResult(BaseModel): 32 | """Result of an action execution.""" 33 | 34 | action: str 35 | tool_used: str | None = None 36 | result: str 37 | success: bool 38 | timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) 39 | metadata: dict[str, Any] = Field(default_factory=dict) 40 | 41 | 42 | class FunctionExecutionResult(BaseModel): 43 | """Result of a function execution with completion signaling.""" 44 | 45 | success: bool 46 | result: Any 47 | completed: bool = False # Signal for goal completion 48 | completion_data: dict[str, Any] = Field(default_factory=dict) 49 | error: str | None = None 50 | timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) 51 | 52 | 53 | class ReflectionData(BaseModel): 54 | """LLM-generated reflection on progress and next steps.""" 55 | 56 | progress_assessment: str = Field(description="LLM assessment of current progress") 57 | goal_achievement_status: GoalStatus 58 | next_action_reasoning: str = Field(description="LLM reasoning for next action") 59 | learned_insights: list[str] = Field(default_factory=list) 60 | challenges_encountered: list[str] = Field(default_factory=list) 61 | estimated_completion: str | None = None 62 | timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) 63 | 64 | 65 | class IterationState(BaseModel): 66 | """Complete state of an iterative agent execution.""" 67 | 68 | iteration_count: int = 0 69 | goal: str 70 | current_plan: list[str] = Field(default_factory=list) 71 | completed_tasks: list[str] = Field(default_factory=list) 72 | action_history: list[ActionResult] = Field(default_factory=list) 73 | reflection_data: ReflectionData | None = None 74 | should_continue: bool = True 75 | context_id: str 76 | task_id: str 77 | started_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) 78 | last_updated: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) 79 | 80 | def add_completed_task(self, task: str) -> None: 81 | """Add a completed task and update timestamp.""" 82 | if task not in self.completed_tasks: 83 | self.completed_tasks.append(task) 84 | self.last_updated = datetime.now(timezone.utc) 85 | 86 | def add_action_result(self, result: ActionResult) -> None: 87 | """Add an action result to history.""" 88 | self.action_history.append(result) 89 | self.last_updated = datetime.now(timezone.utc) 90 | 91 | def update_reflection(self, reflection: ReflectionData) -> None: 92 | """Update reflection data and iteration count.""" 93 | self.reflection_data = reflection 94 | self.iteration_count += 1 95 | self.last_updated = datetime.now(timezone.utc) 96 | 97 | # Update should_continue based on reflection 98 | if reflection.goal_achievement_status == GoalStatus.FULLY_ACHIEVED: 99 | self.should_continue = False 100 | 101 | 102 | class StructuredCompletionResult(BaseModel): 103 | """Structured completion result from goal completion capability.""" 104 | 105 | completed: bool 106 | completion_data: dict[str, Any] = Field(default_factory=dict) 107 | final_response: str = "" 108 | -------------------------------------------------------------------------------- /src/agent/core/models/memory.py: -------------------------------------------------------------------------------- 1 | """Memory integration models for iterative agents.""" 2 | 3 | from datetime import datetime, timezone 4 | from enum import Enum 5 | from typing import Any 6 | 7 | from pydantic import BaseModel, Field 8 | 9 | 10 | class LearningType(str, Enum): 11 | """Types of learning insights.""" 12 | 13 | SUCCESS_PATTERN = "success_pattern" 14 | ERROR_PATTERN = "error_pattern" 15 | OPTIMIZATION = "optimization" 16 | USER_PREFERENCE = "user_preference" 17 | DOMAIN_KNOWLEDGE = "domain_knowledge" 18 | 19 | 20 | class LearningInsight(BaseModel): 21 | """A learning insight extracted from agent execution.""" 22 | 23 | insight: str 24 | learning_type: LearningType 25 | context: str = Field(description="Context where this insight was learned") 26 | confidence: float = Field(default=1.0, ge=0.0, le=1.0) 27 | usage_count: int = 0 28 | first_observed: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) 29 | last_used: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) 30 | metadata: dict[str, Any] = Field(default_factory=dict) 31 | 32 | 33 | class MemoryContext(BaseModel): 34 | """Memory context for agent execution.""" 35 | 36 | context_id: str 37 | agent_type: str 38 | session_start: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) 39 | total_iterations: int = 0 40 | successful_completions: int = 0 41 | failed_attempts: int = 0 42 | learning_insights: list[LearningInsight] = Field(default_factory=list) 43 | common_patterns: dict[str, int] = Field(default_factory=dict) 44 | 45 | def add_insight(self, insight: LearningInsight) -> None: 46 | """Add a learning insight to memory.""" 47 | # Check for duplicate insights 48 | existing = next((i for i in self.learning_insights if i.insight == insight.insight), None) 49 | 50 | if existing: 51 | existing.usage_count += 1 52 | existing.last_used = lambda: datetime.now(timezone.utc)() 53 | existing.confidence = min(existing.confidence + 0.1, 1.0) 54 | else: 55 | self.learning_insights.append(insight) 56 | 57 | def increment_iteration(self) -> None: 58 | """Increment total iterations counter.""" 59 | self.total_iterations += 1 60 | 61 | def mark_success(self) -> None: 62 | """Mark a successful completion.""" 63 | self.successful_completions += 1 64 | 65 | def mark_failure(self) -> None: 66 | """Mark a failed attempt.""" 67 | self.failed_attempts += 1 68 | 69 | @property 70 | def success_rate(self) -> float: 71 | """Calculate success rate.""" 72 | total_attempts = self.successful_completions + self.failed_attempts 73 | if total_attempts == 0: 74 | return 0.0 75 | return self.successful_completions / total_attempts 76 | -------------------------------------------------------------------------------- /src/agent/core/strategies/__init__.py: -------------------------------------------------------------------------------- 1 | """strategies for AgentUp agents.""" 2 | 3 | from .iterative import IterativeStrategy 4 | from .reactive import ReactiveStrategy 5 | 6 | __all__ = [ 7 | "ReactiveStrategy", 8 | "IterativeStrategy", 9 | ] 10 | -------------------------------------------------------------------------------- /src/agent/integrations/__init__.py: -------------------------------------------------------------------------------- 1 | """AgentUp integrations with external agent frameworks.""" 2 | 3 | __all__ = ["crewai"] 4 | -------------------------------------------------------------------------------- /src/agent/integrations/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pydantic import BaseModel, Field 4 | 5 | from agent.integrations.crewai.models import AgentUpConfig 6 | 7 | 8 | class CrewAIIntegrationConfig(BaseModel): 9 | """Configuration for CrewAI integration.""" 10 | 11 | enabled: bool = Field(default=True, description="Enable CrewAI integration") 12 | default_agent_url: str = Field( 13 | default="http://localhost:8000", 14 | description="Default AgentUp agent URL for CrewAI tools", 15 | ) 16 | default_api_key: str | None = Field(default=None, description="Default API key for AgentUp agents") 17 | default_timeout: int = Field(default=30, description="Default timeout for agent requests") 18 | default_max_retries: int = Field(default=3, description="Default maximum number of retries") 19 | auto_discovery: bool = Field(default=False, description="Enable automatic agent discovery") 20 | discovery_urls: list[str] = Field( 21 | default_factory=lambda: ["http://localhost:8000"], 22 | description="URLs to discover agents from", 23 | ) 24 | health_check_interval: int = Field(default=300, description="Health check interval in seconds") 25 | 26 | 27 | class IntegrationConfig(BaseModel): 28 | """Master configuration for all integrations.""" 29 | 30 | crewai: CrewAIIntegrationConfig = Field( 31 | default_factory=CrewAIIntegrationConfig, 32 | description="CrewAI integration configuration", 33 | ) 34 | 35 | @classmethod 36 | def from_env(cls) -> "IntegrationConfig": 37 | """Create configuration from environment variables.""" 38 | crewai_config = CrewAIIntegrationConfig( 39 | enabled=os.getenv("AGENTUP_CREWAI_ENABLED", "true").lower() == "true", 40 | default_agent_url=os.getenv("AGENTUP_URL", "http://localhost:8000"), 41 | default_api_key=os.getenv("AGENTUP_API_KEY"), 42 | default_timeout=int(os.getenv("AGENTUP_TIMEOUT", "30")), 43 | default_max_retries=int(os.getenv("AGENTUP_MAX_RETRIES", "3")), 44 | auto_discovery=os.getenv("AGENTUP_AUTO_DISCOVERY", "false").lower() == "true", 45 | discovery_urls=_parse_urls_from_env(), 46 | health_check_interval=int(os.getenv("AGENTUP_HEALTH_CHECK_INTERVAL", "300")), 47 | ) 48 | 49 | return cls(crewai=crewai_config) 50 | 51 | def to_agentup_config(self, base_url: str | None = None, api_key: str | None = None) -> AgentUpConfig: 52 | """Convert to AgentUpConfig for tool creation.""" 53 | return AgentUpConfig( 54 | base_url=base_url or self.crewai.default_agent_url, 55 | api_key=api_key or self.crewai.default_api_key, 56 | timeout=self.crewai.default_timeout, 57 | max_retries=self.crewai.default_max_retries, 58 | ) 59 | 60 | 61 | def _parse_urls_from_env() -> list[str]: 62 | """Parse URLs from environment variable.""" 63 | urls_str = os.getenv("AGENTUP_URLS", "http://localhost:8000") 64 | return [url.strip() for url in urls_str.split(",")] 65 | 66 | 67 | # Global configuration instance 68 | _config: IntegrationConfig | None = None 69 | 70 | 71 | def get_integration_config() -> IntegrationConfig: 72 | """Get the global integration configuration.""" 73 | global _config 74 | if _config is None: 75 | _config = IntegrationConfig.from_env() 76 | return _config 77 | 78 | 79 | def reload_integration_config() -> IntegrationConfig: 80 | """Reload configuration from environment.""" 81 | global _config 82 | _config = IntegrationConfig.from_env() 83 | return _config 84 | 85 | 86 | # Environment variable reference 87 | INTEGRATION_ENV_VARS = { 88 | "AGENTUP_CREWAI_ENABLED": "Enable/disable CrewAI integration (true/false)", 89 | "AGENTUP_URL": "Default AgentUp agent URL", 90 | "AGENTUP_API_KEY": "Default API key for authentication", 91 | "AGENTUP_TIMEOUT": "Default request timeout in seconds", 92 | "AGENTUP_MAX_RETRIES": "Default maximum number of retries", 93 | "AGENTUP_AUTO_DISCOVERY": "Enable automatic agent discovery (true/false)", 94 | "AGENTUP_URLS": "Comma-separated list of URLs for discovery", 95 | "AGENTUP_HEALTH_CHECK_INTERVAL": "Health check interval in seconds", 96 | } 97 | -------------------------------------------------------------------------------- /src/agent/integrations/crewai/__init__.py: -------------------------------------------------------------------------------- 1 | """CrewAI integration for AgentUp agents.""" 2 | 3 | import warnings 4 | 5 | from .a2a_client import A2AClient 6 | from .discovery import AgentUpDiscovery 7 | 8 | # Check if CrewAI is available 9 | _CREWAI_AVAILABLE = False 10 | try: 11 | import crewai # noqa: F401 12 | 13 | _CREWAI_AVAILABLE = True 14 | except ImportError: 15 | warnings.warn( 16 | "CrewAI not installed. AgentUpTool will not be available. Install with: pip install crewai", 17 | stacklevel=2, 18 | ) 19 | 20 | if _CREWAI_AVAILABLE: 21 | from .agentup_tool import AgentUpTool 22 | 23 | __all__ = ["AgentUpTool", "A2AClient", "AgentUpDiscovery"] 24 | else: 25 | __all__ = ["A2AClient", "AgentUpDiscovery"] 26 | 27 | # Create a custom __getattr__ to handle AgentUpTool imports 28 | def __getattr__(name: str): 29 | if name == "AgentUpTool": 30 | raise ImportError("CrewAI not installed. Install with: pip install agentup[crewai]") 31 | raise AttributeError(f"module '{__name__}' has no attribute '{name}'") 32 | -------------------------------------------------------------------------------- /src/agent/integrations/crewai/models.py: -------------------------------------------------------------------------------- 1 | """Pydantic models for CrewAI integration.""" 2 | 3 | from typing import Any 4 | 5 | from pydantic import BaseModel, Field 6 | 7 | 8 | class A2ARequest(BaseModel): 9 | """A2A JSON-RPC request model.""" 10 | 11 | jsonrpc: str = Field(default="2.0", description="JSON-RPC version") 12 | method: str = Field(..., description="RPC method name") 13 | params: dict[str, Any] = Field(..., description="Method parameters") 14 | id: str = Field(..., description="Request ID") 15 | 16 | 17 | class A2AResponse(BaseModel): 18 | """A2A JSON-RPC response model.""" 19 | 20 | jsonrpc: str = Field(default="2.0", description="JSON-RPC version") 21 | result: dict[str, Any] | None = Field(None, description="Successful result") 22 | error: dict[str, Any] | None = Field(None, description="Error details") 23 | id: str = Field(..., description="Request ID matching the request") 24 | 25 | 26 | class MessagePart(BaseModel): 27 | """Part of a message in A2A protocol.""" 28 | 29 | kind: str = Field(..., description="Type of message part (text, data, etc.)") 30 | text: str | None = Field(None, description="Text content") 31 | data: dict[str, Any] | None = Field(None, description="Data content") 32 | 33 | 34 | class Message(BaseModel): 35 | """A2A message model.""" 36 | 37 | role: str = Field(..., description="Role of the message sender") 38 | parts: list[MessagePart] = Field(..., description="Message parts") 39 | message_id: str = Field(..., description="Unique message ID") 40 | kind: str = Field(default="message", description="Message kind") 41 | 42 | 43 | class AgentUpConfig(BaseModel): 44 | """Configuration for AgentUp integration.""" 45 | 46 | base_url: str = Field( 47 | default="http://localhost:8000", 48 | description="Base URL of the AgentUp agent", 49 | ) 50 | api_key: str | None = Field(None, description="API key for authentication") 51 | timeout: int = Field(default=30, description="Request timeout in seconds") 52 | max_retries: int = Field(default=3, description="Maximum number of retries") 53 | enable_streaming: bool = Field(default=False, description="Enable SSE streaming") 54 | 55 | 56 | class SkillInfo(BaseModel): 57 | """Information about an AgentUp skill from AgentCard.""" 58 | 59 | id: str = Field(..., description="Skill ID") 60 | name: str = Field(..., description="Skill name") 61 | description: str = Field(..., description="Skill description") 62 | input_modes: list[str] = Field(default=["text"], description="Supported input modes") 63 | output_modes: list[str] = Field(default=["text"], description="Supported output modes") 64 | tags: list[str] = Field(default=[], description="Skill tags") 65 | -------------------------------------------------------------------------------- /src/agent/integrations/examples/__init__.py: -------------------------------------------------------------------------------- 1 | """Example CrewAI workflows using AgentUp integration.""" 2 | 3 | __all__ = ["basic_crew", "multi_agent_flow", "streaming_example"] 4 | -------------------------------------------------------------------------------- /src/agent/llm_providers/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import structlog 4 | 5 | from .anthropic import AnthropicProvider 6 | from .base import BaseLLMService, ChatMessage, FunctionCall, LLMResponse 7 | from .ollama import OllamaProvider 8 | from .openai import OpenAIProvider 9 | 10 | logger = structlog.get_logger(__name__) 11 | 12 | # Provider registry 13 | PROVIDER_REGISTRY: dict[str, type[BaseLLMService]] = { 14 | "openai": OpenAIProvider, 15 | "anthropic": AnthropicProvider, 16 | "claude": AnthropicProvider, 17 | "ollama": OllamaProvider, 18 | } 19 | 20 | 21 | def create_llm_provider(provider_type: str, name: str, config: dict[str, Any]) -> BaseLLMService: 22 | """Create an LLM provider instance. 23 | 24 | Args: 25 | provider_type: Type of provider ('openai', 'anthropic', 'ollama', etc.) 26 | name: Name for the provider instance 27 | config: Provider configuration 28 | 29 | Returns: 30 | Configured LLM provider instance 31 | 32 | Raises: 33 | ValueError: If provider type is not supported 34 | """ 35 | provider_type = provider_type.lower() 36 | 37 | if provider_type not in PROVIDER_REGISTRY: 38 | available = ", ".join(PROVIDER_REGISTRY.keys()) 39 | raise ValueError(f"Unsupported LLM provider: {provider_type}. Available: {available}") 40 | 41 | provider_class = PROVIDER_REGISTRY[provider_type] 42 | return provider_class(name, config) 43 | 44 | 45 | def get_available_providers() -> dict[str, type[BaseLLMService]]: 46 | return PROVIDER_REGISTRY.copy() 47 | 48 | 49 | def register_provider(provider_type: str, provider_class: type[BaseLLMService]): 50 | """Register a custom LLM provider. 51 | 52 | Args: 53 | provider_type: Type identifier for the provider 54 | provider_class: Provider class that inherits from BaseLLMService 55 | """ 56 | if not issubclass(provider_class, BaseLLMService): 57 | raise ValueError("Provider class must inherit from BaseLLMService") 58 | 59 | PROVIDER_REGISTRY[provider_type.lower()] = provider_class 60 | logger.info(f"Registered custom LLM provider: {provider_type}") 61 | 62 | 63 | # Export all public components 64 | __all__ = [ 65 | "BaseLLMService", 66 | "LLMResponse", 67 | "ChatMessage", 68 | "FunctionCall", 69 | "OpenAIProvider", 70 | "AnthropicProvider", 71 | "OllamaProvider", 72 | "create_llm_provider", 73 | "get_available_providers", 74 | "register_provider", 75 | "PROVIDER_REGISTRY", 76 | ] 77 | -------------------------------------------------------------------------------- /src/agent/mcp_support/__init__.py: -------------------------------------------------------------------------------- 1 | from .model import ( 2 | MCPCapability, 3 | MCPMessage, 4 | MCPMessageType, 5 | MCPResource, 6 | MCPResourceType, 7 | MCPSession, 8 | MCPSessionState, 9 | MCPTool, 10 | MCPToolType, 11 | create_mcp_validator, 12 | ) 13 | 14 | __all__ = [ 15 | "MCPCapability", 16 | "MCPMessage", 17 | "MCPMessageType", 18 | "MCPResource", 19 | "MCPResourceType", 20 | "MCPSession", 21 | "MCPSessionState", 22 | "MCPTool", 23 | "MCPToolType", 24 | "create_mcp_validator", 25 | ] 26 | -------------------------------------------------------------------------------- /src/agent/middleware/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | AgentUp Middleware System. 3 | 4 | This module provides middleware functionality for the AgentUp framework 5 | including rate limiting, caching, retry logic, and other middleware capabilities. 6 | """ 7 | 8 | import structlog 9 | 10 | from .implementation import ( 11 | RateLimiter, 12 | RateLimitError, 13 | RateLimitExceeded, 14 | apply_ai_routing_middleware, 15 | apply_caching, 16 | apply_rate_limiting, 17 | apply_retry, 18 | cached, 19 | clear_cache, 20 | execute_ai_function_with_middleware, 21 | execute_with_retry, 22 | get_ai_compatible_middleware, 23 | get_cache_stats, 24 | get_rate_limit_stats, 25 | rate_limited, 26 | reset_rate_limits, 27 | retryable, 28 | timed, 29 | with_middleware, 30 | ) 31 | from .model import ( 32 | CacheBackendType, 33 | CacheConfig, 34 | MiddlewareConfig, 35 | MiddlewareError, 36 | MiddlewareRegistry, 37 | MiddlewareType, 38 | RateLimitConfig, 39 | RetryConfig, 40 | create_middleware_validator, 41 | ) 42 | 43 | # Module logger 44 | logger = structlog.get_logger(__name__) 45 | 46 | __all__ = [ 47 | # Models 48 | "CacheBackendType", 49 | "CacheConfig", 50 | "MiddlewareConfig", 51 | "MiddlewareError", 52 | "MiddlewareRegistry", 53 | "MiddlewareType", 54 | "RateLimitConfig", 55 | "RetryConfig", 56 | "create_middleware_validator", 57 | # Functions 58 | "RateLimiter", 59 | "RateLimitError", 60 | "RateLimitExceeded", 61 | "apply_ai_routing_middleware", 62 | "apply_caching", 63 | "apply_rate_limiting", 64 | "apply_retry", 65 | "cached", 66 | "clear_cache", 67 | "execute_ai_function_with_middleware", 68 | "execute_with_retry", 69 | "get_ai_compatible_middleware", 70 | "get_cache_stats", 71 | "get_rate_limit_stats", 72 | "rate_limited", 73 | "reset_rate_limits", 74 | "retryable", 75 | "timed", 76 | "with_middleware", 77 | ] 78 | -------------------------------------------------------------------------------- /src/agent/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import AIFunctionPlugin, Plugin, SimplePlugin 2 | from .decorators import CapabilityMetadata, ai_function, capability 3 | from .integration import enable_plugin_system 4 | from .manager import PluginRegistry, get_plugin_registry 5 | from .models import ( 6 | AIFunction, 7 | CapabilityContext, 8 | CapabilityDefinition, 9 | CapabilityResult, 10 | CapabilityType, 11 | PluginDefinition, 12 | PluginValidationResult, 13 | ) 14 | 15 | __all__ = [ 16 | "Plugin", 17 | "SimplePlugin", 18 | "AIFunctionPlugin", 19 | "capability", 20 | "ai_function", 21 | "CapabilityMetadata", 22 | "PluginRegistry", 23 | "get_plugin_registry", 24 | "enable_plugin_system", 25 | "CapabilityContext", 26 | "CapabilityDefinition", 27 | "CapabilityResult", 28 | "CapabilityType", 29 | "PluginDefinition", 30 | "AIFunction", 31 | "PluginValidationResult", 32 | ] 33 | -------------------------------------------------------------------------------- /src/agent/plugins/registry.py: -------------------------------------------------------------------------------- 1 | """ 2 | Plugin registry module - placeholder for future plugin management. 3 | 4 | This module currently serves as a placeholder. All plugin configuration 5 | and management is handled through explicit configuration in agentup.yml. 6 | """ 7 | 8 | # This file is intentionally minimal. 9 | # Plugin management is handled through configuration, not code. 10 | -------------------------------------------------------------------------------- /src/agent/push/__init__.py: -------------------------------------------------------------------------------- 1 | from .notifier import EnhancedPushNotifier, ValkeyPushNotifier 2 | from .types import * # noqa: F403 3 | 4 | __all__ = [ 5 | "EnhancedPushNotifier", 6 | "ValkeyPushNotifier", 7 | ] 8 | -------------------------------------------------------------------------------- /src/agent/push/types.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from a2a.types import TaskPushNotificationConfig 4 | from pydantic import BaseModel 5 | 6 | 7 | class listTaskPushNotificationConfigParams(BaseModel): 8 | """ 9 | Parameters for the 'tasks/pushNotificationConfig/list' method. 10 | """ 11 | 12 | id: str 13 | """ 14 | The ID of the task. 15 | """ 16 | metadata: dict[str, Any] | None = None 17 | """ 18 | Request-specific metadata. 19 | """ 20 | 21 | 22 | class listTaskPushNotificationConfigRequest(BaseModel): 23 | """ 24 | JSON-RPC request model for the 'tasks/pushNotificationConfig/list' method. 25 | """ 26 | 27 | jsonrpc: str = "2.0" 28 | """ 29 | JSON-RPC version. 30 | """ 31 | id: str | int | None = None 32 | """ 33 | Request identifier. 34 | """ 35 | method: str = "tasks/pushNotificationConfig/list" 36 | """ 37 | RPC method name. 38 | """ 39 | params: listTaskPushNotificationConfigParams 40 | """ 41 | Request parameters. 42 | """ 43 | 44 | 45 | class listTaskPushNotificationConfigResponse(BaseModel): 46 | """ 47 | JSON-RPC response model for the 'tasks/pushNotificationConfig/list' method. 48 | """ 49 | 50 | jsonrpc: str = "2.0" 51 | """ 52 | JSON-RPC version. 53 | """ 54 | id: str | int | None = None 55 | """ 56 | Request identifier. 57 | """ 58 | result: list[TaskPushNotificationConfig] 59 | """ 60 | list of push notification configurations for the task. 61 | """ 62 | 63 | 64 | class DeleteTaskPushNotificationConfigParams(BaseModel): 65 | """ 66 | Parameters for the 'tasks/pushNotificationConfig/delete' method. 67 | """ 68 | 69 | id: str 70 | """ 71 | The ID of the task. 72 | """ 73 | pushNotificationConfigId: str 74 | """ 75 | Push notification configuration ID to delete. 76 | """ 77 | metadata: dict[str, Any] | None = None 78 | """ 79 | Request-specific metadata. 80 | """ 81 | 82 | 83 | class DeleteTaskPushNotificationConfigRequest(BaseModel): 84 | """ 85 | JSON-RPC request model for the 'tasks/pushNotificationConfig/delete' method. 86 | """ 87 | 88 | jsonrpc: str = "2.0" 89 | """ 90 | JSON-RPC version. 91 | """ 92 | id: str | int | None = None 93 | """ 94 | Request identifier. 95 | """ 96 | method: str = "tasks/pushNotificationConfig/delete" 97 | """ 98 | RPC method name. 99 | """ 100 | params: DeleteTaskPushNotificationConfigParams 101 | """ 102 | Request parameters. 103 | """ 104 | 105 | 106 | class DeleteTaskPushNotificationConfigResponse(BaseModel): 107 | """ 108 | JSON-RPC response model for the 'tasks/pushNotificationConfig/delete' method. 109 | """ 110 | 111 | jsonrpc: str = "2.0" 112 | """ 113 | JSON-RPC version. 114 | """ 115 | id: str | int | None = None 116 | """ 117 | Request identifier. 118 | """ 119 | result: None = None 120 | """ 121 | Null result for successful deletion. 122 | """ 123 | 124 | 125 | class JSONRPCError(BaseModel): 126 | """ 127 | JSON-RPC error object. 128 | """ 129 | 130 | code: int 131 | """ 132 | Error code. 133 | """ 134 | message: str 135 | """ 136 | Error message. 137 | """ 138 | data: Any | None = None 139 | """ 140 | Additional error data. 141 | """ 142 | 143 | 144 | class JSONRPCErrorResponse(BaseModel): 145 | """ 146 | JSON-RPC error response. 147 | """ 148 | 149 | jsonrpc: str = "2.0" 150 | """ 151 | JSON-RPC version. 152 | """ 153 | id: str | int | None = None 154 | """ 155 | Request identifier. 156 | """ 157 | error: JSONRPCError 158 | """ 159 | Error object. 160 | """ 161 | -------------------------------------------------------------------------------- /src/agent/security/authenticators/__init__.py: -------------------------------------------------------------------------------- 1 | from .api_key import ApiKeyAuthenticator 2 | from .base import BaseAuthenticator 3 | from .bearer import BearerTokenAuthenticator 4 | from .oauth2 import OAuth2Authenticator 5 | 6 | # Registry of available authenticators 7 | AUTHENTICATOR_REGISTRY: dict[str, type[BaseAuthenticator]] = { 8 | "api_key": ApiKeyAuthenticator, 9 | "bearer": BearerTokenAuthenticator, 10 | "oauth2": OAuth2Authenticator, 11 | } 12 | 13 | 14 | def get_authenticator_class(auth_type: str) -> type[BaseAuthenticator]: 15 | """Get authenticator class by type name. 16 | 17 | Args: 18 | auth_type: The authentication type 19 | 20 | Returns: 21 | Type[BaseAuthenticator]: The authenticator class 22 | 23 | Raises: 24 | KeyError: If authenticator type is not found 25 | """ 26 | if auth_type not in AUTHENTICATOR_REGISTRY: 27 | available = ", ".join(AUTHENTICATOR_REGISTRY.keys()) 28 | raise KeyError(f"Unknown authenticator type '{auth_type}'. Available: {available}") 29 | 30 | return AUTHENTICATOR_REGISTRY[auth_type] 31 | 32 | 33 | def list_authenticator_types() -> list[str]: 34 | """Get list of available authenticator types. 35 | 36 | Returns: 37 | list[str]: list of available authenticator type names 38 | """ 39 | return list(AUTHENTICATOR_REGISTRY.keys()) 40 | 41 | 42 | __all__ = [ 43 | "AUTHENTICATOR_REGISTRY", 44 | "get_authenticator_class", 45 | "list_authenticator_types", 46 | "BaseAuthenticator", 47 | "ApiKeyAuthenticator", 48 | "BearerTokenAuthenticator", 49 | "OAuth2Authenticator", 50 | ] 51 | -------------------------------------------------------------------------------- /src/agent/security/authenticators/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod # noqa: F401 2 | from typing import Any, Optional # noqa: F401 3 | 4 | from fastapi import Request # noqa: F401 5 | 6 | from agent.security.base import AuthenticationResult, BaseAuthenticator # noqa: F401 7 | from agent.security.exceptions import SecurityConfigurationException # noqa: F401 8 | -------------------------------------------------------------------------------- /src/agent/security/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any 3 | 4 | from fastapi import Request 5 | 6 | 7 | class AuthenticationResult: 8 | """ 9 | Result of an authentication attempt. Allow the result to be used by various components. 10 | """ 11 | 12 | def __init__( 13 | self, 14 | success: bool, 15 | user_id: str | None = None, 16 | credentials: str | None = None, 17 | scopes: set[str] | None = None, 18 | metadata: dict[str, Any] | None = None, 19 | ): 20 | self.success = success 21 | self.user_id = user_id 22 | self.credentials = credentials # Never log this 23 | self.scopes = scopes or set() 24 | self.metadata = metadata or {} 25 | 26 | 27 | class BaseAuthenticator(ABC): 28 | def __init__(self, config: dict[str, Any]): 29 | self.config = config 30 | self.auth_type = self.__class__.__name__.lower().replace("authenticator", "") 31 | self._validate_config() 32 | 33 | @abstractmethod 34 | def _validate_config(self) -> None: 35 | """Validate the authenticator configuration. 36 | 37 | Raises: 38 | SecurityConfigurationException: If configuration is invalid 39 | """ 40 | pass 41 | 42 | @abstractmethod 43 | async def authenticate(self, request: Request) -> AuthenticationResult: 44 | """Authenticate a request. 45 | 46 | Args: 47 | request: The FastAPI request object 48 | 49 | Returns: 50 | AuthenticationResult: Result of authentication attempt 51 | 52 | Raises: 53 | AuthenticationFailedException: If authentication fails 54 | InvalidCredentialsException: If credentials are invalid 55 | MissingCredentialsException: If required credentials are missing 56 | """ 57 | pass 58 | 59 | @abstractmethod 60 | def get_auth_type(self) -> str: 61 | pass 62 | 63 | def supports_scopes(self) -> bool: 64 | return False 65 | 66 | def get_required_headers(self) -> set[str]: 67 | return set() 68 | 69 | def get_optional_headers(self) -> set[str]: 70 | return set() 71 | 72 | 73 | class SecurityPolicy: 74 | def __init__( 75 | self, 76 | require_authentication: bool = True, 77 | allowed_auth_types: set[str] | None = None, 78 | required_scopes: set[str] | None = None, 79 | allow_anonymous: bool = False, 80 | ): 81 | self.require_authentication = require_authentication 82 | self.allowed_auth_types = allowed_auth_types or set() 83 | self.required_scopes = required_scopes or set() 84 | self.allow_anonymous = allow_anonymous 85 | 86 | def is_auth_type_allowed(self, auth_type: str) -> bool: 87 | if not self.allowed_auth_types: 88 | return True # No restrictions 89 | return auth_type in self.allowed_auth_types 90 | 91 | def has_required_scopes(self, user_scopes: set[str]) -> bool: 92 | if not self.required_scopes: 93 | return True # No scope requirements 94 | return self.required_scopes.issubset(user_scopes) 95 | -------------------------------------------------------------------------------- /src/agent/security/exceptions.py: -------------------------------------------------------------------------------- 1 | class SecurityException(Exception): 2 | def __init__(self, message: str, details: str | None = None): 3 | super().__init__(message) 4 | self.message = message 5 | self.details = details # Internal details, never exposed to clients 6 | 7 | 8 | class AuthenticationFailedException(SecurityException): 9 | pass 10 | 11 | 12 | class AuthorizationFailedException(SecurityException): 13 | pass 14 | 15 | 16 | class InvalidCredentialsException(AuthenticationFailedException): 17 | pass 18 | 19 | 20 | class MissingCredentialsException(AuthenticationFailedException): 21 | pass 22 | 23 | 24 | class InvalidAuthenticationTypeException(SecurityException): 25 | pass 26 | 27 | 28 | class SecurityConfigurationException(SecurityException): 29 | pass 30 | 31 | 32 | class AuthenticatorNotFound(SecurityException): 33 | pass 34 | -------------------------------------------------------------------------------- /src/agent/services/__init__.py: -------------------------------------------------------------------------------- 1 | """AgentUp Service Layer 2 | 3 | This module provides a service-oriented architecture for the AgentUp framework, 4 | encapsulating core functionality into cohesive, testable services. 5 | 6 | New Services: 7 | - ConfigurationManager: Singleton configuration management with caching 8 | - BuiltinCapabilityRegistry: Unified capability registration and execution 9 | - AgentBootstrapper: Orchestrates service initialization and plugin integration 10 | - SecurityService: Authentication and authorization 11 | - MiddlewareManager: Middleware configuration and application 12 | - StateManager: Conversation and application state management 13 | - MCPService: Model Context Protocol integration 14 | - PushNotificationService: Push notification handling 15 | 16 | Legacy Services (for backwards compatibility): 17 | - ServiceRegistry: External service integration 18 | - CacheService, WebAPIService: External service types 19 | - MultiModalProcessor: Multimodal processing 20 | """ 21 | 22 | # New service layer 23 | # Legacy services for backwards compatibility 24 | from agent.config import Config 25 | 26 | from .base import Service 27 | from .bootstrap import AgentBootstrapper 28 | from .builtin_capabilities import BuiltinCapabilityRegistry, CapabilityMetadata 29 | from .config import ConfigurationManager 30 | from .mcp import MCPService 31 | from .middleware import MiddlewareManager 32 | from .multimodal import MultiModalProcessor 33 | from .push import PushNotificationService 34 | from .registry import ( 35 | CacheService, 36 | ServiceError, 37 | ServiceRegistry, 38 | WebAPIService, 39 | get_services, 40 | ) 41 | from .security import SecurityService 42 | from .state import StateManager 43 | 44 | __all__ = [ 45 | # New service layer 46 | "Service", 47 | "ConfigurationManager", 48 | "BuiltinCapabilityRegistry", 49 | "CapabilityMetadata", 50 | "AgentBootstrapper", 51 | "SecurityService", 52 | "MiddlewareManager", 53 | "StateManager", 54 | "MCPService", 55 | "PushNotificationService", 56 | # Legacy services 57 | "get_services", 58 | "initialize_services", 59 | "initialize_services_from_config", 60 | "ServiceError", 61 | "ServiceRegistry", 62 | "CacheService", 63 | "WebAPIService", 64 | "MultiModalProcessor", 65 | "Config", 66 | ] 67 | -------------------------------------------------------------------------------- /src/agent/services/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import TYPE_CHECKING, Any 3 | 4 | import structlog 5 | 6 | if TYPE_CHECKING: 7 | from .config import ConfigurationManager 8 | 9 | 10 | class Service(ABC): 11 | """Base service class with lifecycle management. 12 | 13 | All services in the AgentUp framework should inherit from this class 14 | to ensure consistent lifecycle management and configuration access. 15 | """ 16 | 17 | def __init__(self, config_manager: "ConfigurationManager"): 18 | """Initialize the service with configuration manager. 19 | 20 | Args: 21 | config_manager: Singleton configuration manager instance 22 | """ 23 | self.config = config_manager 24 | self._initialized = False 25 | self.logger = structlog.get_logger(self.__class__.__name__) 26 | 27 | @abstractmethod 28 | async def initialize(self) -> None: 29 | """Initialize the service. 30 | 31 | This method should be called once during application startup. 32 | Services should perform any necessary setup here, such as: 33 | - Loading configuration 34 | - Establishing connections 35 | - Registering handlers 36 | 37 | Raises: 38 | Exception: If initialization fails 39 | """ 40 | pass 41 | 42 | async def shutdown(self) -> None: 43 | """Cleanup service resources. 44 | 45 | This method is called during application shutdown. 46 | Services should override this to clean up resources such as: 47 | - Closing connections 48 | - Flushing buffers 49 | - Releasing locks 50 | """ 51 | # Base implementation does nothing - services override as needed 52 | self._initialized = False 53 | 54 | @property 55 | def initialized(self) -> bool: 56 | return self._initialized 57 | 58 | def get_config(self, key: str, default: Any = None) -> Any: 59 | """Get configuration value for this service. 60 | 61 | Args: 62 | key: Configuration key 63 | default: Default value if key not found 64 | 65 | Returns: 66 | Configuration value or default 67 | """ 68 | return self.config.get(key, default) 69 | -------------------------------------------------------------------------------- /src/agent/services/config.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | 3 | import structlog 4 | 5 | 6 | class ConfigurationManager: 7 | """Singleton configuration manager with caching. 8 | 9 | This class provides a centralized, cached access to application configuration, 10 | eliminating the need for repeated Config access throughout the codebase. 11 | """ 12 | 13 | _instance: Optional["ConfigurationManager"] = None 14 | _config: dict[str, Any] | None = None 15 | 16 | def __new__(cls): 17 | if cls._instance is None: 18 | cls._instance = super().__new__(cls) 19 | cls._instance.logger = structlog.get_logger(__name__) 20 | return cls._instance 21 | 22 | @property 23 | def config(self) -> dict[str, Any]: 24 | """Get the application configuration with caching. 25 | 26 | Returns: 27 | Dictionary containing the full application configuration 28 | """ 29 | if self._config is None: 30 | self.logger.debug("Loading configuration for the first time") 31 | from agent.config import Config 32 | 33 | self._config = Config.model_dump() 34 | self.logger.info("Configuration loaded successfully") 35 | return self._config 36 | 37 | @property 38 | def pydantic_config(self): 39 | """Get the underlying Pydantic Settings model. 40 | 41 | Returns: 42 | Settings instance with full Pydantic validation and typed access 43 | """ 44 | from agent.config import Config 45 | 46 | return Config 47 | 48 | def get(self, key: str, default: Any = None) -> Any: 49 | """Get a configuration value by key. 50 | 51 | Args: 52 | key: Configuration key (supports nested keys with dot notation) 53 | default: Default value if key not found 54 | 55 | Returns: 56 | Configuration value or default 57 | 58 | Examples: 59 | >>> config.get("agent.name", "DefaultAgent") 60 | >>> config.get("plugins", []) 61 | """ 62 | # Support nested key access with dot notation 63 | if "." in key: 64 | keys = key.split(".") 65 | value = self.config 66 | for k in keys: 67 | if isinstance(value, dict): 68 | value = value.get(k) 69 | if value is None: 70 | return default 71 | else: 72 | return default 73 | return value 74 | 75 | return self.config.get(key, default) 76 | 77 | def reload(self) -> None: 78 | """Force reload configuration. 79 | 80 | This clears the cache and forces a fresh load on next access. 81 | Useful for testing or when configuration changes at runtime. 82 | """ 83 | self.logger.info("Reloading configuration") 84 | self._config = None 85 | 86 | def update(self, updates: dict[str, Any]) -> None: 87 | """Update configuration values. 88 | 89 | Args: 90 | updates: Dictionary of configuration updates 91 | 92 | Note: 93 | This only updates the in-memory configuration and 94 | does not persist changes to disk. 95 | """ 96 | if self._config is None: 97 | _ = self.config # Force load 98 | 99 | self._config.update(updates) 100 | self.logger.debug(f"Configuration updated with keys: {list(updates.keys())}") 101 | 102 | def get_agent_info(self) -> dict[str, str]: 103 | """Get agent information from configuration. 104 | 105 | Returns: 106 | Dictionary with agent name, version, and description 107 | """ 108 | agent_config = self.get("agent", {}) 109 | return { 110 | "name": agent_config.get("name", "Agent"), 111 | "version": agent_config.get("version", "0.5.1"), 112 | "description": agent_config.get("description", "AgentUp Agent"), 113 | } 114 | 115 | def is_feature_enabled(self, feature: str) -> bool: 116 | """Check if a feature is enabled in configuration. 117 | 118 | Args: 119 | feature: Feature name to check 120 | 121 | Returns: 122 | True if feature is enabled, False otherwise 123 | """ 124 | # Use Pydantic models for proper validation 125 | from agent.config import Config 126 | 127 | # Check common feature patterns 128 | if feature == "security": 129 | return Config.security.enabled 130 | elif feature == "mcp": 131 | return Config.mcp.enabled 132 | elif feature == "state_management": 133 | return Config.state_management.get("enabled", False) 134 | elif feature == "plugins": 135 | # Plugins are enabled if any are configured 136 | return bool(Config.plugins) 137 | 138 | # Generic feature check - fallback to dict access 139 | return self.get(f"{feature}.enabled", False) 140 | -------------------------------------------------------------------------------- /src/agent/services/llm/__init__.py: -------------------------------------------------------------------------------- 1 | from .manager import LLMManager 2 | 3 | __all__ = [ 4 | "LLMManager", 5 | ] 6 | -------------------------------------------------------------------------------- /src/agent/services/mcp.py: -------------------------------------------------------------------------------- 1 | from .base import Service 2 | from .builtin_capabilities import BuiltinCapabilityRegistry 3 | from .config import ConfigurationManager 4 | 5 | 6 | class MCPService(Service): 7 | """Manages MCP integration for the agent. 8 | 9 | This service handles: 10 | - MCP client/server initialization 11 | - MCP tool registration as capabilities 12 | - MCP HTTP server setup 13 | """ 14 | 15 | def __init__(self, config_manager: ConfigurationManager, capability_registry: BuiltinCapabilityRegistry): 16 | super().__init__(config_manager) 17 | self.capabilities = capability_registry 18 | self._mcp_client = None 19 | self._mcp_server = None 20 | 21 | async def initialize(self) -> None: 22 | self.logger.debug("Initializing MCP service") 23 | 24 | mcp_config = self.config.get("mcp", {}) 25 | if not mcp_config.get("enabled", False): 26 | self.logger.info("MCP integration disabled") 27 | self._initialized = True 28 | return 29 | 30 | try: 31 | # Initialize MCP integration using existing code 32 | from agent.mcp_support.mcp_integration import initialize_mcp_integration 33 | 34 | await initialize_mcp_integration(self.config.config) 35 | 36 | self._initialized = True 37 | 38 | except Exception as e: 39 | self.logger.error(f"Failed to initialize MCP integration: {e}") 40 | raise 41 | 42 | async def shutdown(self) -> None: 43 | self.logger.debug("Shutting down MCP service") 44 | 45 | try: 46 | from agent.mcp_support.mcp_integration import shutdown_mcp_integration 47 | 48 | await shutdown_mcp_integration() 49 | self.logger.info("MCP integration shut down successfully") 50 | 51 | except Exception as e: 52 | self.logger.error(f"Failed to shutdown MCP integration: {e}") 53 | 54 | self._mcp_client = None 55 | self._mcp_server = None 56 | -------------------------------------------------------------------------------- /src/agent/services/middleware.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import Any 3 | 4 | from .base import Service 5 | from .config import ConfigurationManager 6 | 7 | 8 | class MiddlewareManager(Service): 9 | """Manages middleware configuration and application. 10 | 11 | This service centralizes middleware management, providing: 12 | - Global middleware configuration 13 | - Plugin-specific middleware overrides 14 | - Middleware factory methods 15 | """ 16 | 17 | def __init__(self, config_manager: ConfigurationManager): 18 | super().__init__(config_manager) 19 | self._global_config: list[dict[str, Any]] = [] 20 | self._middleware_factories: dict[str, Callable] = {} 21 | 22 | async def initialize(self) -> None: 23 | self.logger.info("Initializing middleware manager") 24 | 25 | # Load global middleware configuration 26 | self._global_config = self.config.get("middleware", []) 27 | 28 | # Register available middleware factories 29 | self._register_middleware_factories() 30 | 31 | self._initialized = True 32 | self.logger.info(f"Middleware manager initialized with {len(self._global_config)} global middleware") 33 | 34 | def _register_middleware_factories(self) -> None: 35 | try: 36 | from agent.middleware import cached, rate_limited, retryable, timed 37 | from agent.middleware.model import CacheConfig, RateLimitConfig, RetryConfig 38 | 39 | self._middleware_factories = { 40 | "timed": lambda params: timed(), 41 | "cached": lambda params: cached(CacheConfig(**params)) if params else cached(), 42 | "rate_limited": lambda params: rate_limited(RateLimitConfig(**params)) if params else rate_limited(), 43 | "retryable": lambda params: retryable(RetryConfig(**params)) if params else retryable(), 44 | } 45 | 46 | self.logger.debug(f"Registered {len(self._middleware_factories)} middleware types") 47 | except ImportError as e: 48 | self.logger.warning(f"Some middleware types not available: {e}") 49 | 50 | def get_global_config(self) -> list[dict[str, Any]]: 51 | return self._global_config.copy() 52 | 53 | def get_middleware_for_plugin(self, plugin_name: str) -> list[dict[str, Any]]: 54 | """Get middleware configuration for a specific plugin. 55 | 56 | Args: 57 | plugin_name: Plugin name/package identifier 58 | 59 | Returns: 60 | List of middleware configurations 61 | """ 62 | # Check for plugin-specific override 63 | plugins = self.config.get("plugins", {}) 64 | 65 | if isinstance(plugins, dict): 66 | # New dictionary-based structure 67 | for package_name, plugin_config in plugins.items(): 68 | if package_name == plugin_name or plugin_config.get("name") == plugin_name: 69 | if "plugin_override" in plugin_config: 70 | self.logger.debug(f"Using plugin override for plugin {plugin_name}") 71 | return plugin_config["plugin_override"] 72 | else: 73 | # Legacy list structure 74 | for plugin in plugins: 75 | if plugin.get("name") == plugin_name: 76 | if "plugin_override" in plugin: 77 | self.logger.debug(f"Using plugin override for plugin {plugin_name}") 78 | return plugin["plugin_override"] 79 | 80 | # Return global config 81 | return self.get_global_config() 82 | 83 | def create_middleware_stack(self, configs: list[dict[str, Any]]) -> Callable: 84 | """Create a middleware stack from configuration. 85 | 86 | Args: 87 | configs: List of middleware configurations 88 | 89 | Returns: 90 | Composed middleware function 91 | """ 92 | try: 93 | from agent.middleware import with_middleware 94 | 95 | return with_middleware(configs) 96 | except ImportError: 97 | self.logger.warning("Middleware module not available") 98 | return lambda f: f # Identity function as fallback 99 | -------------------------------------------------------------------------------- /src/agent/services/push.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from .base import Service 4 | from .config import ConfigurationManager 5 | 6 | 7 | class PushNotificationService(Service): 8 | """Manages push notifications for the agent. 9 | 10 | This service handles: 11 | - Push notification configuration 12 | - Webhook management 13 | - Notification delivery 14 | """ 15 | 16 | def __init__(self, config_manager: ConfigurationManager): 17 | super().__init__(config_manager) 18 | self._push_notifier = None 19 | self._backend = None 20 | 21 | async def initialize(self) -> None: 22 | self.logger.info("Initializing push notification service") 23 | 24 | push_config = self.config.get("push_notifications", {}) 25 | if not push_config.get("enabled", True): 26 | self.logger.info("Push notifications disabled") 27 | self._initialized = True 28 | return 29 | 30 | self._backend = push_config.get("backend", "memory") 31 | 32 | try: 33 | if self._backend == "valkey": 34 | await self._setup_valkey_backend(push_config) 35 | else: 36 | await self._setup_memory_backend() 37 | 38 | self._initialized = True 39 | self.logger.info(f"Push notification service initialized with {self._backend} backend") 40 | 41 | except Exception as e: 42 | self.logger.error(f"Failed to initialize push notification service: {e}") 43 | raise 44 | 45 | async def shutdown(self) -> None: 46 | self.logger.debug("Shutting down push notification service") 47 | self._push_notifier = None 48 | 49 | async def _setup_memory_backend(self) -> None: 50 | import httpx 51 | 52 | from agent.push.notifier import EnhancedPushNotifier 53 | 54 | client = httpx.AsyncClient() 55 | self._push_notifier = EnhancedPushNotifier(client=client) 56 | self.logger.debug("Using memory push notifier") 57 | 58 | async def _setup_valkey_backend(self, push_config: dict[str, Any]) -> None: 59 | try: 60 | import httpx 61 | import valkey.asyncio as valkey 62 | 63 | from agent.push.notifier import ValkeyPushNotifier 64 | from agent.services import get_services 65 | 66 | # Get services and find cache service 67 | services = get_services() 68 | cache_service_name = None 69 | services_config = self.config.get("services", {}) 70 | 71 | for service_name, service_config in services_config.items(): 72 | if service_config.get("type") == "cache": 73 | cache_service_name = service_name 74 | break 75 | 76 | if cache_service_name: 77 | valkey_service = services.get_cache(cache_service_name) 78 | if valkey_service and hasattr(valkey_service, "url"): 79 | valkey_url = valkey_service.url 80 | valkey_client = valkey.from_url(valkey_url) 81 | 82 | # Create Valkey push notifier 83 | client = httpx.AsyncClient() 84 | self._push_notifier = ValkeyPushNotifier( 85 | client=client, 86 | valkey_client=valkey_client, 87 | key_prefix=push_config.get("key_prefix", "agentup:push:"), 88 | validate_urls=push_config.get("validate_urls", True), 89 | ) 90 | self.logger.debug("Using Valkey push notifier") 91 | return 92 | 93 | # Fallback to memory if Valkey setup fails 94 | self.logger.warning("Valkey setup failed, falling back to memory push notifier") 95 | await self._setup_memory_backend() 96 | 97 | except Exception as e: 98 | self.logger.warning(f"Failed to setup Valkey backend: {e}, using memory backend") 99 | await self._setup_memory_backend() 100 | 101 | @property 102 | def push_notifier(self): 103 | return self._push_notifier 104 | -------------------------------------------------------------------------------- /src/agent/services/security.py: -------------------------------------------------------------------------------- 1 | from .base import Service 2 | from .config import ConfigurationManager 3 | 4 | 5 | class SecurityService(Service): 6 | """Manages security and authentication for the agent. 7 | 8 | This service consolidates all security-related functionality, 9 | including authentication, authorization, and security context management. 10 | """ 11 | 12 | def __init__(self, config_manager: ConfigurationManager): 13 | super().__init__(config_manager) 14 | self._security_manager = None 15 | 16 | async def initialize(self) -> None: 17 | self.logger.info("Initializing security service") 18 | 19 | try: 20 | # Create security manager using existing implementation 21 | from agent.config import Config 22 | from agent.security import create_security_manager, set_global_security_manager 23 | 24 | # Pass the full config as dictionary for backward compatibility 25 | config_dict = Config.model_dump() 26 | self._security_manager = create_security_manager(config_dict) 27 | set_global_security_manager(self._security_manager) 28 | 29 | if self._security_manager.is_auth_enabled(): 30 | auth_type = self._security_manager.get_primary_auth_type() 31 | self.logger.info(f"Security enabled with {auth_type} authentication") 32 | else: 33 | self.logger.warning("Security disabled - all endpoints are UNPROTECTED") 34 | 35 | self._initialized = True 36 | 37 | except Exception as e: 38 | self.logger.error(f"Failed to initialize security service: {e}") 39 | raise 40 | 41 | async def shutdown(self) -> None: 42 | self.logger.debug("Shutting down security service") 43 | self._security_manager = None 44 | 45 | @property 46 | def security_manager(self): 47 | return self._security_manager 48 | 49 | def is_enabled(self) -> bool: 50 | return self._security_manager is not None and self._security_manager.is_auth_enabled() 51 | -------------------------------------------------------------------------------- /src/agent/services/state.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from .base import Service 4 | from .config import ConfigurationManager 5 | 6 | 7 | class StateManager(Service): 8 | """Manages conversation and application state. 9 | 10 | This service provides centralized state management with support for: 11 | - Multiple backends (memory, file, valkey) 12 | - Context management 13 | - State persistence 14 | """ 15 | 16 | def __init__(self, config_manager: ConfigurationManager): 17 | super().__init__(config_manager) 18 | self._context_manager = None 19 | self._backend = None 20 | self._backend_config = {} 21 | 22 | async def initialize(self) -> None: 23 | self.logger.info("Initializing state manager") 24 | 25 | state_config = self.config.get("state_management", {}) 26 | if not state_config.get("enabled", False): 27 | self.logger.info("State management disabled") 28 | self._initialized = True 29 | return 30 | 31 | self._backend = state_config.get("backend", "memory") 32 | self._backend_config = self._prepare_backend_config(state_config) 33 | 34 | try: 35 | from agent.state.context import get_context_manager 36 | 37 | self._context_manager = get_context_manager(self._backend, **self._backend_config) 38 | 39 | self._initialized = True 40 | self.logger.info(f"State manager initialized with {self._backend} backend") 41 | 42 | except Exception as e: 43 | self.logger.error(f"Failed to initialize state manager: {e}") 44 | raise 45 | 46 | async def shutdown(self) -> None: 47 | if self._context_manager: 48 | try: 49 | # Cleanup old contexts 50 | cleaned = await self._context_manager.cleanup_old_contexts(max_age_hours=24) 51 | if cleaned > 0: 52 | self.logger.info(f"Cleaned up {cleaned} old conversation contexts") 53 | except Exception as e: 54 | self.logger.error(f"Error during state cleanup: {e}") 55 | 56 | self._context_manager = None 57 | 58 | def _prepare_backend_config(self, state_config: dict[str, Any]) -> dict[str, Any]: 59 | backend_config = {} 60 | 61 | if self._backend == "valkey": 62 | # Get Valkey URL from services configuration or state config 63 | valkey_service = self.config.get("services.valkey.config", {}) 64 | backend_config["url"] = valkey_service.get("url", "valkey://localhost:6379") 65 | backend_config["ttl"] = state_config.get("ttl", 3600) 66 | elif self._backend == "file": 67 | backend_config["storage_dir"] = state_config.get("storage_dir", "./conversation_states") 68 | 69 | # Add any additional config from state_config 70 | if "config" in state_config: 71 | backend_config.update(state_config["config"]) 72 | 73 | return backend_config 74 | 75 | @property 76 | def context_manager(self): 77 | return self._context_manager 78 | 79 | @property 80 | def backend_type(self) -> str: 81 | return self._backend 82 | 83 | def is_enabled(self) -> bool: 84 | return self._context_manager is not None 85 | -------------------------------------------------------------------------------- /src/agent/state/__init__.py: -------------------------------------------------------------------------------- 1 | from .context import ConversationContext, get_context_manager 2 | from .conversation import ConversationManager 3 | 4 | __all__ = [ 5 | "ConversationContext", 6 | "get_context_manager", 7 | "ConversationManager", 8 | ] 9 | -------------------------------------------------------------------------------- /src/agent/templates/.env.j2: -------------------------------------------------------------------------------- 1 | # Environment Variables for AgentUp Agent 2 | 3 | # OpenAI API Key (if using OpenAI provider) 4 | # OPENAI_API_KEY=your_openai_api_key_here 5 | 6 | # Anthropic API Key (if using Anthropic provider) 7 | # ANTHROPIC_API_KEY=your_anthropic_api_key_here 8 | 9 | # Ollama Base URL (if using Ollama provider) 10 | # OLLAMA_BASE_URL=http://localhost:11434 11 | 12 | # Valkey/Redis URL (if using Valkey services) 13 | # VALKEY_URL=valkey://localhost:6379 14 | 15 | {% if auth_type == 'oauth2' %} 16 | # OAuth2 Authentication Configuration 17 | {% if oauth2_provider == 'github' %} 18 | # GitHub OAuth2 19 | GITHUB_CLIENT_ID=your_github_client_id 20 | GITHUB_CLIENT_SECRET=your_github_client_secret 21 | # GITHUB_REDIRECT_URI=http://localhost:8000/auth/callback 22 | {% elif oauth2_provider == 'google' %} 23 | # Google OAuth2 24 | GOOGLE_CLIENT_ID=your_google_client_id 25 | GOOGLE_CLIENT_SECRET=your_google_client_secret 26 | # GOOGLE_REDIRECT_URI=http://localhost:8000/auth/callback 27 | {% elif oauth2_provider == 'keycloak' %} 28 | # Keycloak OAuth2 29 | KEYCLOAK_CLIENT_ID=your_keycloak_client_id 30 | KEYCLOAK_CLIENT_SECRET=your_keycloak_client_secret 31 | KEYCLOAK_BASE_URL=https://your-keycloak.com/auth 32 | KEYCLOAK_REALM=your-realm 33 | # KEYCLOAK_REDIRECT_URI=http://localhost:8000/auth/callback 34 | {% else %} 35 | # Generic OAuth2 36 | OAUTH2_CLIENT_ID=your_client_id 37 | OAUTH2_CLIENT_SECRET=your_client_secret 38 | OAUTH2_AUTHORIZATION_URL=https://your-provider.com/oauth/authorize 39 | OAUTH2_TOKEN_URL=https://your-provider.com/oauth/token 40 | # OAUTH2_REDIRECT_URI=http://localhost:8000/auth/callback 41 | OAUTH2_JWKS_URL=https://your-provider.com/.well-known/jwks.json 42 | OAUTH2_JWT_ISSUER=https://your-provider.com 43 | OAUTH2_JWT_AUDIENCE={{ project_name_snake }} 44 | {% endif %} 45 | {% endif %} -------------------------------------------------------------------------------- /src/agent/templates/Dockerfile.j2: -------------------------------------------------------------------------------- 1 | # {{ project_name }} Dockerfile 2 | FROM cgr.dev/chainguard/wolfi-base 3 | 4 | ARG PYTHON_VERSION="3.12" 5 | 6 | RUN apk update && apk upgrade && \ 7 | apk add --no-cache python-$PYTHON_VERSION && \ 8 | apk add --no-cache uv 9 | 10 | WORKDIR /app 11 | 12 | # Copy dependency files 13 | COPY pyproject.toml ./ 14 | 15 | # Install dependencies using uv for faster builds 16 | RUN uv sync 17 | 18 | # Copy application configuration 19 | COPY agentup.yml ./ 20 | COPY .env ./ 21 | 22 | # Create non-root user for security 23 | USER 65532:65532 24 | 25 | # Health check endpoint - using AgentUp's built-in health endpoint 26 | HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ 27 | CMD python -c "import httpx; httpx.get('http://localhost:8000/health', timeout=5.0)" || exit 1 28 | 29 | # Expose the default AgentUp port 30 | EXPOSE 8000 31 | 32 | # Run the AgentUp agent using uvicorn (same as agentup run) 33 | CMD ["uv", "run", "uvicorn", "agent.api.app:app", "--host", "0.0.0.0", "--port", "8000"] 34 | -------------------------------------------------------------------------------- /src/agent/templates/__init__.py: -------------------------------------------------------------------------------- 1 | import questionary 2 | 3 | 4 | def get_feature_choices() -> list[questionary.Choice]: 5 | return [ 6 | questionary.Choice("Authentication Method (API Key, Bearer(JWT), OAuth2)", value="auth", checked=True), 7 | questionary.Choice( 8 | "Context-Aware Middleware (caching, retry, rate limiting)", value="middleware", checked=True 9 | ), 10 | questionary.Choice("State Management (conversation persistence)", value="state_management", checked=True), 11 | questionary.Choice("AI Provider (ollama, openai, anthropic)", value="ai_provider"), 12 | questionary.Choice("MCP Integration (Model Context Protocol)", value="mcp", checked=True), 13 | questionary.Choice("Push Notifications (webhooks)", value="push_notifications"), 14 | questionary.Choice("Development Features (filesystem plugins, debug mode)", value="development"), 15 | questionary.Choice("Deployment (Kubernetes, Helm Charts)", value="deployment"), 16 | ] 17 | -------------------------------------------------------------------------------- /src/agent/templates/docker-compose.yml.j2: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | {{ project_name_snake }}: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | ports: 9 | - "8000:8000" 10 | environment: 11 | - AGENTUP_CONFIG_PATH=/app/agentup.yml 12 | - PYTHONUNBUFFERED=1 13 | {% if has_ai_provider %} # AI Provider Configuration 14 | {% if ai_provider_config.provider == "openai" %} - OPENAI_API_KEY=${OPENAI_API_KEY} 15 | {% elif ai_provider_config.provider == "anthropic" %} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} 16 | {% elif ai_provider_config.provider == "ollama" %} - OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://ollama:11434} 17 | {% endif %}{% endif %} 18 | {% if has_state_management and feature_config.state_backend == "valkey" %} # State Management 19 | - VALKEY_URL=redis://valkey:6379/0 20 | {% endif %} 21 | {% if has_middleware and "cache" in feature_config.middleware and feature_config.cache_backend == "valkey" %} # Cache Backend 22 | - CACHE_BACKEND_URL=redis://valkey:6379/1 23 | {% endif %} 24 | volumes: 25 | - ./agentup.yml:/app/agentup.yml:ro 26 | {% if has_env_file %} - ./.env:/app/.env:ro 27 | {% endif %} 28 | {% if (has_state_management and feature_config.state_backend == "valkey") or (has_middleware and "cache" in feature_config.middleware and feature_config.cache_backend == "valkey") or (ai_provider_config.provider == "ollama") %} depends_on: 29 | {% if has_state_management and feature_config.state_backend == "valkey" %} - valkey 30 | {% elif has_middleware and "cache" in feature_config.middleware and feature_config.cache_backend == "valkey" %} - valkey 31 | {% elif ai_provider_config.provider == "ollama" %} - ollama 32 | {% endif %}{% endif %} 33 | healthcheck: 34 | test: ["CMD", "python", "-c", "import httpx; httpx.get('http://localhost:8000/health', timeout=5.0)"] 35 | interval: 30s 36 | timeout: 10s 37 | retries: 3 38 | start_period: 5s 39 | 40 | {% if (has_state_management and feature_config.state_backend == "valkey") or (has_middleware and "cache" in feature_config.middleware and feature_config.cache_backend == "valkey") %} valkey: 41 | image: cgr.dev/chainguard/valkey:latest 42 | ports: 43 | - "6379:6379" 44 | volumes: 45 | - valkey_data:/data 46 | healthcheck: 47 | test: ["CMD", "valkey-cli", "ping"] 48 | interval: 10s 49 | timeout: 5s 50 | retries: 3 51 | 52 | {% endif %}{% if ai_provider_config.provider == "ollama" %} ollama: 53 | image: ollama/ollama:latest 54 | ports: 55 | - "11434:11434" 56 | volumes: 57 | - ollama_data:/root/.ollama 58 | environment: 59 | - OLLAMA_HOST=0.0.0.0 60 | 61 | {% endif %}volumes: 62 | {% if (has_state_management and feature_config.state_backend == "valkey") or (has_middleware and "cache" in feature_config.middleware and feature_config.cache_backend == "valkey") %} valkey_data: 63 | {% endif %}{% if ai_provider_config.provider == "ollama" %} ollama_data: 64 | {% endif %} -------------------------------------------------------------------------------- /src/agent/templates/helm/Chart.yaml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: {{ project_name_snake }} 3 | description: {{ description }} 4 | type: application 5 | version: 0.1.0 6 | appVersion: "0.1.0" 7 | keywords: 8 | - agentup 9 | - ai-agent 10 | - a2a 11 | home: https://github.com/your-org/{{ project_name_snake }} 12 | maintainers: 13 | - name: {{ project_name }} Team 14 | email: team@example.com 15 | -------------------------------------------------------------------------------- /src/agent/templates/helm/templates/_helpers.tpl.j2: -------------------------------------------------------------------------------- 1 | {{ "{{" }}/* 2 | Expand the name of the chart. 3 | */{{ "}}" }} 4 | {{ "{{-" }} define "{{ project_name_snake }}.name" -{{ "}}" }} 5 | {{ "{{-" }} default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" {{ "}}" }} 6 | {{ "{{-" }} end {{ "}}" }} 7 | 8 | {{ "{{" }}/* 9 | Create a default fully qualified app name. 10 | */{{ "}}" }} 11 | {{ "{{-" }} define "{{ project_name_snake }}.fullname" -{{ "}}" }} 12 | {{ "{{-" }} if .Values.fullnameOverride {{ "}}" }} 13 | {{ "{{-" }} .Values.fullnameOverride | trunc 63 | trimSuffix "-" {{ "}}" }} 14 | {{ "{{-" }} else {{ "}}" }} 15 | {{ "{{-" }} $name := default .Chart.Name .Values.nameOverride {{ "}}" }} 16 | {{ "{{-" }} if contains $name .Release.Name {{ "}}" }} 17 | {{ "{{-" }} .Release.Name | trunc 63 | trimSuffix "-" {{ "}}" }} 18 | {{ "{{-" }} else {{ "}}" }} 19 | {{ "{{-" }} printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" {{ "}}" }} 20 | {{ "{{-" }} end {{ "}}" }} 21 | {{ "{{-" }} end {{ "}}" }} 22 | {{ "{{-" }} end {{ "}}" }} 23 | 24 | {{ "{{" }}/* 25 | Create chart name and version as used by the chart label. 26 | */{{ "}}" }} 27 | {{ "{{-" }} define "{{ project_name_snake }}.chart" -{{ "}}" }} 28 | {{ "{{-" }} printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" {{ "}}" }} 29 | {{ "{{-" }} end {{ "}}" }} 30 | 31 | {{ "{{" }}/* 32 | Common labels 33 | */{{ "}}" }} 34 | {{ "{{-" }} define "{{ project_name_snake }}.labels" -{{ "}}" }} 35 | helm.sh/chart: {{ "{{" }} include "{{ project_name_snake }}.chart" . {{ "}}" }} 36 | {{ "{{" }} include "{{ project_name_snake }}.selectorLabels" . {{ "}}" }} 37 | {{ "{{-" }} if .Chart.AppVersion {{ "}}" }} 38 | app.kubernetes.io/version: {{ "{{" }} .Chart.AppVersion | quote {{ "}}" }} 39 | {{ "{{-" }} end {{ "}}" }} 40 | app.kubernetes.io/managed-by: {{ "{{" }} .Release.Service {{ "}}" }} 41 | {{ "{{-" }} end {{ "}}" }} 42 | 43 | {{ "{{" }}/* 44 | Selector labels 45 | */{{ "}}" }} 46 | {{ "{{-" }} define "{{ project_name_snake }}.selectorLabels" -{{ "}}" }} 47 | app.kubernetes.io/name: {{ "{{" }} include "{{ project_name_snake }}.name" . {{ "}}" }} 48 | app.kubernetes.io/instance: {{ "{{" }} .Release.Name {{ "}}" }} 49 | {{ "{{-" }} end {{ "}}" }} 50 | 51 | {{ "{{" }}/* 52 | Create the name of the service account to use 53 | */{{ "}}" }} 54 | {{ "{{-" }} define "{{ project_name_snake }}.serviceAccountName" -{{ "}}" }} 55 | {{ "{{-" }} if .Values.serviceAccount.create {{ "}}" }} 56 | {{ "{{-" }} default (include "{{ project_name_snake }}.fullname" .) .Values.serviceAccount.name {{ "}}" }} 57 | {{ "{{-" }} else {{ "}}" }} 58 | {{ "{{-" }} default "default" .Values.serviceAccount.name {{ "}}" }} 59 | {{ "{{-" }} end {{ "}}" }} 60 | {{ "{{-" }} end {{ "}}" }} 61 | -------------------------------------------------------------------------------- /src/agent/templates/helm/templates/deployment.yaml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ "{{" }} include "{{ project_name_snake }}.fullname" . {{ "}}" }} 5 | labels: 6 | {{ "{{-" }} include "{{ project_name_snake }}.labels" . | nindent 4 {{ "}}" }} 7 | spec: 8 | {{ "{{-" }} if not .Values.autoscaling.enabled {{ "}}" }} 9 | replicas: {{ "{{" }} .Values.replicaCount {{ "}}" }} 10 | {{ "{{-" }} end {{ "}}" }} 11 | selector: 12 | matchLabels: 13 | {{ "{{-" }} include "{{ project_name_snake }}.selectorLabels" . | nindent 6 {{ "}}" }} 14 | template: 15 | metadata: 16 | {{ "{{-" }} with .Values.podAnnotations {{ "}}" }} 17 | annotations: 18 | {{ "{{-" }} toYaml . | nindent 8 {{ "}}" }} 19 | {{ "{{-" }} end {{ "}}" }} 20 | labels: 21 | {{ "{{-" }} include "{{ project_name_snake }}.selectorLabels" . | nindent 8 {{ "}}" }} 22 | spec: 23 | {{ "{{-" }} with .Values.imagePullSecrets {{ "}}" }} 24 | imagePullSecrets: 25 | {{ "{{-" }} toYaml . | nindent 8 {{ "}}" }} 26 | {{ "{{-" }} end {{ "}}" }} 27 | serviceAccountName: {{ "{{" }} include "{{ project_name_snake }}.serviceAccountName" . {{ "}}" }} 28 | securityContext: 29 | {{ "{{-" }} toYaml .Values.podSecurityContext | nindent 8 {{ "}}" }} 30 | containers: 31 | - name: {{ "{{" }} .Chart.Name {{ "}}" }} 32 | securityContext: 33 | {{ "{{-" }} toYaml .Values.securityContext | nindent 12 {{ "}}" }} 34 | image: "{{ "{{" }} .Values.image.repository {{ "}}" }}:{{ "{{" }} .Values.image.tag | default .Chart.AppVersion {{ "}}" }}" 35 | imagePullPolicy: {{ "{{" }} .Values.image.pullPolicy {{ "}}" }} 36 | ports: 37 | - name: http 38 | containerPort: 8000 39 | protocol: TCP 40 | env: 41 | - name: AGENTUP_CONFIG_PATH 42 | value: {{ "{{" }} .Values.agentup.configPath {{ "}}" }} 43 | - name: PYTHONUNBUFFERED 44 | value: "1" 45 | {{ "{{-" }} range .Values.agentup.env {{ "}}" }} 46 | - name: {{ "{{" }} .name {{ "}}" }} 47 | {{ "{{-" }} if .value {{ "}}" }} 48 | value: {{ "{{" }} .value | quote {{ "}}" }} 49 | {{ "{{-" }} else if .valueFrom {{ "}}" }} 50 | valueFrom: 51 | {{ "{{-" }} toYaml .valueFrom | nindent 16 {{ "}}" }} 52 | {{ "{{-" }} end {{ "}}" }} 53 | {{ "{{-" }} end {{ "}}" }} 54 | livenessProbe: 55 | httpGet: 56 | path: /health 57 | port: http 58 | initialDelaySeconds: 30 59 | periodSeconds: 30 60 | timeoutSeconds: 10 61 | readinessProbe: 62 | httpGet: 63 | path: /health 64 | port: http 65 | initialDelaySeconds: 5 66 | periodSeconds: 10 67 | timeoutSeconds: 5 68 | resources: 69 | {{ "{{-" }} toYaml .Values.resources | nindent 12 {{ "}}" }} 70 | volumeMounts: 71 | - name: config 72 | mountPath: /app/agentup.yml 73 | subPath: agentup.yml 74 | readOnly: true 75 | volumes: 76 | - name: config 77 | configMap: 78 | name: {{ "{{" }} include "{{ project_name_snake }}.fullname" . {{ "}}" }}-config 79 | {{ "{{-" }} with .Values.nodeSelector {{ "}}" }} 80 | nodeSelector: 81 | {{ "{{-" }} toYaml . | nindent 8 {{ "}}" }} 82 | {{ "{{-" }} end {{ "}}" }} 83 | {{ "{{-" }} with .Values.affinity {{ "}}" }} 84 | affinity: 85 | {{ "{{-" }} toYaml . | nindent 8 {{ "}}" }} 86 | {{ "{{-" }} end {{ "}}" }} 87 | {{ "{{-" }} with .Values.tolerations {{ "}}" }} 88 | tolerations: 89 | {{ "{{-" }} toYaml . | nindent 8 {{ "}}" }} 90 | {{ "{{-" }} end {{ "}}" }} 91 | -------------------------------------------------------------------------------- /src/agent/templates/helm/templates/service.yaml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ "{{" }} include "{{ project_name_snake }}.fullname" . {{ "}}" }} 5 | labels: 6 | {{ "{{-" }} include "{{ project_name_snake }}.labels" . | nindent 4 {{ "}}" }} 7 | spec: 8 | type: {{ "{{" }} .Values.service.type {{ "}}" }} 9 | ports: 10 | - port: {{ "{{" }} .Values.service.port {{ "}}" }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{ "{{-" }} include "{{ project_name_snake }}.selectorLabels" . | nindent 4 {{ "}}" }} 16 | -------------------------------------------------------------------------------- /src/agent/templates/helm/values.yaml.j2: -------------------------------------------------------------------------------- 1 | # Default values for {{ project_name_snake }} 2 | replicaCount: 1 3 | 4 | image: 5 | repository: your-registry/{{ project_name_snake }} 6 | pullPolicy: IfNotPresent 7 | tag: "latest" 8 | 9 | imagePullSecrets: [] 10 | nameOverride: "" 11 | fullnameOverride: "" 12 | 13 | serviceAccount: 14 | create: true 15 | annotations: {} 16 | name: "" 17 | 18 | podAnnotations: {} 19 | 20 | podSecurityContext: 21 | fsGroup: 65532 22 | 23 | securityContext: 24 | allowPrivilegeEscalation: false 25 | capabilities: 26 | drop: 27 | - ALL 28 | readOnlyRootFilesystem: true 29 | runAsNonRoot: true 30 | runAsUser: 65532 31 | 32 | service: 33 | type: ClusterIP 34 | port: 8000 35 | 36 | ingress: 37 | enabled: false 38 | className: "" 39 | annotations: {} 40 | hosts: 41 | - host: {{ project_name_snake }}.local 42 | paths: 43 | - path: / 44 | pathType: Prefix 45 | tls: [] 46 | 47 | resources: 48 | limits: 49 | cpu: 500m 50 | memory: 512Mi 51 | requests: 52 | cpu: 100m 53 | memory: 128Mi 54 | 55 | autoscaling: 56 | enabled: false 57 | minReplicas: 1 58 | maxReplicas: 10 59 | targetCPUUtilizationPercentage: 80 60 | targetMemoryUtilizationPercentage: 80 61 | 62 | nodeSelector: {} 63 | 64 | tolerations: [] 65 | 66 | affinity: {} 67 | 68 | # AgentUp specific configuration 69 | agentup: 70 | configPath: /app/agentup.yml 71 | 72 | # Environment variables 73 | env: 74 | - name: PYTHONUNBUFFERED 75 | value: "1" 76 | - name: AGENTUP_CONFIG_PATH 77 | value: "/app/agentup.yml" 78 | 79 | # External services -------------------------------------------------------------------------------- /src/agent/templates/plugins/.github/dependabot.yml.j2: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "uv" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /src/agent/templates/plugins/.github/workflows/ci.yml.j2: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | pull_request: 7 | branches: [ main, develop ] 8 | 9 | jobs: 10 | test: 11 | name: Test Python {% raw %}${{ matrix.python-version }}{% endraw %} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: ["3.11", "3.12"] 17 | 18 | steps: 19 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 20 | 21 | - name: Install uv 22 | uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4 23 | with: 24 | enable-cache: true 25 | cache-dependency-glob: | 26 | **/pyproject.toml 27 | **/uv.lock 28 | 29 | - name: Set up Python {% raw %}${{ matrix.python-version }}{% endraw %} 30 | run: | 31 | uv python install {% raw %}${{ matrix.python-version }}{% endraw %} 32 | uv python pin {% raw %}${{ matrix.python-version }}{% endraw %} 33 | 34 | - name: Install dependencies 35 | run: | 36 | make install-dev 37 | 38 | - name: Check code formatting 39 | run: | 40 | make format-check 41 | 42 | - name: Run linting 43 | run: | 44 | make lint 45 | 46 | build: 47 | name: Build Package 48 | runs-on: ubuntu-latest 49 | needs: test 50 | 51 | steps: 52 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 53 | 54 | - name: Install uv 55 | uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4 56 | with: 57 | enable-cache: true 58 | 59 | - name: Set up Python 60 | run: | 61 | uv python install 3.11 62 | uv python pin 3.11 63 | 64 | - name: Install dependencies 65 | run: | 66 | make install 67 | 68 | - name: Build package 69 | run: | 70 | make build 71 | 72 | - name: Check package 73 | run: | 74 | make build-check 75 | 76 | - name: Upload build artifacts 77 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 78 | with: 79 | name: dist 80 | path: dist/ 81 | -------------------------------------------------------------------------------- /src/agent/templates/plugins/.github/workflows/security.yml.j2: -------------------------------------------------------------------------------- 1 | name: Security Scan 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | pull_request: 7 | branches: [ main, develop ] 8 | schedule: 9 | # Run security scan weekly on Monday at 3 AM UTC 10 | - cron: '0 3 * * 1' 11 | workflow_dispatch: # Allow manual trigger 12 | 13 | jobs: 14 | bandit: 15 | name: Bandit Security Scan 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 20 | 21 | - name: Install uv 22 | uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4 23 | with: 24 | enable-cache: true 25 | 26 | - name: Set up Python 27 | run: | 28 | uv python install 3.11 29 | uv python pin 3.11 30 | 31 | - name: Install dependencies 32 | run: | 33 | uv sync --extra dev 34 | 35 | - name: Run Bandit security scan 36 | run: | 37 | uv run bandit -r src/ -f json -o bandit-report.json || true 38 | uv run bandit -r src/ -f txt 39 | 40 | - name: Upload Bandit report 41 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 42 | if: always() 43 | with: 44 | name: bandit-report 45 | path: bandit-report.json 46 | 47 | dependency-check: 48 | name: Dependency Vulnerability Check 49 | runs-on: ubuntu-latest 50 | 51 | steps: 52 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 53 | 54 | - name: Install uv 55 | uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4 56 | with: 57 | enable-cache: true 58 | 59 | - name: Set up Python 60 | run: | 61 | uv python install 3.11 62 | uv python pin 3.11 63 | 64 | - name: Install dependencies 65 | run: | 66 | make install-dev 67 | 68 | - name: Check for vulnerable dependencies 69 | run: | 70 | uv pip check || true 71 | # Export current dependencies for scanning 72 | uv pip freeze > requirements.txt 73 | 74 | - name: Run pip-audit 75 | run: | 76 | uv pip install pip-audit 77 | uv run pip-audit --desc || true 78 | 79 | - name: Upload dependency report 80 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 81 | if: always() 82 | with: 83 | name: dependency-report 84 | path: requirements.txt 85 | -------------------------------------------------------------------------------- /src/agent/templates/plugins/.gitignore.j2: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | share/python-wheels/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | MANIFEST 24 | 25 | # PyInstaller 26 | *.manifest 27 | *.spec 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .nox/ 33 | .coverage 34 | .coverage.* 35 | .cache 36 | nosetests.xml 37 | coverage.xml 38 | *.cover 39 | *.py,cover 40 | .hypothesis/ 41 | .pytest_cache/ 42 | cover/ 43 | 44 | # Environments 45 | .env 46 | .venv 47 | env/ 48 | venv/ 49 | ENV/ 50 | env.bak/ 51 | venv.bak/ 52 | 53 | # IDE 54 | .vscode/ 55 | .idea/ 56 | *.swp 57 | *.swo 58 | *~ 59 | 60 | # OS 61 | .DS_Store 62 | .DS_Store? 63 | ._* 64 | .Spotlight-V100 65 | .Trashes 66 | ehthumbs.db 67 | Thumbs.db -------------------------------------------------------------------------------- /src/agent/templates/plugins/Makefile.j2: -------------------------------------------------------------------------------- 1 | # {{ plugin_name }} Development Makefile 2 | # Useful commands for testing, template generation, and development of {{ plugin_name }} 3 | 4 | .PHONY: help Install install-dev check-deps test lint lint-fix format format-check 5 | .PHONY: security security-report ci-deps build build-check clean version env-info 6 | .PHONY: dev-setup dev-test dev-full 7 | 8 | # Default target 9 | help: ## Show this help message 10 | @echo "{{ plugin_name }} Development Commands" 11 | @echo "==========================" 12 | @echo "" 13 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\0.4.0m %s\n", $$1, $$2}' 14 | 15 | # Environment setup 16 | install: ## Install dependencies with uv 17 | uv sync --all-extras 18 | @echo "Dependencies installed" 19 | 20 | install-dev: ## Install development dependencies 21 | uv sync --all-extras --dev 22 | uv pip install -e . 23 | @echo "Development environment ready" 24 | 25 | check-deps: ## Check for missing dependencies 26 | uv pip check 27 | @echo "All dependencies satisfied" 28 | 29 | # Testing commands 30 | test: ## Run all tests (unit + integration + e2e) 31 | @echo "Running comprehensive test suite..." 32 | uv run pytest tests/ -v 33 | 34 | # Code quality 35 | lint: ## Run linting checks 36 | uv run ruff check src/ tests/ 37 | 38 | lint-fix: ## Fix linting issues automatically 39 | uv run ruff check --fix src/ tests/ 40 | uv run ruff format src/ tests/ 41 | 42 | format: ## Format code with ruff 43 | uv run ruff format src/ tests/ 44 | 45 | format-check: ## Check code formatting 46 | uv run ruff format --check src/ tests/ 47 | 48 | # Security scanning 49 | security: ## Run bandit security scan 50 | uv run bandit -r src/ -ll 51 | 52 | security-report: ## Generate bandit security report in JSON 53 | uv run bandit -r src/ -f json -o bandit-report.json 54 | 55 | security-full: ## Run full security scan with medium severity 56 | uv run bandit -r src/ -l 57 | 58 | ci-deps: ## Check dependencies for CI 59 | uv pip check 60 | uv pip freeze > requirements-ci.txt 61 | 62 | 63 | # Build and release 64 | build: ## Build package 65 | uv build 66 | @echo "Package built in dist/" 67 | 68 | build-check: ## Check package build 69 | uv run twine check dist/* 70 | 71 | # Cleanup commands 72 | clean: ## Clean temporary files 73 | rm -rf build/ 74 | rm -rf dist/ 75 | rm -rf *.egg-info/ 76 | rm -rf .pytest_cache/ 77 | rm -rf htmlcov/ 78 | rm -rf .coverage 79 | rm -rf test-render/ 80 | find . -type d -name __pycache__ -exec rm -rf {} + 81 | find . -type f -name "*.pyc" -delete 82 | @echo "🧹 Cleaned temporary files" 83 | 84 | # Utility commands 85 | version: ## Show current version 86 | @python -c "import toml; print('{{ plugin_name }} version:', toml.load('pyproject.toml')['project']['version'])" 87 | 88 | env-info: ## Show environment information 89 | @echo "Environment Information" 90 | @echo "=====================" 91 | @echo "Python version: $$(python --version)" 92 | @echo "UV version: $$(uv --version)" 93 | @echo "Working directory: $$(pwd)" 94 | @echo "Git branch: $$(git branch --show-current 2>/dev/null || echo 'Not a git repo')" 95 | @echo "Git status: $$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') files changed" 96 | 97 | # Quick development workflows 98 | dev-setup: install-dev ## Complete development setup 99 | @echo "Running complete development setup..." 100 | make check-deps 101 | make test-fast 102 | @echo "{{ plugin_name }} development environment ready!" 103 | 104 | dev-test: ## Quick development test cycle 105 | @echo "Running development test cycle..." 106 | make lint-fix 107 | make test-fast 108 | make template-test-syntax 109 | @echo "{{ plugin_name }} development tests passed!" 110 | 111 | dev-full: ## Full development validation 112 | @echo "Running full development validation..." 113 | make clean 114 | make dev-setup 115 | make validate-all 116 | make agent-create-minimal 117 | make agent-test 118 | @echo "{{ plugin_name }} development validation completed!" 119 | -------------------------------------------------------------------------------- /src/agent/templates/plugins/README.md.j2: -------------------------------------------------------------------------------- 1 | # {{ display_name }} 2 | 3 | {{ description }} 4 | 5 | ## Installation 6 | 7 | ### For development: 8 | ```bash 9 | cd {{ plugin_name }} 10 | pip install -e . 11 | ``` 12 | 13 | ### From AgentUp Registry or PyPi (when published): 14 | ```bash 15 | pip install {{ plugin_name }} 16 | ``` 17 | 18 | ## Usage 19 | 20 | This plugin provides the `{{ capability_id }}` capability to AgentUp agents. 21 | 22 | ## Development 23 | 24 | 1. Edit `src/{{ plugin_name_snake }}/plugin.py` to implement your capability logic 25 | 2. Test locally with an AgentUp agent 26 | 3. Replace the static/logo.png with your plugin's logo (image prompt below) 27 | 4. Publish to PyPI when ready 28 | 5. Update the `README.md` with any additional usage instructions 29 | 30 | ## Configuration 31 | 32 | The capability can be configured in `agentup.yml`: 33 | 34 | ```yaml 35 | plugins: 36 | - plugin_id: {{ capability_id }} 37 | config: 38 | # Add your configuration options here 39 | ``` 40 | 41 | ## Image Generation Prompt 42 | 43 | I had success generating a logo with most of OpenAI's modes (o4-mini, gpt4o seem to work best). 44 | I cannot get anything decent out of Anthropics models 45 | 46 | 47 | ### Example Prompt 48 | ``` 49 | Create a high-quality 2D digital illustration in a modern rubber hose + retro cartoon style, featuring an anthropomorphic vintage computer (CRT monitor with a keyboard as body and gloved hands/feet). 50 | The character should have a friendly expression and be interacting with a themed object (e.g., holding a camera, map, wrench, etc.) depending on the topic. 51 | Use a limited vintage-inspired color palette: muted teal, beige, off-white, faded red, and dark outlines. 52 | Include bold, distressed sans-serif lettering above and below the character, with the top word(s) indicating the app or brand (e.g., "AGENTUP") and the bottom word(s) indicating the tool name (e.g., "IMAGE", "SYS TOOLS", or “MAPS", “SLACK”). 53 | The background should be transparent or a light textured paper tone if transparency isn’t possible. 54 | The overall style should be playful, bold, clean, and ideal for branding, badges, or banners. 55 | The image dimensions must be 400x400, with an alpha channel to provide a transparent background. 56 | 57 | Image Theme: 58 | A computer riding a bicycle, with the words "AGENTUP" and "$YOUR_PLUGIN_NAME" underneath 59 | ``` -------------------------------------------------------------------------------- /src/agent/templates/plugins/__init__.py.j2: -------------------------------------------------------------------------------- 1 | """Plugin: {{ display_name }}""" 2 | 3 | from .plugin import {{ class_name }} as Plugin 4 | 5 | __all__ = ["Plugin"] 6 | -------------------------------------------------------------------------------- /src/agent/templates/plugins/pyproject.toml.j2: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "{{ plugin_name }}" 3 | version = "{{ version | default('0.0.1') }}" 4 | description = "{{ description }}" 5 | requires-python = ">=3.11" 6 | authors = [ 7 | { name = "{{ author or 'Your Name' }}"{% if email %}, email = "{{ email }}"{% endif %} } 8 | ] 9 | dependencies = [ 10 | "agentup>={{ agentup_version }}", 11 | ] 12 | 13 | classifiers = [ 14 | "Development Status :: 4 - Beta", 15 | "Intended Audience :: Developers", 16 | "License :: OSI Approved :: Apache Software License", 17 | "Programming Language :: Python :: 3", 18 | "Programming Language :: Python :: 3.11", 19 | "Programming Language :: Python :: 3.12", 20 | ] 21 | 22 | [project.entry-points."agentup.plugins"] 23 | {{ plugin_name_snake }} = "{{ plugin_name_snake }}.plugin:{{ class_name }}" 24 | 25 | [build-system] 26 | requires = ["hatchling"] 27 | build-backend = "hatchling.build" 28 | 29 | [tool.hatch.build.targets.wheel] 30 | packages = ["src/{{ plugin_name_snake }}"] 31 | 32 | # This file is needed to transfer the image logo files to the AgentUp registry 33 | [tool.hatch.build.targets.wheel.force-include] 34 | "static" = "static" 35 | 36 | [tool.pytest.ini_options] 37 | asyncio_mode = "auto" 38 | 39 | [tool.uv] 40 | extra-index-url = ["https://api.agentup.dev/simple"] 41 | -------------------------------------------------------------------------------- /src/agent/templates/plugins/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/src/agent/templates/plugins/static/logo.png -------------------------------------------------------------------------------- /src/agent/templates/plugins/test_plugin.py.j2: -------------------------------------------------------------------------------- 1 | """Tests for {{ display_name }} plugin.""" 2 | 3 | import pytest 4 | from agent.plugins.models import CapabilityContext 5 | from {{ plugin_name_snake }}.plugin import {{ class_name }} 6 | 7 | 8 | @pytest.fixture 9 | def plugin(): 10 | """Create plugin instance for testing.""" 11 | return {{ class_name }}() 12 | 13 | 14 | @pytest.mark.asyncio 15 | async def test_{{ capability_method_name }}(plugin): 16 | """Test the {{ capability_id }} capability.""" 17 | # Create mock context 18 | context = CapabilityContext( 19 | request_id="test-123", 20 | user_id="test-user", 21 | agent_id="test-agent", 22 | conversation_id="test-conv", 23 | message="Test message", 24 | metadata={"parameters": {"input": "test input"}} 25 | ) 26 | 27 | # Execute capability 28 | result = await plugin.{{ capability_method_name }}(context) 29 | 30 | # Verify result 31 | assert result is not None 32 | if isinstance(result, dict): 33 | assert "success" in result 34 | assert "content" in result 35 | else: 36 | assert isinstance(result, str) -------------------------------------------------------------------------------- /src/agent/templates/pyproject.toml.j2: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "{{ project_name_snake }}" 3 | version = "{{ version | default('0.0.1') }}" 4 | description = "{{ description }}" 5 | readme = "README.md" 6 | requires-python = ">=3.11" 7 | authors = [ 8 | { name = "{{ author_info.name | default("Your name") }}", email = "{{ author_info.email | default("email") }}" }, 9 | ] 10 | classifiers = [ 11 | "Development Status :: 3 - Alpha", 12 | "Intended Audience :: Developers", 13 | "License :: OSI Approved :: MIT License", 14 | "Programming Language :: Python :: 3", 15 | "Programming Language :: Python :: 3.11", 16 | "Programming Language :: Python :: 3.12", 17 | ] 18 | dependencies = [ 19 | "agentup>={{ agentup_version }}", # AgentUp framework - provides all core functionality 20 | ] 21 | 22 | [tool.uv] 23 | extra-index-url = ["https://api.agentup.dev/simple"] 24 | -------------------------------------------------------------------------------- /src/agent/templates/system_prompt.txt.j2: -------------------------------------------------------------------------------- 1 | You are {{ project_name }}, an AI agent with access to specific functions/plugins. 2 | 3 | Your role: 4 | - Understand user requests naturally and conversationally 5 | - Use the appropriate functions when needed to help users 6 | - Provide helpful, accurate, and friendly responses 7 | - Maintain context across conversations 8 | 9 | When users ask for something: 10 | 1. If you have a relevant function, call it with appropriate parameters 11 | 2. If multiple functions are needed, call them in logical order 12 | 3. Synthesize the results into a natural, helpful response 13 | 4. If no function is needed, respond conversationally 14 | 15 | Always be helpful, accurate, and maintain a friendly tone. You are designed to assist users effectively while being natural and conversational. -------------------------------------------------------------------------------- /src/agent/types.py: -------------------------------------------------------------------------------- 1 | """ 2 | Core type definitions for AgentUp. 3 | 4 | This module provides common type aliases and utility types used throughout 5 | the AgentUp codebase to ensure consistent typing. 6 | """ 7 | 8 | from datetime import datetime 9 | from pathlib import Path 10 | from typing import Any 11 | 12 | # Common type aliases 13 | UserId = str 14 | ScopeName = str 15 | IPAddress = str 16 | ModulePath = str 17 | FilePath = str | Path 18 | HeaderName = str 19 | QueryParam = str 20 | CookieName = str 21 | 22 | # HTTP-related types 23 | Headers = dict[str, str] 24 | QueryParams = dict[str, str | list[str]] 25 | FormData = dict[str, str | list[str]] 26 | 27 | # JSON-compatible value type (using Any to avoid recursion) 28 | JsonValue = Any 29 | 30 | # Configuration types 31 | ConfigDict = dict[str, Any] 32 | MetadataDict = dict[str, str] 33 | EnvironmentDict = dict[str, str] 34 | 35 | # Time-related types 36 | Timestamp = datetime 37 | Duration = float # seconds 38 | TTL = int # seconds 39 | 40 | # Service types 41 | ServiceName = str 42 | ServiceType = str 43 | ServiceId = str 44 | 45 | 46 | # Plugin types 47 | PluginId = str 48 | CapabilityId = str 49 | HookName = str 50 | 51 | # Security types 52 | Token = str 53 | Hash = str 54 | Salt = str 55 | Signature = str 56 | 57 | # MCP types 58 | MCPServerName = str 59 | MCPToolName = str 60 | MCPResourceId = str 61 | 62 | # API types 63 | RequestId = str 64 | TaskId = str 65 | SessionId = str 66 | CorrelationId = str 67 | 68 | # Error types 69 | ErrorCode = str 70 | ErrorMessage = str 71 | 72 | # Version type 73 | Version = str 74 | 75 | # URL types 76 | URL = str 77 | WebSocketURL = str 78 | 79 | # File content types 80 | FileContent = str | bytes 81 | MimeType = str 82 | 83 | # Logging types 84 | LogLevel = str 85 | LoggerName = str 86 | 87 | # Database types (for future use) 88 | TableName = str 89 | ColumnName = str 90 | PrimaryKey = str | int 91 | 92 | 93 | # Common constants as types 94 | class HttpMethod: 95 | GET = "GET" 96 | POST = "POST" 97 | PUT = "PUT" 98 | DELETE = "DELETE" 99 | PATCH = "PATCH" 100 | HEAD = "HEAD" 101 | OPTIONS = "OPTIONS" 102 | 103 | 104 | class ContentType: 105 | JSON = "application/json" 106 | XML = "application/xml" 107 | TEXT = "text/plain" 108 | HTML = "text/html" 109 | FORM = "application/x-www-form-urlencoded" 110 | MULTIPART = "multipart/form-data" 111 | BINARY = "application/octet-stream" 112 | 113 | 114 | class AuthScheme: 115 | BEARER = "Bearer" 116 | BASIC = "Basic" 117 | API_KEY = "ApiKey" 118 | OAUTH2 = "OAuth2" 119 | 120 | 121 | # Re-export commonly used types 122 | __all__ = [ 123 | # Core JSON type 124 | "JsonValue", 125 | # Identity types 126 | "UserId", 127 | "ScopeName", 128 | "IPAddress", 129 | "ModulePath", 130 | "FilePath", 131 | # HTTP types 132 | "HeaderName", 133 | "QueryParam", 134 | "CookieName", 135 | "Headers", 136 | "QueryParams", 137 | "FormData", 138 | # Configuration types 139 | "ConfigDict", 140 | "MetadataDict", 141 | "EnvironmentDict", 142 | # Time types 143 | "Timestamp", 144 | "Duration", 145 | "TTL", 146 | # Service types 147 | "ServiceName", 148 | "ServiceType", 149 | "ServiceId", 150 | # Plugin types 151 | "PluginId", 152 | "CapabilityId", 153 | "HookName", 154 | # Security types 155 | "Token", 156 | "Hash", 157 | "Salt", 158 | "Signature", 159 | # MCP types 160 | "MCPServerName", 161 | "MCPToolName", 162 | "MCPResourceId", 163 | # API types 164 | "RequestId", 165 | "TaskId", 166 | "SessionId", 167 | "CorrelationId", 168 | # Error types 169 | "ErrorCode", 170 | "ErrorMessage", 171 | # Other types 172 | "Version", 173 | "URL", 174 | "WebSocketURL", 175 | "FileContent", 176 | "MimeType", 177 | "LogLevel", 178 | "LoggerName", 179 | "TableName", 180 | "ColumnName", 181 | "PrimaryKey", 182 | # Constants 183 | "HttpMethod", 184 | "ContentType", 185 | "AuthScheme", 186 | ] 187 | -------------------------------------------------------------------------------- /src/agent/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .helpers import * # noqa: F403 2 | from .messages import * # noqa: F403 3 | 4 | __all__ = [] 5 | -------------------------------------------------------------------------------- /src/agent/utils/git_utils.py: -------------------------------------------------------------------------------- 1 | import subprocess # nosec 2 | from pathlib import Path 3 | 4 | 5 | def get_git_author_info() -> dict[str, str | None]: 6 | """ 7 | Retrieves the Git user name and email from the local repository configuration. 8 | 9 | Returns: 10 | A dictionary with 'name' and 'email' keys. The values will be strings 11 | if found, otherwise they will be None. 12 | """ 13 | author_info: dict[str, str | None] = {"name": None, "email": None} 14 | 15 | try: 16 | # Get the user name 17 | name_result = subprocess.run( 18 | ["git", "config", "--get", "user.name"], 19 | capture_output=True, 20 | text=True, 21 | check=False, 22 | timeout=5, 23 | ) # nosec 24 | if name_result.returncode == 0: 25 | author_info["name"] = name_result.stdout.strip() or None 26 | 27 | # Get the user email 28 | email_result = subprocess.run( 29 | ["git", "config", "--get", "user.email"], 30 | capture_output=True, 31 | text=True, 32 | check=False, 33 | timeout=5, 34 | ) # nosec 35 | if email_result.returncode == 0: 36 | author_info["email"] = email_result.stdout.strip() or None 37 | 38 | except (FileNotFoundError, subprocess.TimeoutExpired): 39 | # Handle cases where git is not installed or times out 40 | return {"name": None, "email": None} 41 | 42 | return author_info 43 | 44 | 45 | def initialize_git_repo(project_path: Path) -> tuple[bool, str | None]: 46 | """ 47 | Initialize a git repository in the project directory. 48 | 49 | Returns: 50 | tuple[bool, str | None]: (success, error_message) 51 | - success: True if git initialization was successful, False otherwise 52 | - error_message: Error description if failed, None if successful 53 | 54 | Bandit: 55 | This function uses subprocess to run git commands, which is generally safe 56 | as long as the entry point is the CLI command and the project_path is controlled. 57 | """ 58 | try: 59 | subprocess.run(["git", "--version"], check=True, capture_output=True) # nosec 60 | 61 | subprocess.run(["git", "init"], cwd=project_path, check=True, capture_output=True) # nosec 62 | 63 | subprocess.run(["git", "add", "."], cwd=project_path, check=True, capture_output=True) # nosec 64 | 65 | subprocess.run(["git", "commit", "-m", "Initial commit"], cwd=project_path, check=True, capture_output=True) # nosec 66 | 67 | return True, None 68 | 69 | except subprocess.CalledProcessError as e: 70 | return False, f"Git command failed: {e.cmd} (exit code {e.returncode})" 71 | except FileNotFoundError: 72 | return False, "Git not found. Please install Git." 73 | -------------------------------------------------------------------------------- /src/agent/utils/helpers.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | from collections.abc import Callable 3 | from datetime import datetime, timezone 4 | from typing import Any 5 | 6 | from a2a.types import Task 7 | 8 | 9 | def load_callable(path: str | None) -> Callable[..., Any] | None: 10 | """ 11 | Given "some.module:func", import and return the func, 12 | or return None if path is falsy or import fails. 13 | """ 14 | if not path: 15 | return None 16 | module_name, func_name = path.split(":") 17 | try: 18 | module = importlib.import_module(module_name) 19 | return getattr(module, func_name) 20 | except (ImportError, AttributeError): 21 | return None 22 | 23 | 24 | class TaskValidator: 25 | @staticmethod 26 | def validate_task(task: Task) -> list[str]: 27 | errors = [] 28 | 29 | # Check required fields 30 | if not task.id: 31 | errors.append("Task ID is required") 32 | 33 | # Check task metadata structure 34 | if task.metadata is not None and not isinstance(task.metadata, dict): 35 | errors.append("Task metadata must be a dictionary") 36 | 37 | # Validate history if present 38 | if hasattr(task, "history") and task.history: 39 | for i, entry in enumerate(task.history): 40 | if not hasattr(entry, "parts") or not entry.parts: 41 | errors.append(f"History entry {i} missing parts") 42 | 43 | return errors 44 | 45 | @staticmethod 46 | def is_valid_task(task: Task) -> bool: 47 | return len(TaskValidator.validate_task(task)) == 0 48 | 49 | 50 | def extract_parameter(text: str, param_name: str) -> str | None: 51 | import re 52 | 53 | # Pattern for "param_name: value" or "param_name = value" 54 | pattern = rf"{re.escape(param_name)}\s*[:=]\s*([^,\n]+)" 55 | match = re.search(pattern, text, re.IGNORECASE) 56 | if match: 57 | return match.group(1).strip().strip("\"'") 58 | return None 59 | 60 | 61 | def format_response(content: str, format_type: str = "plain") -> str: 62 | if format_type == "markdown": 63 | # Simple markdown formatting 64 | content = content.replace("\n\n", "\n\n---\n\n") 65 | elif format_type == "json": 66 | import json 67 | 68 | try: 69 | # Try to parse and pretty-format JSON 70 | data = json.loads(content) 71 | content = json.dumps(data, indent=2) 72 | except json.JSONDecodeError: 73 | # If not JSON, wrap in JSON structure 74 | content = json.dumps({"response": content}, indent=2) 75 | elif format_type == "html": 76 | # Basic HTML escaping 77 | content = content.replace("&", "&") 78 | content = content.replace("<", "<") 79 | content = content.replace(">", ">") 80 | content = content.replace("\n", "
") 81 | 82 | return content 83 | 84 | 85 | def sanitize_input(text: str) -> str: 86 | # Remove potentially dangerous characters 87 | text = text.replace("", "</script>") 89 | text = text.replace("javascript:", "") 90 | text = text.replace("data:", "") 91 | 92 | # Limit length 93 | if len(text) > 10000: 94 | text = text[:10000] + "... [truncated]" 95 | 96 | return text 97 | 98 | 99 | def generate_task_id() -> str: 100 | import uuid 101 | 102 | return str(uuid.uuid4()) 103 | 104 | 105 | def get_timestamp() -> str: 106 | return datetime.now(timezone.utc).isoformat() 107 | 108 | 109 | # Export utility functions 110 | __all__ = [ 111 | "TaskValidator", 112 | "extract_parameter", 113 | "format_response", 114 | "sanitize_input", 115 | "generate_task_id", 116 | "get_timestamp", 117 | ] 118 | -------------------------------------------------------------------------------- /src/agent/utils/version.py: -------------------------------------------------------------------------------- 1 | """Version management utilities for AgentUp. 2 | 3 | This module provides centralized version reading functionality, ensuring 4 | all parts of the application use the same version information. 5 | """ 6 | 7 | import importlib.metadata 8 | import re 9 | from pathlib import Path 10 | 11 | import structlog 12 | 13 | logger = structlog.get_logger(__name__) 14 | 15 | 16 | def get_version() -> str: 17 | """Get the current AgentUp version. 18 | 19 | Tries multiple methods to determine the version: 20 | 1. From installed package metadata (production/installed mode) 21 | 2. From pyproject.toml parsing (development mode) 22 | 3. Fallback to a default if all else fails 23 | 24 | Returns: 25 | The version string (e.g., "0.5.1") 26 | """ 27 | # Try to get version from installed package metadata first 28 | try: 29 | return importlib.metadata.version("agentup") 30 | except importlib.metadata.PackageNotFoundError: 31 | logger.debug("Package not installed, trying pyproject.toml") 32 | except Exception as e: 33 | logger.debug("Error reading package metadata", error=str(e)) 34 | 35 | # Fallback to reading from pyproject.toml (development mode) 36 | version = _read_version_from_pyproject() 37 | if version: 38 | return version 39 | 40 | # Final fallback 41 | logger.warning("Could not determine version, using fallback") 42 | return "0.0.0-dev" 43 | 44 | 45 | def _read_version_from_pyproject() -> str | None: 46 | """Read version from pyproject.toml file. 47 | 48 | Returns: 49 | Version string if found, None otherwise 50 | """ 51 | try: 52 | # Find pyproject.toml - look up the directory tree from this file 53 | current_path = Path(__file__).resolve() 54 | pyproject_path = None 55 | 56 | # Walk up the directory tree looking for pyproject.toml 57 | for parent in current_path.parents: 58 | candidate = parent / "pyproject.toml" 59 | if candidate.exists(): 60 | pyproject_path = candidate 61 | break 62 | 63 | if not pyproject_path: 64 | logger.debug("pyproject.toml not found") 65 | return None 66 | 67 | # Read and parse the version line 68 | content = pyproject_path.read_text(encoding="utf-8") 69 | 70 | # Look for version = "x.y.z" in the [project] section 71 | pattern = r'^\s*version\s*=\s*["\']([^"\']+)["\']' 72 | for line in content.splitlines(): 73 | match = re.match(pattern, line) 74 | if match: 75 | version = match.group(1) 76 | logger.debug("Found version in pyproject.toml", version=version) 77 | return version 78 | 79 | logger.debug("Version not found in pyproject.toml") 80 | return None 81 | 82 | except Exception as e: 83 | logger.debug("Error reading pyproject.toml", error=str(e)) 84 | return None 85 | 86 | 87 | def get_version_info() -> dict[str, str]: 88 | """Get detailed version information. 89 | 90 | Returns: 91 | Dictionary with version details 92 | """ 93 | version = get_version() 94 | 95 | return { 96 | "version": version, 97 | "package": "agentup", 98 | "source": _get_version_source(), 99 | } 100 | 101 | 102 | def _get_version_source() -> str: 103 | """Determine where the version information came from. 104 | 105 | Returns: 106 | Source description (e.g., "package", "pyproject.toml", "fallback") 107 | """ 108 | try: 109 | importlib.metadata.version("agentup") 110 | return "package" 111 | except importlib.metadata.PackageNotFoundError: 112 | if _read_version_from_pyproject(): 113 | return "pyproject.toml" 114 | return "fallback" 115 | except Exception: 116 | return "error" 117 | 118 | 119 | def to_version_case(version: str) -> str | None: 120 | """Normalizes a version string into semantic versioning format (e.g., 1.0.0).""" 121 | if not version: 122 | return None 123 | 124 | cleaned_text = re.sub(r"[\s_-]+", ".", version.strip()) 125 | # Remove any existing 'v' prefix for consistency 126 | cleaned_text = re.sub(r"^v", "", cleaned_text, flags=re.IGNORECASE) 127 | 128 | match = re.match(r"^(\d+(?:\.\d+){0,2})$", cleaned_text) 129 | 130 | if match: 131 | numbers_part = match.group(1) 132 | 133 | if numbers_part.count(".") == 1: 134 | numbers_part += ".0" 135 | elif numbers_part.count(".") == 0: 136 | numbers_part += ".0.0" 137 | 138 | return numbers_part 139 | 140 | return version 141 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/tests/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | """Integration tests for AgentUp.""" 2 | -------------------------------------------------------------------------------- /tests/integration/configs/mcp_sse_config.yml: -------------------------------------------------------------------------------- 1 | # AgentUp Configuration for MCP SSE Transport Testing 2 | name: MCP SSE Test Agent 3 | description: Test agent for MCP SSE transport integration testing 4 | version: 1.0.0 5 | 6 | # Security Configuration 7 | security: 8 | enabled: true 9 | auth: 10 | api_key: 11 | header_name: "X-API-Key" 12 | keys: 13 | - key: "test-api-key" 14 | scopes: ["admin"] 15 | scope_hierarchy: 16 | admin: ["*"] 17 | weather:admin: ["weather:read", "alerts:read"] 18 | 19 | # AI Provider Configuration (Test) 20 | ai_provider: 21 | provider: openai 22 | api_key: sk-test-key-for-integration-tests 23 | model: gpt-4o-mini 24 | temperature: 0 25 | 26 | # MCP Configuration - SSE Transport 27 | mcp: 28 | enabled: true 29 | client_enabled: true 30 | servers: 31 | - name: "sse_weather" 32 | enabled: true 33 | transport: "sse" 34 | url: "http://localhost:8123/sse" 35 | timeout: 30 36 | headers: 37 | Authorization: "Bearer ${MCP_API_KEY}" 38 | tool_scopes: 39 | # Prefixed tool names (server:tool format) - REQUIRED 40 | "sse_weather:get_alerts": ["alerts:read"] 41 | "sse_weather:get_forecast": ["weather:read"] 42 | 43 | # API Configuration 44 | api: 45 | enabled: true 46 | host: "127.0.0.1" 47 | port: 8000 48 | workers: 1 49 | reload: false 50 | debug: true 51 | 52 | # Logging Configuration 53 | logging: 54 | enabled: true 55 | level: "DEBUG" 56 | format: "text" 57 | console: 58 | enabled: true 59 | colors: false # Disable colors for CI/testing 60 | correlation_id: false 61 | request_logging: false 62 | modules: 63 | "a2a": "CRITICAL" # Suppress all a2a logs below ERROR level 64 | "a2a.utils": "CRITICAL" # Specifically suppress a2a.utils logs 65 | "a2a.utils.telemetry": "CRITICAL" # Specifically suppress telemetry logs 66 | "a2a.utils.telemetry": "CRITICAL" # Only critical errors from telemetry 67 | 68 | # Middleware Configuration (Minimal for testing) 69 | middleware: 70 | enabled: false 71 | 72 | # Push Notifications 73 | push_notifications: 74 | enabled: false 75 | 76 | # State Management 77 | state_management: 78 | enabled: true 79 | backend: memory 80 | ttl: 3600 -------------------------------------------------------------------------------- /tests/integration/configs/mcp_stdio_config.yml: -------------------------------------------------------------------------------- 1 | # AgentUp Configuration for MCP Stdio Transport Testing 2 | name: MCP Stdio Test Agent 3 | description: Test agent for MCP stdio transport integration testing 4 | version: 1.0.0 5 | 6 | # Security Configuration 7 | security: 8 | enabled: true 9 | auth: 10 | api_key: 11 | header_name: "X-API-Key" 12 | keys: 13 | - key: "test-api-key" 14 | scopes: ["admin"] 15 | scope_hierarchy: 16 | admin: ["*"] 17 | weather:admin: ["weather:read", "alerts:read"] 18 | 19 | # AI Provider Configuration (Test) 20 | ai_provider: 21 | provider: openai 22 | api_key: sk-test-key-for-integration-tests 23 | model: gpt-4o-mini 24 | temperature: 0 25 | 26 | # MCP Configuration - Stdio Transport 27 | mcp: 28 | enabled: true 29 | client_enabled: true 30 | servers: 31 | - name: "stdio_weather" 32 | enabled: true 33 | transport: "stdio" 34 | command: "python" 35 | args: ["scripts/mcp/weather_server.py", "--transport", "stdio"] 36 | timeout: 30 37 | tool_scopes: 38 | # Prefixed tool names (server:tool format) - REQUIRED 39 | "stdio_weather:get_alerts": ["alerts:read"] 40 | "stdio_weather:get_forecast": ["weather:read"] 41 | 42 | # API Configuration 43 | api: 44 | enabled: true 45 | host: "127.0.0.1" 46 | port: 8000 47 | workers: 1 48 | reload: false 49 | debug: true 50 | 51 | # Logging Configuration 52 | logging: 53 | enabled: true 54 | level: "DEBUG" 55 | format: "text" 56 | console: 57 | enabled: true 58 | colors: false # Disable colors for CI/testing 59 | correlation_id: false 60 | request_logging: false 61 | modules: 62 | "a2a": "CRITICAL" # Suppress all a2a logs below ERROR level 63 | "a2a.utils": "CRITICAL" # Specifically suppress a2a.utils logs 64 | "a2a.utils.telemetry": "CRITICAL" # Specifically suppress telemetry logs 65 | "a2a.utils.telemetry": "CRITICAL" # Only critical errors from telemetry 66 | 67 | # Middleware Configuration (Minimal for testing) 68 | middleware: 69 | enabled: false 70 | 71 | # Push Notifications 72 | push_notifications: 73 | enabled: false 74 | 75 | # State Management 76 | state_management: 77 | enabled: true 78 | backend: memory 79 | ttl: 3600 -------------------------------------------------------------------------------- /tests/integration/configs/mcp_streamable_config.yml: -------------------------------------------------------------------------------- 1 | # AgentUp Configuration for MCP Streamable HTTP Transport Testing 2 | name: MCP Streamable HTTP Test Agent 3 | description: Test agent for MCP streamable HTTP transport integration testing 4 | version: 1.0.0 5 | 6 | # Security Configuration 7 | security: 8 | enabled: true 9 | auth: 10 | api_key: 11 | header_name: "X-API-Key" 12 | keys: 13 | - key: "test-api-key" 14 | scopes: ["admin"] 15 | scope_hierarchy: 16 | admin: ["*"] 17 | weather:admin: ["weather:read", "alerts:read"] 18 | 19 | # AI Provider Configuration (Test) 20 | ai_provider: 21 | provider: openai 22 | api_key: sk-test-key-for-integration-tests 23 | model: gpt-4o-mini 24 | temperature: 0 25 | 26 | # MCP Configuration - Streamable HTTP Transport 27 | mcp: 28 | enabled: true 29 | client_enabled: true 30 | servers: 31 | - name: "streamable_weather" 32 | enabled: true 33 | transport: "streamable_http" 34 | url: "http://localhost:8123/mcp" 35 | timeout: 30 36 | headers: 37 | Authorization: "Bearer ${MCP_API_KEY}" 38 | tool_scopes: 39 | # Prefixed tool names (server:tool format) - REQUIRED 40 | "streamable_weather:get_alerts": ["alerts:read"] 41 | "streamable_weather:get_forecast": ["weather:read"] 42 | 43 | # API Configuration 44 | api: 45 | enabled: true 46 | host: "127.0.0.1" 47 | port: 8000 48 | workers: 1 49 | reload: false 50 | debug: false 51 | 52 | # Logging Configuration 53 | logging: 54 | enabled: true 55 | level: "ERROR" # Reduce log level to minimize output 56 | format: "text" 57 | console: 58 | enabled: true 59 | colors: false # Disable colors for CI/testing 60 | correlation_id: false 61 | request_logging: false 62 | modules: 63 | "a2a": "CRITICAL" # Suppress all a2a logs below ERROR level 64 | "a2a.utils": "CRITICAL" # Specifically suppress a2a.utils logs 65 | "a2a.utils.telemetry": "CRITICAL" # Specifically suppress telemetry logs 66 | "a2a.utils.telemetry": "CRITICAL" # Only critical errors from telemetry 67 | 68 | # Middleware Configuration (Minimal for testing) 69 | middleware: 70 | enabled: false 71 | 72 | # Push Notifications 73 | push_notifications: 74 | enabled: false 75 | 76 | # State Management 77 | state_management: 78 | enabled: true 79 | backend: memory 80 | ttl: 3600 -------------------------------------------------------------------------------- /tests/integration/mocks/__init__.py: -------------------------------------------------------------------------------- 1 | """MCP Integration Test Mocks Package. 2 | 3 | This package provides mock implementations for MCP integration testing. 4 | """ 5 | 6 | from .mock_llm_provider import MockLLMProvider, create_mock_llm_provider 7 | 8 | __all__ = [ 9 | "MockLLMProvider", 10 | "create_mock_llm_provider", 11 | ] 12 | -------------------------------------------------------------------------------- /tests/integration/test_mcp_integration.py: -------------------------------------------------------------------------------- 1 | """MCP Integration Tests. 2 | 3 | This module contains comprehensive integration tests for MCP (Model Context Protocol) 4 | functionality across all supported transport types: SSE, Streamable HTTP, and stdio. 5 | """ 6 | 7 | import pytest 8 | 9 | # Mark all tests in this module as integration tests 10 | pytestmark = pytest.mark.integration 11 | 12 | 13 | class TestMCPTransportConnectivity: 14 | """Test MCP connectivity across different transport types.""" 15 | 16 | @pytest.mark.mcp_transport 17 | @pytest.mark.asyncio 18 | async def test_mcp_server_startup(self, mcp_server, transport_type): 19 | """Test that MCP server starts successfully for each transport.""" 20 | assert mcp_server.process is not None 21 | 22 | if transport_type == "stdio": 23 | # For stdio servers, process may exit immediately which is normal 24 | # We just check that the server started and transport is correct 25 | assert mcp_server.transport == transport_type 26 | else: 27 | # For HTTP servers, process should still be running 28 | assert mcp_server.process.poll() is None, f"{transport_type} server should be running" 29 | assert mcp_server.transport == transport_type 30 | 31 | @pytest.mark.mcp_transport 32 | @pytest.mark.asyncio 33 | async def test_agentup_server_startup(self, agentup_server, agentup_port): 34 | """Test that AgentUp server starts successfully.""" 35 | assert agentup_server.process is not None 36 | assert agentup_server.process.poll() is None, "AgentUp server should be running" 37 | 38 | @pytest.mark.mcp_transport 39 | @pytest.mark.asyncio 40 | async def test_server_health_check(self, full_test_setup): 41 | """Test that AgentUp server health endpoint works.""" 42 | import httpx 43 | 44 | base_url = full_test_setup["base_url"] 45 | api_key = full_test_setup["api_key"] 46 | 47 | async with httpx.AsyncClient(timeout=5.0) as client: 48 | response = await client.get(f"{base_url}/health", headers={"X-API-Key": api_key}) 49 | assert response.status_code == 200 50 | 51 | 52 | class TestMCPAuthentication: 53 | """Test basic authentication functionality without LLM calls.""" 54 | 55 | @pytest.mark.mcp_auth 56 | @pytest.mark.asyncio 57 | async def test_server_accepts_valid_api_key(self, full_test_setup): 58 | """Test that server accepts valid API key for basic requests.""" 59 | import httpx 60 | 61 | base_url = full_test_setup["base_url"] 62 | valid_api_key = full_test_setup["api_key"] 63 | 64 | async with httpx.AsyncClient(timeout=5.0) as client: 65 | response = await client.get(f"{base_url}/health", headers={"X-API-Key": valid_api_key}) 66 | assert response.status_code == 200 67 | 68 | @pytest.mark.mcp_auth 69 | @pytest.mark.asyncio 70 | async def test_server_rejects_invalid_api_key(self, full_test_setup): 71 | """Test that server rejects invalid API key for protected endpoints.""" 72 | from tests.integration.utils.mcp_test_utils import send_json_rpc_request 73 | 74 | base_url = full_test_setup["base_url"] 75 | invalid_api_key = "invalid-key-123" 76 | 77 | # Test with a protected endpoint (JSON-RPC) instead of health 78 | try: 79 | response = await send_json_rpc_request( 80 | url=base_url, method="message/send", params={}, api_key=invalid_api_key 81 | ) 82 | # If we get a response, check for error 83 | assert "error" in response 84 | except Exception as e: 85 | # HTTP 401/403 exceptions are expected 86 | assert "401" in str(e) or "403" in str(e) or "unauthorized" in str(e).lower() 87 | 88 | 89 | # Removed LLM-dependent test classes to focus on basic server functionality 90 | -------------------------------------------------------------------------------- /tests/integration/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """MCP Integration Test Utilities Package. 2 | 3 | This package provides utility functions and classes for MCP integration testing. 4 | """ 5 | 6 | from .mcp_test_utils import ( 7 | AgentUpServerManager, 8 | MCPServerManager, 9 | extract_tool_result, 10 | generate_mcp_config, 11 | send_json_rpc_request, 12 | validate_mcp_tool_response, 13 | ) 14 | 15 | __all__ = [ 16 | "MCPServerManager", 17 | "AgentUpServerManager", 18 | "generate_mcp_config", 19 | "send_json_rpc_request", 20 | "validate_mcp_tool_response", 21 | "extract_tool_result", 22 | ] 23 | -------------------------------------------------------------------------------- /tests/test_cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/tests/test_cli/__init__.py -------------------------------------------------------------------------------- /tests/test_core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/tests/test_core/__init__.py -------------------------------------------------------------------------------- /tests/test_core/test_handlers.py: -------------------------------------------------------------------------------- 1 | # import logging 2 | 3 | # from a2a.types import Task 4 | 5 | # from src.agent.handlers.handlers import register_handler 6 | 7 | # logger = logging.getLogger(__name__) 8 | 9 | 10 | # @register_handler("stateful_echo") 11 | # async def handle_stateful_echo(task: Task, context=None, context_id=None) -> str: 12 | # 13 | 14 | # # Extract user message 15 | # user_message = "No message" 16 | # if task.history and len(task.history) > 0: 17 | # latest_message = task.history[-1] 18 | # if latest_message.parts and len(latest_message.parts) > 0: 19 | # user_message = latest_message.parts[0].text 20 | 21 | # response_parts = [f"Echo: {user_message}"] 22 | 23 | # # If we have state context, use it 24 | # if context and context_id: 25 | # try: 26 | # # Get previous message count 27 | # prev_count = await context.get_variable(context_id, "message_count", 0) 28 | # new_count = prev_count + 1 29 | 30 | # # Store updated count 31 | # await context.set_variable(context_id, "message_count", new_count) 32 | 33 | # # Store this message in history 34 | # await context.add_to_history( 35 | # context_id, 36 | # "user", 37 | # user_message, 38 | # {"timestamp": str(task.status.timestamp) if task.status and task.status.timestamp else "unknown"}, 39 | # ) 40 | 41 | # # Get recent history 42 | # history = await context.get_history(context_id, limit=3) 43 | 44 | # response_parts.append(f"Message count: {new_count}") 45 | # response_parts.append(f"Recent messages: {len(history)}") 46 | 47 | # logger.info(f"State used - Context ID: {context_id}, Count: {new_count}") 48 | 49 | # except Exception as e: 50 | # logger.error(f"State management error: {e}") 51 | # response_parts.append(f"State error: {e}") 52 | # else: 53 | # response_parts.append("No state management available") 54 | # logger.info("No state context provided") 55 | 56 | # return " | ".join(response_parts) 57 | 58 | 59 | # @register_handler("stateful_counter") 60 | # async def handle_stateful_counter(task: Task, context=None, context_id=None) -> str: 61 | # 62 | 63 | # if not context or not context_id: 64 | # return "Counter: No state management available" 65 | 66 | # try: 67 | # # Get current count 68 | # current_count = await context.get_variable(context_id, "counter", 0) 69 | # new_count = current_count + 1 70 | 71 | # # Store updated count 72 | # await context.set_variable(context_id, "counter", new_count) 73 | 74 | # logger.info(f"Counter updated - Context ID: {context_id}, Count: {new_count}") 75 | 76 | # return f"Counter: {new_count} (Context: {context_id})" 77 | 78 | # except Exception as e: 79 | # logger.error(f"Counter state error: {e}") 80 | # return f"Counter error: {e}" 81 | 82 | 83 | # @register_handler("state_info") 84 | # async def handle_state_info(task: Task, context=None, context_id=None) -> str: 85 | # 86 | 87 | # if not context or not context_id: 88 | # return "State Info: No state management available" 89 | 90 | # try: 91 | # # Get all variables for this context 92 | # variables = await context.get_variable(context_id, "_all_variables", {}) 93 | # history_count = len(await context.get_history(context_id)) 94 | 95 | # return f"State Info - Context: {context_id} | Variables: {len(variables)} | History: {history_count}" 96 | 97 | # except Exception as e: 98 | # logger.error(f"State info error: {e}") 99 | # return f"State info error: {e}" 100 | -------------------------------------------------------------------------------- /tests/test_core/test_llm_anthropic.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) 5 | 6 | 7 | class TestLLMResponse: 8 | """Test the LLMResponse data class.""" 9 | -------------------------------------------------------------------------------- /tests/test_core/test_llm_base.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) 5 | 6 | 7 | class TestLLMResponse: 8 | """Test the LLMResponse data class.""" 9 | -------------------------------------------------------------------------------- /tests/test_core/test_llm_ollama.py: -------------------------------------------------------------------------------- 1 | # Add src to path for imports 2 | import sys 3 | from pathlib import Path 4 | 5 | sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) 6 | 7 | 8 | class TestOllamaProviderInitialization: 9 | """Test the LLMResponse data class.""" 10 | -------------------------------------------------------------------------------- /tests/test_state/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDotRocket/AgentUp/00080ec27e6ea0c4548c2edbd18e761127140b2b/tests/test_state/__init__.py -------------------------------------------------------------------------------- /tests/thirdparty/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for AgentUp integrations.""" 2 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .mock_services import * # noqa: F403 2 | from .test_helpers import * # noqa: F403 3 | --------------------------------------------------------------------------------