aegis-stack 0.2.0rc2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aegis/__init__.py +5 -0
- aegis/__main__.py +51 -0
- aegis/cli/__init__.py +6 -0
- aegis/cli/callbacks.py +114 -0
- aegis/cli/interactive.py +611 -0
- aegis/cli/utils.py +70 -0
- aegis/cli/validators.py +34 -0
- aegis/commands/__init__.py +6 -0
- aegis/commands/add.py +353 -0
- aegis/commands/add_service.py +332 -0
- aegis/commands/components.py +35 -0
- aegis/commands/init.py +370 -0
- aegis/commands/remove.py +227 -0
- aegis/commands/services.py +52 -0
- aegis/commands/update.py +252 -0
- aegis/commands/version.py +12 -0
- aegis/config/__init__.py +1 -0
- aegis/config/shared_files.py +136 -0
- aegis/core/CLAUDE.md +377 -0
- aegis/core/__init__.py +6 -0
- aegis/core/component_files.py +228 -0
- aegis/core/component_utils.py +220 -0
- aegis/core/components.py +127 -0
- aegis/core/copier_manager.py +315 -0
- aegis/core/copier_updater.py +475 -0
- aegis/core/dependency_resolver.py +119 -0
- aegis/core/manual_updater.py +554 -0
- aegis/core/post_gen_tasks.py +547 -0
- aegis/core/service_resolver.py +261 -0
- aegis/core/services.py +157 -0
- aegis/core/template_generator.py +266 -0
- aegis/core/version_compatibility.py +259 -0
- aegis/templates/CLAUDE.md +591 -0
- aegis/templates/cookiecutter-aegis-project/cookiecutter.json +39 -0
- aegis/templates/cookiecutter-aegis-project/hooks/post_gen_project.py +214 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.dockerignore +71 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.env.example.j2 +130 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.gitignore +131 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Makefile +236 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/README.md.j2 +196 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/alembic.ini.j2 +111 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/env.py.j2 +91 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/script.py.mako +25 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/versions/001_initial_auth.py.j2 +51 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/__init__.py +5 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/__init__.py +6 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/ai.py.j2 +700 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/ai_rendering.py +361 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/auth.py.j2 +253 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/health.py.j2 +419 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/load_test.py.j2 +656 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/main.py.j2 +65 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/marko_terminal_renderer.py +489 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/tasks.py.j2 +328 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/{% if cookiecutter.include_scheduler == /"yes/" %}tasks.py{% endif %}" +340 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/ai/__init__.py +8 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/ai/router.py +329 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/auth/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/auth/router.py +64 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/deps.py +58 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/health.py +163 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/models.py.j2 +280 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/routing.py.j2 +32 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/scheduler.py.j2 +121 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/worker.py.j2 +478 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/hooks.py +144 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/main.py +31 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/cors.py +20 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/cleanup.py +14 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/component_health.py.j2 +418 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/database_init.py.j2 +83 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/__init__.py +5 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/__init__.py +27 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/table.py +78 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/text.py +142 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/__init__.py.j2 +47 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/ai_card.py +287 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/auth_card.py +198 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/base_card.py +256 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/card_factory.py +227 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/card_utils.py +333 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/database_card.py +420 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/fastapi_card.py +328 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/flet_card.py +267 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/redis_card.py +322 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/scheduler_card.py +352 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/services_card.py +233 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/worker_card.py +684 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/main.py.j2 +653 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/theme.py +48 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/main.py.j2 +156 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/CLAUDE.md.j2 +213 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/__init__.py +6 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/constants.py.j2 +30 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/pools.py +97 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/load_test.py +55 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/media.py +49 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/system.py +44 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/registry.py +139 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/__init__.py +120 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/load_tasks.py +507 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/simple_system_tasks.py +33 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/system_tasks.py +281 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/config.py.j2 +178 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/constants.py +58 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/db.py.j2 +176 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/log.py +92 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/security.py +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/webserver.py +40 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/{% if cookiecutter.include_scheduler == /"yes/" %}scheduler.py{% endif %}" +21 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/main.py +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/models/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/models/user.py +44 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/py.typed +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/__init__.py +8 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/config.py +130 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/conversation.py +213 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/health.py +96 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/models.py +229 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/providers.py +370 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/service.py +388 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/auth_service.py +41 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/health.py +164 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/user_service.py +83 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/middleware_inspector.py.j2 +223 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/models.py.j2 +70 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/route_inspector.py.j2 +155 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test.py +679 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test_models.py +266 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/__init__.py.j2 +21 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/models.py.j2 +119 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/scheduled_task_manager.py.j2 +273 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/task_monitor.py.j2 +189 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/__init__.py +15 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/models.py +26 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/__init__.py +52 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/alerts.py +94 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/backup.py.j2 +119 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/health.py.j2 +1333 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/models.py +243 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/ui.py +52 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto-dark.png +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto-square-backup.png +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto.png +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.dockerignore +71 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.env.example.j2 +64 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.gitignore +131 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/Dockerfile +53 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/Makefile +211 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/README.md.j2 +172 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/docker-compose.yml.j2 +78 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/mkdocs.yml.j2 +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/pyproject.toml.j2 +120 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/uv.lock +1673 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docker-compose.yml.j2 +200 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/api.md +191 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md.j2 +621 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/development.md +215 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/health.md +240 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/javascripts/mermaid-config.js +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/stylesheets/mermaid.css +95 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/mkdocs.yml.j2 +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/pyproject.toml.j2 +131 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh +87 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh.j2 +93 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/gen_docs.py +16 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_auth_endpoints.py.j2 +307 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_health_endpoints.py.j2 +262 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_scheduler_endpoints.py.j2 +214 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_worker_endpoints.py.j2 +165 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/cli/test_ai_rendering.py +427 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/cli/test_conversation_memory.py +465 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/components/test_scheduler.py +43 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/conftest.py.j2 +195 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/conftest.py +78 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_health.py +157 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_models.py +164 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_service.py +198 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_auth_integration.py.j2 +528 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_component_integration.py.j2 +387 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_conversation_persistence.py +342 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_health_logic.py.j2 +663 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_models.py +619 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_service.py +603 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_middleware_inspector.py.j2 +248 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_scheduled_task_manager.py.j2 +292 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_system_service.py +98 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_worker_health_registration.py.j2 +257 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/test_core.py +49 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/uv.lock +1673 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/.copier-answers.yml.jinja +21 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/.dockerignore +71 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/.env.example.jinja +130 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/.gitignore +131 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/Dockerfile +53 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/Makefile.jinja +236 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/README.md.jinja +196 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/alembic.ini.jinja +111 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/env.py.jinja +91 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/script.py.mako +25 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/versions/001_initial_auth.py.jinja +51 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/__init__.py.jinja +5 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/__init__.py.jinja +6 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/ai.py.jinja +700 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/ai_rendering.py +360 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/auth.py.jinja +253 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/health.py.jinja +419 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/load_test.py.jinja +656 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/main.py.jinja +65 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/marko_terminal_renderer.py +489 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/tasks.py.jinja +328 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/__init__.py +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/__init__.py +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/ai/__init__.py +8 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/ai/router.py +329 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/auth/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/auth/router.py +64 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/deps.py +58 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/health.py.jinja +163 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/models.py.jinja +280 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/routing.py.jinja +32 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/scheduler.py.jinja +121 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/worker.py.jinja +478 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/hooks.py +144 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/main.py +31 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/middleware/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/middleware/cors.py +20 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/shutdown/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/shutdown/cleanup.py +14 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/component_health.py.jinja +418 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/database_init.py.jinja +83 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/__init__.py +5 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/__init__.py +27 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/table.py +78 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/text.py +142 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/__init__.py.jinja +47 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/ai_card.py +287 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/auth_card.py +198 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/base_card.py +256 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/card_factory.py +227 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/card_utils.py +333 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/database_card.py +420 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/fastapi_card.py +328 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/flet_card.py +267 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/redis_card.py +322 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/scheduler_card.py +352 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/services_card.py +233 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/worker_card.py +684 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/main.py.jinja +653 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/theme.py +48 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/scheduler/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/scheduler/main.py.jinja +156 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/CLAUDE.md.jinja +213 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/__init__.py +6 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/constants.py.jinja +30 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/pools.py +97 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/load_test.py +55 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/media.py +49 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/system.py +44 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/registry.py +139 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/__init__.py +120 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/load_tasks.py +507 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/simple_system_tasks.py +33 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/system_tasks.py +281 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/config.py.jinja +178 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/constants.py +58 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/db.py.jinja +176 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/log.py +92 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/security.py +62 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/scheduler.py.jinja +21 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/webserver.py +39 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/integrations/__init__.py +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/integrations/main.py +61 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/models/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/models/user.py +44 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/py.typed +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/__init__.py +8 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/config.py +130 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/conversation.py +213 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/health.py +96 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/models.py +229 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/providers.py.jinja +370 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/service.py +387 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/auth_service.py +40 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/health.py +162 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/user_service.py +82 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/middleware_inspector.py.jinja +223 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/models.py.jinja +70 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/route_inspector.py.jinja +155 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/load_test.py +678 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/load_test_models.py +265 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/__init__.py.jinja +21 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/models.py.jinja +119 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/scheduled_task_manager.py.jinja +273 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/task_monitor.py.jinja +189 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/shared/__init__.py +15 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/shared/models.py +26 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/__init__.py +52 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/alerts.py +94 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/backup.py.jinja +119 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/health.py.jinja +1333 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/models.py +243 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/ui.py +52 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57223!aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57224!aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57225!aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57533!aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57534!aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57538!aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57897!aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57898!aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57904!aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58315!aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58316!aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58324!aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58837!aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58838!aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58849!aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/.env.example.jinja +64 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/README.md.jinja +172 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/docker-compose.yml.jinja +78 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/mkdocs.yml.jinja +62 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/pyproject.toml.jinja +120 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docker-compose.yml.jinja +200 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/api.md.jinja +191 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/components/scheduler.md +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/components/scheduler.md.jinja +621 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/development.md.jinja +215 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/health.md.jinja +240 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/javascripts/mermaid-config.js +62 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/stylesheets/mermaid.css +95 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/mkdocs.yml.jinja +62 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/pyproject.toml.jinja +131 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/entrypoint.sh +87 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/entrypoint.sh.jinja +93 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/gen_docs.py +16 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_auth_endpoints.py.jinja +307 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_health_endpoints.py.jinja +262 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_scheduler_endpoints.py.jinja +214 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_worker_endpoints.py.jinja +165 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/cli/test_ai_rendering.py +427 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/cli/test_conversation_memory.py +465 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/components/test_scheduler.py +43 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/conftest.py.jinja +195 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/conftest.py +78 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_health.py +157 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_models.py +164 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_service.py +198 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_auth_integration.py.jinja +528 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_component_integration.py.jinja +387 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_conversation_persistence.py +342 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_health_logic.py.jinja +663 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_load_test_models.py +619 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_load_test_service.py +603 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_middleware_inspector.py.jinja +248 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_scheduled_task_manager.py.jinja +292 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_system_service.py +98 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_worker_health_registration.py.jinja +257 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/test_core.py +49 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/uv.lock +1673 -0
- aegis_stack-0.2.0rc2.dist-info/METADATA +165 -0
- aegis_stack-0.2.0rc2.dist-info/RECORD +392 -0
- aegis_stack-0.2.0rc2.dist-info/WHEEL +4 -0
- aegis_stack-0.2.0rc2.dist-info/entry_points.txt +3 -0
- aegis_stack-0.2.0rc2.dist-info/licenses/LICENSE +21 -0
aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/health.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI service health check functions.
|
|
3
|
+
|
|
4
|
+
Health monitoring for AI service functionality including provider configuration,
|
|
5
|
+
API connectivity, and service-specific metrics.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from app.core.log import logger
|
|
9
|
+
from app.services.system.models import ComponentStatus, ComponentStatusType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def check_ai_service_health() -> ComponentStatus:
|
|
13
|
+
"""
|
|
14
|
+
Check AI service health including provider configuration and dependencies.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
ComponentStatus indicating AI service health
|
|
18
|
+
"""
|
|
19
|
+
try:
|
|
20
|
+
# Import the shared AI service instance from the API router
|
|
21
|
+
# This ensures health checks reflect actual API conversation state
|
|
22
|
+
from app.components.backend.api.ai.router import ai_service
|
|
23
|
+
|
|
24
|
+
# Get service status
|
|
25
|
+
service_status = ai_service.get_service_status()
|
|
26
|
+
validation_errors = ai_service.validate_service()
|
|
27
|
+
|
|
28
|
+
# Determine overall health
|
|
29
|
+
if not service_status["enabled"]:
|
|
30
|
+
status = ComponentStatusType.DEGRADED
|
|
31
|
+
message = "AI service is disabled"
|
|
32
|
+
elif validation_errors:
|
|
33
|
+
status = ComponentStatusType.UNHEALTHY
|
|
34
|
+
message = (
|
|
35
|
+
f"AI service configuration issues: {'; '.join(validation_errors[:2])}"
|
|
36
|
+
)
|
|
37
|
+
else:
|
|
38
|
+
status = ComponentStatusType.HEALTHY
|
|
39
|
+
message = f"AI service ready - {service_status['provider']} provider"
|
|
40
|
+
|
|
41
|
+
# Collect comprehensive metadata
|
|
42
|
+
metadata = {
|
|
43
|
+
"service_type": "ai",
|
|
44
|
+
"engine": "pydantic-ai",
|
|
45
|
+
"enabled": service_status["enabled"],
|
|
46
|
+
"provider": service_status["provider"],
|
|
47
|
+
"model": service_status["model"],
|
|
48
|
+
"agent_ready": service_status["agent_initialized"],
|
|
49
|
+
"total_conversations": service_status["total_conversations"],
|
|
50
|
+
"configuration_valid": service_status["configuration_valid"],
|
|
51
|
+
"validation_errors": validation_errors,
|
|
52
|
+
"validation_errors_count": len(validation_errors),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Add dependency status
|
|
56
|
+
metadata["dependencies"] = {
|
|
57
|
+
"backend": "required",
|
|
58
|
+
"pydantic_ai": "required",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Add provider-specific info
|
|
62
|
+
if service_status["enabled"]:
|
|
63
|
+
from .models import get_free_providers, get_provider_capabilities
|
|
64
|
+
|
|
65
|
+
provider_caps = get_provider_capabilities(ai_service.config.provider)
|
|
66
|
+
free_providers = get_free_providers()
|
|
67
|
+
|
|
68
|
+
metadata.update(
|
|
69
|
+
{
|
|
70
|
+
"provider_supports_streaming": provider_caps.supports_streaming,
|
|
71
|
+
"provider_free_tier": ai_service.config.provider in free_providers,
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return ComponentStatus(
|
|
76
|
+
name="ai",
|
|
77
|
+
status=status,
|
|
78
|
+
message=message,
|
|
79
|
+
response_time_ms=None, # Will be set by caller
|
|
80
|
+
metadata=metadata,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.error(f"AI service health check failed: {e}")
|
|
85
|
+
return ComponentStatus(
|
|
86
|
+
name="ai",
|
|
87
|
+
status=ComponentStatusType.UNHEALTHY,
|
|
88
|
+
message=f"AI service health check failed: {str(e)}",
|
|
89
|
+
response_time_ms=None,
|
|
90
|
+
metadata={
|
|
91
|
+
"service_type": "ai",
|
|
92
|
+
"engine": "pydantic-ai",
|
|
93
|
+
"error": str(e),
|
|
94
|
+
"error_type": "health_check_failure",
|
|
95
|
+
},
|
|
96
|
+
)
|
aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/models.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI service data models and enums.
|
|
3
|
+
|
|
4
|
+
This module defines the core data structures for AI service configuration,
|
|
5
|
+
conversation management, and provider integration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AIProvider(str, Enum):
|
|
16
|
+
"""Supported AI providers for PydanticAI integration."""
|
|
17
|
+
|
|
18
|
+
OPENAI = "openai"
|
|
19
|
+
ANTHROPIC = "anthropic"
|
|
20
|
+
GOOGLE = "google"
|
|
21
|
+
GROQ = "groq"
|
|
22
|
+
MISTRAL = "mistral"
|
|
23
|
+
COHERE = "cohere"
|
|
24
|
+
PUBLIC = "public" # Free public endpoints (no API key required)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MessageRole(str, Enum):
|
|
28
|
+
"""Message roles in a conversation."""
|
|
29
|
+
|
|
30
|
+
USER = "user"
|
|
31
|
+
ASSISTANT = "assistant"
|
|
32
|
+
SYSTEM = "system"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ProviderConfig(BaseModel):
|
|
36
|
+
"""Configuration for a specific AI provider."""
|
|
37
|
+
|
|
38
|
+
name: AIProvider
|
|
39
|
+
api_key: str | None = None
|
|
40
|
+
base_url: str | None = None
|
|
41
|
+
max_tokens: int = 1000
|
|
42
|
+
temperature: float = 0.7
|
|
43
|
+
timeout_seconds: float = 30.0
|
|
44
|
+
|
|
45
|
+
class Config:
|
|
46
|
+
use_enum_values = True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ConversationMessage(BaseModel):
|
|
50
|
+
"""A single message in a conversation."""
|
|
51
|
+
|
|
52
|
+
id: str = Field(..., description="Unique message identifier")
|
|
53
|
+
role: MessageRole
|
|
54
|
+
content: str
|
|
55
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
56
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Conversation(BaseModel):
|
|
60
|
+
"""A conversation containing multiple messages."""
|
|
61
|
+
|
|
62
|
+
id: str = Field(..., description="Unique conversation identifier")
|
|
63
|
+
title: str | None = None
|
|
64
|
+
messages: list[ConversationMessage] = Field(default_factory=list)
|
|
65
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
66
|
+
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
67
|
+
provider: AIProvider
|
|
68
|
+
model: str
|
|
69
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
70
|
+
|
|
71
|
+
class Config:
|
|
72
|
+
use_enum_values = True
|
|
73
|
+
|
|
74
|
+
def add_message(
|
|
75
|
+
self, role: MessageRole, content: str, message_id: str | None = None
|
|
76
|
+
) -> ConversationMessage:
|
|
77
|
+
"""Add a new message to the conversation."""
|
|
78
|
+
import uuid
|
|
79
|
+
|
|
80
|
+
message = ConversationMessage(
|
|
81
|
+
id=message_id or str(uuid.uuid4()), role=role, content=content
|
|
82
|
+
)
|
|
83
|
+
self.messages.append(message)
|
|
84
|
+
self.updated_at = datetime.now(UTC)
|
|
85
|
+
|
|
86
|
+
# Auto-generate title from first user message
|
|
87
|
+
if not self.title and role == MessageRole.USER and len(self.messages) == 1:
|
|
88
|
+
# Use first 50 characters as title
|
|
89
|
+
self.title = content[:50] + "..." if len(content) > 50 else content
|
|
90
|
+
|
|
91
|
+
return message
|
|
92
|
+
|
|
93
|
+
def get_message_count(self) -> int:
|
|
94
|
+
"""Get total number of messages in conversation."""
|
|
95
|
+
return len(self.messages)
|
|
96
|
+
|
|
97
|
+
def get_last_message(self) -> ConversationMessage | None:
|
|
98
|
+
"""Get the most recent message."""
|
|
99
|
+
return self.messages[-1] if self.messages else None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class StreamingMessage(BaseModel):
|
|
103
|
+
"""A streaming message chunk with metadata."""
|
|
104
|
+
|
|
105
|
+
content: str = Field(..., description="Partial or complete message content")
|
|
106
|
+
is_final: bool = Field(False, description="Whether this is the final chunk")
|
|
107
|
+
is_delta: bool = Field(False, description="Whether content is delta or cumulative")
|
|
108
|
+
message_id: str | None = Field(None, description="Message ID once finalized")
|
|
109
|
+
conversation_id: str | None = Field(None, description="Associated conversation ID")
|
|
110
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
111
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class StreamingConversation(BaseModel):
|
|
115
|
+
"""Extended conversation model for streaming state management."""
|
|
116
|
+
|
|
117
|
+
conversation: Conversation
|
|
118
|
+
current_message_id: str | None = None
|
|
119
|
+
accumulated_content: str = ""
|
|
120
|
+
stream_start_time: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
121
|
+
|
|
122
|
+
def reset_stream(self) -> None:
|
|
123
|
+
"""Reset streaming state for new message."""
|
|
124
|
+
self.current_message_id = None
|
|
125
|
+
self.accumulated_content = ""
|
|
126
|
+
self.stream_start_time = datetime.now(UTC)
|
|
127
|
+
|
|
128
|
+
def accumulate_content(self, content: str, is_delta: bool = False) -> str:
|
|
129
|
+
"""Accumulate streaming content and return total content."""
|
|
130
|
+
if is_delta:
|
|
131
|
+
self.accumulated_content += content
|
|
132
|
+
else:
|
|
133
|
+
self.accumulated_content = content
|
|
134
|
+
return self.accumulated_content
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class AIServiceStatus(BaseModel):
|
|
138
|
+
"""Status information for the AI service."""
|
|
139
|
+
|
|
140
|
+
enabled: bool
|
|
141
|
+
provider: AIProvider
|
|
142
|
+
model: str
|
|
143
|
+
available_providers: list[AIProvider]
|
|
144
|
+
conversation_count: int = 0
|
|
145
|
+
last_activity: datetime | None = None
|
|
146
|
+
|
|
147
|
+
class Config:
|
|
148
|
+
use_enum_values = True
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class ProviderCapabilities(BaseModel):
|
|
152
|
+
"""Capabilities for an AI provider (binary features only)."""
|
|
153
|
+
|
|
154
|
+
provider: AIProvider
|
|
155
|
+
supports_streaming: bool = True
|
|
156
|
+
supports_function_calling: bool = False
|
|
157
|
+
supports_vision: bool = False
|
|
158
|
+
free_tier_available: bool = False
|
|
159
|
+
|
|
160
|
+
class Config:
|
|
161
|
+
use_enum_values = True
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# Provider capability definitions (binary features only)
|
|
165
|
+
PROVIDER_CAPABILITIES = {
|
|
166
|
+
AIProvider.OPENAI: ProviderCapabilities(
|
|
167
|
+
provider=AIProvider.OPENAI,
|
|
168
|
+
supports_streaming=True,
|
|
169
|
+
supports_function_calling=True,
|
|
170
|
+
supports_vision=True,
|
|
171
|
+
free_tier_available=False,
|
|
172
|
+
),
|
|
173
|
+
AIProvider.ANTHROPIC: ProviderCapabilities(
|
|
174
|
+
provider=AIProvider.ANTHROPIC,
|
|
175
|
+
supports_streaming=True,
|
|
176
|
+
supports_function_calling=True,
|
|
177
|
+
supports_vision=True,
|
|
178
|
+
free_tier_available=False,
|
|
179
|
+
),
|
|
180
|
+
AIProvider.GOOGLE: ProviderCapabilities(
|
|
181
|
+
provider=AIProvider.GOOGLE,
|
|
182
|
+
supports_streaming=True,
|
|
183
|
+
supports_function_calling=True,
|
|
184
|
+
supports_vision=True,
|
|
185
|
+
free_tier_available=True,
|
|
186
|
+
),
|
|
187
|
+
AIProvider.GROQ: ProviderCapabilities(
|
|
188
|
+
provider=AIProvider.GROQ,
|
|
189
|
+
supports_streaming=True,
|
|
190
|
+
supports_function_calling=False,
|
|
191
|
+
supports_vision=False,
|
|
192
|
+
free_tier_available=True, # Very generous free tier
|
|
193
|
+
),
|
|
194
|
+
AIProvider.MISTRAL: ProviderCapabilities(
|
|
195
|
+
provider=AIProvider.MISTRAL,
|
|
196
|
+
supports_streaming=True,
|
|
197
|
+
supports_function_calling=True,
|
|
198
|
+
supports_vision=False,
|
|
199
|
+
free_tier_available=False,
|
|
200
|
+
),
|
|
201
|
+
AIProvider.COHERE: ProviderCapabilities(
|
|
202
|
+
provider=AIProvider.COHERE,
|
|
203
|
+
supports_streaming=True,
|
|
204
|
+
supports_function_calling=False,
|
|
205
|
+
supports_vision=False,
|
|
206
|
+
free_tier_available=True,
|
|
207
|
+
),
|
|
208
|
+
AIProvider.PUBLIC: ProviderCapabilities(
|
|
209
|
+
provider=AIProvider.PUBLIC,
|
|
210
|
+
supports_streaming=False,
|
|
211
|
+
supports_function_calling=False,
|
|
212
|
+
supports_vision=False,
|
|
213
|
+
free_tier_available=True, # No API key required
|
|
214
|
+
),
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_provider_capabilities(provider: AIProvider) -> ProviderCapabilities:
|
|
219
|
+
"""Get capabilities for a specific provider."""
|
|
220
|
+
return PROVIDER_CAPABILITIES.get(provider, ProviderCapabilities(provider=provider))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def get_free_providers() -> list[AIProvider]:
|
|
224
|
+
"""Get list of providers that offer free tiers."""
|
|
225
|
+
return [
|
|
226
|
+
provider
|
|
227
|
+
for provider, caps in PROVIDER_CAPABILITIES.items()
|
|
228
|
+
if caps.free_tier_available
|
|
229
|
+
]
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI provider factory for creating PydanticAI agents.
|
|
3
|
+
|
|
4
|
+
Simple factory that maps providers to PydanticAI model classes and creates
|
|
5
|
+
Agent instances. Supports PydanticAI 1.0+ API with demo mode for immediate testing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from openai import AsyncOpenAI
|
|
14
|
+
from pydantic_ai import Agent
|
|
15
|
+
|
|
16
|
+
# Import only what's needed for PUBLIC provider by default
|
|
17
|
+
# Other providers imported dynamically when needed
|
|
18
|
+
from pydantic_ai.models.openai import OpenAIChatModel
|
|
19
|
+
from pydantic_ai.providers.openai import OpenAIProvider
|
|
20
|
+
from pydantic_ai.settings import ModelSettings
|
|
21
|
+
|
|
22
|
+
# Removed TestModel - that's for unit tests, not user demos
|
|
23
|
+
from app.core.log import logger
|
|
24
|
+
|
|
25
|
+
from .config import AIServiceConfig
|
|
26
|
+
from .models import AIProvider
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Lazy loading of provider model classes to avoid import errors
|
|
30
|
+
def _get_model_class(provider: AIProvider):
|
|
31
|
+
"""Get model class for provider with lazy import to avoid dependency issues."""
|
|
32
|
+
if provider == AIProvider.OPENAI:
|
|
33
|
+
return OpenAIChatModel
|
|
34
|
+
elif provider == AIProvider.ANTHROPIC:
|
|
35
|
+
from pydantic_ai.models.anthropic import AnthropicModel
|
|
36
|
+
|
|
37
|
+
return AnthropicModel
|
|
38
|
+
elif provider == AIProvider.GOOGLE:
|
|
39
|
+
from pydantic_ai.models.google import GoogleModel
|
|
40
|
+
|
|
41
|
+
return GoogleModel
|
|
42
|
+
elif provider == AIProvider.GROQ:
|
|
43
|
+
from pydantic_ai.models.groq import GroqModel
|
|
44
|
+
|
|
45
|
+
return GroqModel
|
|
46
|
+
elif provider == AIProvider.MISTRAL:
|
|
47
|
+
return OpenAIChatModel # Mistral uses OpenAI-compatible API
|
|
48
|
+
elif provider == AIProvider.COHERE:
|
|
49
|
+
return OpenAIChatModel # Cohere can use OpenAI-compatible interface
|
|
50
|
+
elif provider == AIProvider.PUBLIC:
|
|
51
|
+
return OpenAIChatModel # Public endpoints use OpenAI-compatible API
|
|
52
|
+
else:
|
|
53
|
+
raise ProviderError(f"Unsupported provider: {provider}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ProviderError(Exception):
|
|
57
|
+
"""Exception raised when provider setup fails."""
|
|
58
|
+
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_agent(config: AIServiceConfig, settings: Any) -> Agent:
|
|
63
|
+
"""
|
|
64
|
+
Create a PydanticAI Agent for the configured provider.
|
|
65
|
+
|
|
66
|
+
Falls back to demo mode if no API keys are configured for immediate testing.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
config: AI service configuration
|
|
70
|
+
settings: Application settings for API keys
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Agent: Configured PydanticAI Agent
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ProviderError: If agent creation fails
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
# Special handling for PUBLIC provider (no API key required)
|
|
80
|
+
if config.provider == AIProvider.PUBLIC:
|
|
81
|
+
return _create_public_agent(config)
|
|
82
|
+
|
|
83
|
+
# Check if we have API key for this provider
|
|
84
|
+
provider_config = config.get_provider_config(settings)
|
|
85
|
+
|
|
86
|
+
if not provider_config.api_key:
|
|
87
|
+
raise ProviderError(
|
|
88
|
+
f"No API key configured for {config.provider}. "
|
|
89
|
+
f"Set {_get_env_var_name(config.provider)} environment variable. "
|
|
90
|
+
f"For free usage without API keys, try 'public' provider or Groq: https://console.groq.com/keys"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Set environment variable for PydanticAI 1.0+ (they expect env vars)
|
|
94
|
+
_set_provider_env_var(config.provider, provider_config.api_key)
|
|
95
|
+
|
|
96
|
+
# Get model class for provider (with lazy loading)
|
|
97
|
+
model_class = _get_model_class(config.provider)
|
|
98
|
+
|
|
99
|
+
# For PydanticAI 1.0+, NO api_key parameter - just model_name
|
|
100
|
+
model_kwargs = {"model_name": config.model}
|
|
101
|
+
|
|
102
|
+
# Add provider-specific configuration
|
|
103
|
+
if config.provider == AIProvider.MISTRAL:
|
|
104
|
+
model_kwargs["base_url"] = "https://api.mistral.ai/v1"
|
|
105
|
+
elif config.provider == AIProvider.COHERE:
|
|
106
|
+
model_kwargs["base_url"] = "https://api.cohere.ai/v1"
|
|
107
|
+
|
|
108
|
+
# Create model instance (PydanticAI 1.0+ style)
|
|
109
|
+
model = model_class(**model_kwargs)
|
|
110
|
+
|
|
111
|
+
# Create agent with system prompt and model settings
|
|
112
|
+
agent = Agent(
|
|
113
|
+
model=model,
|
|
114
|
+
model_settings=ModelSettings(
|
|
115
|
+
temperature=config.temperature,
|
|
116
|
+
max_tokens=config.max_tokens,
|
|
117
|
+
timeout=config.timeout_seconds,
|
|
118
|
+
),
|
|
119
|
+
system_prompt=(
|
|
120
|
+
"You are a helpful AI assistant. Provide clear, accurate, "
|
|
121
|
+
"and concise responses. Be friendly and professional."
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# logger.info(f"Created {config.provider} agent with model {config.model}")
|
|
126
|
+
return agent
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
error_msg = f"Failed to create agent for {config.provider}: {e}"
|
|
130
|
+
logger.error(error_msg)
|
|
131
|
+
raise ProviderError(error_msg) from e
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _create_public_agent(config: AIServiceConfig) -> Agent:
|
|
135
|
+
"""
|
|
136
|
+
Create agent for PUBLIC provider using free public endpoints.
|
|
137
|
+
|
|
138
|
+
Uses LLM7.io service which provides free access through an OpenAI-compatible API.
|
|
139
|
+
Note: The service accepts model names for compatibility but routes to their
|
|
140
|
+
available models (actual model used may differ from requested).
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
config: AI service configuration
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Agent: Configured PydanticAI Agent for public endpoint
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
ProviderError: If agent creation fails
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
# Create a custom HTTP client that fixes LLM7.io response format
|
|
153
|
+
class FixedLLM7Client(httpx.AsyncClient):
|
|
154
|
+
"""Custom HTTP client that adds missing index field to LLM7.io responses."""
|
|
155
|
+
|
|
156
|
+
async def send(self, request, **kwargs):
|
|
157
|
+
# logger.info(
|
|
158
|
+
# f"🔧 FixedLLM7Client.send: {request.method} {request.url}"
|
|
159
|
+
# )
|
|
160
|
+
response = await super().send(request, **kwargs)
|
|
161
|
+
|
|
162
|
+
# If this is a chat completions request, fix the response
|
|
163
|
+
if (
|
|
164
|
+
"/chat/completions" in str(request.url)
|
|
165
|
+
and request.method.upper() == "POST"
|
|
166
|
+
):
|
|
167
|
+
# logger.debug(
|
|
168
|
+
# "Detected chat completions request, attempting to "
|
|
169
|
+
# "fix response"
|
|
170
|
+
# )
|
|
171
|
+
try:
|
|
172
|
+
# Check if this is a streaming response first
|
|
173
|
+
content_type = response.headers.get("content-type", "")
|
|
174
|
+
if (
|
|
175
|
+
"text/plain" in content_type
|
|
176
|
+
or "text/event-stream" in content_type
|
|
177
|
+
or "application/x-ndjson" in content_type
|
|
178
|
+
):
|
|
179
|
+
# logger.debug("Skipping fix for streaming response")
|
|
180
|
+
return response
|
|
181
|
+
|
|
182
|
+
# Only process non-streaming JSON responses
|
|
183
|
+
if not response.headers.get("content-type", "").startswith(
|
|
184
|
+
"application/json"
|
|
185
|
+
):
|
|
186
|
+
# logger.debug("Skipping fix for non-JSON response")
|
|
187
|
+
return response
|
|
188
|
+
|
|
189
|
+
# Get response text, letting httpx handle encoding/decompression
|
|
190
|
+
response_text = response.text
|
|
191
|
+
data = json.loads(response_text)
|
|
192
|
+
|
|
193
|
+
# logger.debug(f"Original response data: {data}")
|
|
194
|
+
|
|
195
|
+
# Add missing index field to choices
|
|
196
|
+
if "choices" in data and isinstance(data["choices"], list):
|
|
197
|
+
for i, choice in enumerate(data["choices"]):
|
|
198
|
+
if "index" not in choice or choice["index"] is None:
|
|
199
|
+
choice["index"] = i # Always set index
|
|
200
|
+
# logger.debug(f"Added index {i} to choice")
|
|
201
|
+
|
|
202
|
+
# Monkey patch the response to return fixed content
|
|
203
|
+
fixed_content = json.dumps(data)
|
|
204
|
+
response._content = fixed_content.encode()
|
|
205
|
+
response._text = fixed_content
|
|
206
|
+
|
|
207
|
+
# logger.debug(
|
|
208
|
+
# f"Fixed LLM7.io response: added index fields "
|
|
209
|
+
# f"to {len(data.get('choices', []))} choices"
|
|
210
|
+
# )
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.warning(f"Failed to fix LLM7.io response: {e}")
|
|
214
|
+
# If fixing fails, return original response
|
|
215
|
+
|
|
216
|
+
return response
|
|
217
|
+
|
|
218
|
+
# Create custom HTTP client
|
|
219
|
+
custom_http_client = FixedLLM7Client()
|
|
220
|
+
# logger.debug(f"Created custom HTTP client: {type(custom_http_client)}")
|
|
221
|
+
|
|
222
|
+
# Create the AsyncOpenAI client directly to ensure
|
|
223
|
+
# our custom HTTP client is used
|
|
224
|
+
openai_client = AsyncOpenAI(
|
|
225
|
+
api_key="unused", # LLM7.io doesn't need a real key
|
|
226
|
+
base_url="https://api.llm7.io/v1", # Public endpoint
|
|
227
|
+
http_client=custom_http_client,
|
|
228
|
+
)
|
|
229
|
+
# logger.debug("Created OpenAI client with custom HTTP client")
|
|
230
|
+
|
|
231
|
+
# Create provider using the custom openai_client
|
|
232
|
+
provider = OpenAIProvider(openai_client=openai_client)
|
|
233
|
+
# logger.debug(f"Created provider with custom OpenAI client: {provider}")
|
|
234
|
+
|
|
235
|
+
# Create OpenAI model using the provider
|
|
236
|
+
# Note: LLM7.io accepts various model names but routes to their available models
|
|
237
|
+
model_name = (
|
|
238
|
+
config.model if config.model and config.model != "auto" else "gpt-4o-mini"
|
|
239
|
+
)
|
|
240
|
+
model = OpenAIChatModel(model_name=model_name, provider=provider)
|
|
241
|
+
|
|
242
|
+
# Create agent with friendly system prompt and model settings
|
|
243
|
+
agent = Agent(
|
|
244
|
+
model=model,
|
|
245
|
+
model_settings=ModelSettings(
|
|
246
|
+
temperature=config.temperature,
|
|
247
|
+
max_tokens=config.max_tokens,
|
|
248
|
+
timeout=config.timeout_seconds,
|
|
249
|
+
),
|
|
250
|
+
system_prompt=(
|
|
251
|
+
"You are a helpful AI assistant powered by free public endpoints. "
|
|
252
|
+
"Provide clear, accurate, and concise responses. "
|
|
253
|
+
"Be friendly and professional."
|
|
254
|
+
),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# logger.info(f"Created PUBLIC agent with model {config.model} via LLM7.io")
|
|
258
|
+
return agent
|
|
259
|
+
|
|
260
|
+
except Exception as e:
|
|
261
|
+
# Fallback with helpful instructions if public endpoint fails
|
|
262
|
+
error_msg = (
|
|
263
|
+
f"Public endpoint failed ({e}). Here are reliable free alternatives:\n\n"
|
|
264
|
+
"🌟 RECOMMENDED - Groq (Fastest, Most Generous Free Tier):\n"
|
|
265
|
+
" 1. Visit: https://console.groq.com/keys\n"
|
|
266
|
+
" 2. Get API key: export GROQ_API_KEY=your_key_here\n"
|
|
267
|
+
" 3. Switch: {{cookiecutter.project_slug}} ai config "
|
|
268
|
+
"set-provider groq\n\n"
|
|
269
|
+
"🔥 Google AI Studio (Also Free):\n"
|
|
270
|
+
" 1. Visit: https://aistudio.google.com/app/apikey\n"
|
|
271
|
+
" 2. Get API key: export GOOGLE_API_KEY=your_key_here\n"
|
|
272
|
+
" 3. Switch: {{cookiecutter.project_slug}} ai config set-provider google"
|
|
273
|
+
)
|
|
274
|
+
logger.error(f"Failed to create PUBLIC agent: {e}")
|
|
275
|
+
raise ProviderError(error_msg) from e
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _get_env_var_name(provider: AIProvider) -> str:
|
|
279
|
+
"""Get the environment variable name for a provider."""
|
|
280
|
+
env_var_map = {
|
|
281
|
+
AIProvider.OPENAI: "OPENAI_API_KEY",
|
|
282
|
+
AIProvider.ANTHROPIC: "ANTHROPIC_API_KEY",
|
|
283
|
+
AIProvider.GOOGLE: "GOOGLE_API_KEY",
|
|
284
|
+
AIProvider.GROQ: "GROQ_API_KEY",
|
|
285
|
+
AIProvider.MISTRAL: "MISTRAL_API_KEY",
|
|
286
|
+
AIProvider.COHERE: "COHERE_API_KEY",
|
|
287
|
+
AIProvider.PUBLIC: "PUBLIC_API_KEY", # Not actually needed
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# Debug logging
|
|
291
|
+
# logger.debug(
|
|
292
|
+
# f"Looking up env var for provider: {provider} (type: {type(provider)})"
|
|
293
|
+
# )
|
|
294
|
+
|
|
295
|
+
result = env_var_map.get(provider)
|
|
296
|
+
if result:
|
|
297
|
+
return result
|
|
298
|
+
else:
|
|
299
|
+
# Safe fallback without calling .value in case of type issues
|
|
300
|
+
return f"{str(provider).upper()}_API_KEY"
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _set_provider_env_var(provider: AIProvider, api_key: str) -> None:
|
|
304
|
+
"""Set environment variable for provider API key (PydanticAI 1.0+ expects this)."""
|
|
305
|
+
env_var_map = {
|
|
306
|
+
AIProvider.OPENAI: "OPENAI_API_KEY",
|
|
307
|
+
AIProvider.ANTHROPIC: "ANTHROPIC_API_KEY",
|
|
308
|
+
AIProvider.GOOGLE: "GOOGLE_API_KEY",
|
|
309
|
+
AIProvider.GROQ: "GROQ_API_KEY",
|
|
310
|
+
AIProvider.MISTRAL: "MISTRAL_API_KEY",
|
|
311
|
+
AIProvider.COHERE: "COHERE_API_KEY",
|
|
312
|
+
AIProvider.PUBLIC: "PUBLIC_API_KEY", # Not actually needed
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
env_var = env_var_map.get(provider)
|
|
316
|
+
if env_var and api_key:
|
|
317
|
+
os.environ[env_var] = api_key
|
|
318
|
+
# logger.debug(f"Set {env_var} environment variable")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def validate_provider_support(provider: AIProvider) -> bool:
|
|
322
|
+
"""
|
|
323
|
+
Check if a provider is supported.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
provider: The provider to check
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
bool: True if provider is supported
|
|
330
|
+
"""
|
|
331
|
+
try:
|
|
332
|
+
_get_model_class(provider)
|
|
333
|
+
return True
|
|
334
|
+
except ProviderError:
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def get_supported_providers() -> list[AIProvider]:
|
|
339
|
+
"""
|
|
340
|
+
Get list of supported providers.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
list[AIProvider]: List of supported providers
|
|
344
|
+
"""
|
|
345
|
+
# Return all providers that are defined in the enum
|
|
346
|
+
return [
|
|
347
|
+
AIProvider.OPENAI,
|
|
348
|
+
AIProvider.ANTHROPIC,
|
|
349
|
+
AIProvider.GOOGLE,
|
|
350
|
+
AIProvider.GROQ,
|
|
351
|
+
AIProvider.MISTRAL,
|
|
352
|
+
AIProvider.COHERE,
|
|
353
|
+
AIProvider.PUBLIC,
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def get_provider_model_class(provider: AIProvider):
|
|
358
|
+
"""
|
|
359
|
+
Get the PydanticAI model class for a provider.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
provider: The AI provider
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
Model class for the provider
|
|
366
|
+
|
|
367
|
+
Raises:
|
|
368
|
+
ProviderError: If provider is not supported
|
|
369
|
+
"""
|
|
370
|
+
return _get_model_class(provider)
|