├── .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 |
4 |
5 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | 🚀 积极开发中
48 |
49 | 🏃♂️ 我们进展很快,可能会有变化!
50 |
51 | |
52 |
53 |
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 | [](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 |
12 |
13 |
14 |
15 |
16 | |
17 |
18 | |
19 |
20 | |
21 |
22 | |
23 |
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 |
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("