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/service.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI service core implementation using PydanticAI.
|
|
3
|
+
|
|
4
|
+
This module provides the main AIService class that handles AI chat functionality,
|
|
5
|
+
conversation management, and provider integration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import uuid
|
|
9
|
+
from collections.abc import AsyncIterator
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic_ai.exceptions import ModelRetry, UnexpectedModelBehavior
|
|
14
|
+
|
|
15
|
+
from app.core.log import logger
|
|
16
|
+
|
|
17
|
+
from .config import get_ai_config
|
|
18
|
+
from .conversation import ConversationManager
|
|
19
|
+
from .models import (
|
|
20
|
+
Conversation,
|
|
21
|
+
ConversationMessage,
|
|
22
|
+
MessageRole,
|
|
23
|
+
StreamingConversation,
|
|
24
|
+
StreamingMessage,
|
|
25
|
+
)
|
|
26
|
+
from .providers import get_agent
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AIServiceError(Exception):
|
|
30
|
+
"""Base exception for AI service errors."""
|
|
31
|
+
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ProviderError(AIServiceError):
|
|
36
|
+
"""Exception raised when AI provider fails."""
|
|
37
|
+
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ConversationError(AIServiceError):
|
|
42
|
+
"""Exception raised when conversation management fails."""
|
|
43
|
+
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AIService:
|
|
48
|
+
"""
|
|
49
|
+
Core AI service using PydanticAI for chat functionality.
|
|
50
|
+
|
|
51
|
+
Handles chat completions, conversation management, and provider abstraction.
|
|
52
|
+
Creates Agent instances per request for simplicity and resource efficiency.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, settings: Any):
|
|
56
|
+
"""Initialize AI service with configuration."""
|
|
57
|
+
self.settings = settings
|
|
58
|
+
self.config = get_ai_config(settings)
|
|
59
|
+
self.conversation_manager = ConversationManager()
|
|
60
|
+
|
|
61
|
+
# logger.info(
|
|
62
|
+
# f"AI service initialized - Provider: {self.config.provider}, "
|
|
63
|
+
# f"Enabled: {self.config.enabled}"
|
|
64
|
+
# )
|
|
65
|
+
|
|
66
|
+
async def chat(
|
|
67
|
+
self, message: str, conversation_id: str | None = None, user_id: str = "default"
|
|
68
|
+
) -> ConversationMessage:
|
|
69
|
+
"""
|
|
70
|
+
Send a chat message and get AI response.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
message: The user's message
|
|
74
|
+
conversation_id: Optional conversation ID (creates new if None)
|
|
75
|
+
user_id: User identifier for conversation ownership
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
ConversationMessage: The AI's response message
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
AIServiceError: If service is disabled or not configured
|
|
82
|
+
ProviderError: If AI provider fails
|
|
83
|
+
ConversationError: If conversation management fails
|
|
84
|
+
"""
|
|
85
|
+
if not self.config.enabled:
|
|
86
|
+
raise AIServiceError("AI service is disabled")
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
# Setup conversation and add user message
|
|
90
|
+
conversation = self._setup_conversation(message, conversation_id, user_id)
|
|
91
|
+
|
|
92
|
+
# Prepare agent and conversation context
|
|
93
|
+
agent, conversation_context = self._prepare_agent_and_context(conversation)
|
|
94
|
+
|
|
95
|
+
# Get AI response
|
|
96
|
+
start_time = datetime.now(UTC)
|
|
97
|
+
result = await agent.run(conversation_context)
|
|
98
|
+
end_time = datetime.now(UTC)
|
|
99
|
+
response_time_ms = (end_time - start_time).total_seconds() * 1000
|
|
100
|
+
|
|
101
|
+
# Add AI response to conversation
|
|
102
|
+
ai_message = conversation.add_message(
|
|
103
|
+
MessageRole.ASSISTANT, result.output, message_id=str(uuid.uuid4())
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Store conversation ID in message metadata for easy lookup
|
|
107
|
+
ai_message.metadata["conversation_id"] = conversation.id
|
|
108
|
+
|
|
109
|
+
# Finalize conversation (update metadata and save)
|
|
110
|
+
self._finalize_conversation(conversation, response_time_ms)
|
|
111
|
+
|
|
112
|
+
return ai_message
|
|
113
|
+
|
|
114
|
+
except (ModelRetry, UnexpectedModelBehavior) as e:
|
|
115
|
+
error_msg = f"AI provider error: {e}"
|
|
116
|
+
logger.error(error_msg)
|
|
117
|
+
raise ProviderError(error_msg) from e
|
|
118
|
+
except Exception as e:
|
|
119
|
+
error_msg = f"Chat processing failed: {e}"
|
|
120
|
+
logger.error(error_msg)
|
|
121
|
+
raise AIServiceError(error_msg) from e
|
|
122
|
+
|
|
123
|
+
async def stream_chat(
|
|
124
|
+
self,
|
|
125
|
+
message: str,
|
|
126
|
+
conversation_id: str | None = None,
|
|
127
|
+
user_id: str = "default",
|
|
128
|
+
stream_delta: bool = False,
|
|
129
|
+
) -> AsyncIterator[StreamingMessage]:
|
|
130
|
+
"""
|
|
131
|
+
Stream a chat message with real-time response generation.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
message: The user's message
|
|
135
|
+
conversation_id: Optional conversation ID (creates new if None)
|
|
136
|
+
user_id: User identifier for conversation ownership
|
|
137
|
+
stream_delta: Whether to stream delta changes or full content
|
|
138
|
+
|
|
139
|
+
Yields:
|
|
140
|
+
StreamingMessage: Real-time message chunks
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
AIServiceError: If service is disabled or not configured
|
|
144
|
+
ProviderError: If AI provider fails
|
|
145
|
+
ConversationError: If conversation management fails
|
|
146
|
+
"""
|
|
147
|
+
if not self.config.enabled:
|
|
148
|
+
raise AIServiceError("AI service is disabled")
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
# Setup conversation and add user message
|
|
152
|
+
conversation = self._setup_conversation(message, conversation_id, user_id)
|
|
153
|
+
|
|
154
|
+
# Create streaming conversation wrapper
|
|
155
|
+
streaming_conv = StreamingConversation(conversation=conversation)
|
|
156
|
+
streaming_conv.reset_stream()
|
|
157
|
+
|
|
158
|
+
# Prepare agent and conversation context
|
|
159
|
+
agent, conversation_context = self._prepare_agent_and_context(conversation)
|
|
160
|
+
|
|
161
|
+
# Start streaming
|
|
162
|
+
start_time = datetime.now(UTC)
|
|
163
|
+
|
|
164
|
+
# Generate a message ID for the streaming response
|
|
165
|
+
message_id = str(uuid.uuid4())
|
|
166
|
+
|
|
167
|
+
# Use PydanticAI's run_stream method for streaming
|
|
168
|
+
async with agent.run_stream(conversation_context) as result:
|
|
169
|
+
# Stream text chunks
|
|
170
|
+
async for text_chunk in result.stream_text(delta=stream_delta):
|
|
171
|
+
# Accumulate content
|
|
172
|
+
total_content = streaming_conv.accumulate_content(
|
|
173
|
+
text_chunk, is_delta=stream_delta
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Yield streaming message chunk
|
|
177
|
+
yield StreamingMessage(
|
|
178
|
+
content=text_chunk if stream_delta else total_content,
|
|
179
|
+
is_final=False,
|
|
180
|
+
is_delta=stream_delta,
|
|
181
|
+
message_id=message_id,
|
|
182
|
+
conversation_id=conversation.id,
|
|
183
|
+
metadata={
|
|
184
|
+
"provider": self.config.provider,
|
|
185
|
+
"model": self.config.model,
|
|
186
|
+
"stream_delta": stream_delta,
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
end_time = datetime.now(UTC)
|
|
191
|
+
response_time_ms = (end_time - start_time).total_seconds() * 1000
|
|
192
|
+
|
|
193
|
+
# Add final message to conversation using accumulated streaming content
|
|
194
|
+
final_content = streaming_conv.accumulated_content or "No content received"
|
|
195
|
+
ai_message = conversation.add_message(
|
|
196
|
+
MessageRole.ASSISTANT, final_content, message_id=message_id
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Store conversation metadata
|
|
200
|
+
ai_message.metadata["conversation_id"] = conversation.id
|
|
201
|
+
|
|
202
|
+
# Finalize conversation (update metadata and save)
|
|
203
|
+
self._finalize_conversation(
|
|
204
|
+
conversation, response_time_ms, is_streaming=True
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Yield final streaming message
|
|
208
|
+
yield StreamingMessage(
|
|
209
|
+
content=final_content,
|
|
210
|
+
is_final=True,
|
|
211
|
+
is_delta=False,
|
|
212
|
+
message_id=message_id,
|
|
213
|
+
conversation_id=conversation.id,
|
|
214
|
+
metadata={
|
|
215
|
+
"provider": self.config.provider,
|
|
216
|
+
"model": self.config.model,
|
|
217
|
+
"response_time_ms": response_time_ms,
|
|
218
|
+
"stream_complete": True,
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
except (ModelRetry, UnexpectedModelBehavior) as e:
|
|
223
|
+
error_msg = f"AI provider streaming error: {e}"
|
|
224
|
+
logger.error(error_msg)
|
|
225
|
+
raise ProviderError(error_msg) from e
|
|
226
|
+
except Exception as e:
|
|
227
|
+
error_msg = f"Streaming failed: {e}"
|
|
228
|
+
logger.error(error_msg)
|
|
229
|
+
raise AIServiceError(error_msg) from e
|
|
230
|
+
|
|
231
|
+
def _setup_conversation(
|
|
232
|
+
self,
|
|
233
|
+
message: str,
|
|
234
|
+
conversation_id: str | None,
|
|
235
|
+
user_id: str,
|
|
236
|
+
) -> Conversation:
|
|
237
|
+
"""
|
|
238
|
+
Get or create conversation and add user message.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
message: The user's message
|
|
242
|
+
conversation_id: Optional conversation ID (creates new if None)
|
|
243
|
+
user_id: User identifier for conversation ownership
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Conversation: The conversation with user message added
|
|
247
|
+
|
|
248
|
+
Raises:
|
|
249
|
+
ConversationError: If conversation_id provided but not found
|
|
250
|
+
"""
|
|
251
|
+
# Get or create conversation
|
|
252
|
+
if conversation_id:
|
|
253
|
+
conversation = self.conversation_manager.get_conversation(conversation_id)
|
|
254
|
+
if not conversation:
|
|
255
|
+
raise ConversationError(f"Conversation {conversation_id} not found")
|
|
256
|
+
else:
|
|
257
|
+
conversation = self.conversation_manager.create_conversation(
|
|
258
|
+
provider=self.config.provider,
|
|
259
|
+
model=self.config.model,
|
|
260
|
+
user_id=user_id,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Add user message to conversation
|
|
264
|
+
conversation.add_message(MessageRole.USER, message)
|
|
265
|
+
|
|
266
|
+
return conversation
|
|
267
|
+
|
|
268
|
+
def _prepare_agent_and_context(self, conversation: Conversation) -> tuple[Any, str]:
|
|
269
|
+
"""
|
|
270
|
+
Create agent for request and build conversation context.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
conversation: The conversation to prepare context from
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
tuple[Any, str]: (agent instance, conversation context string)
|
|
277
|
+
"""
|
|
278
|
+
# Create agent for this request
|
|
279
|
+
agent = get_agent(self.config, self.settings)
|
|
280
|
+
|
|
281
|
+
# Build conversation context for AI
|
|
282
|
+
conversation_context = self._build_conversation_context(conversation)
|
|
283
|
+
|
|
284
|
+
return agent, conversation_context
|
|
285
|
+
|
|
286
|
+
def _finalize_conversation(
|
|
287
|
+
self,
|
|
288
|
+
conversation: Conversation,
|
|
289
|
+
response_time_ms: float,
|
|
290
|
+
is_streaming: bool = False,
|
|
291
|
+
) -> None:
|
|
292
|
+
"""
|
|
293
|
+
Update conversation metadata and save.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
conversation: The conversation to finalize
|
|
297
|
+
response_time_ms: Response time in milliseconds
|
|
298
|
+
is_streaming: Whether this was a streaming response
|
|
299
|
+
"""
|
|
300
|
+
# Update conversation metadata
|
|
301
|
+
metadata_update = {
|
|
302
|
+
"last_response_time_ms": response_time_ms,
|
|
303
|
+
"total_messages": conversation.get_message_count(),
|
|
304
|
+
"last_activity": datetime.now(UTC).isoformat(),
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if is_streaming:
|
|
308
|
+
metadata_update["streaming"] = True
|
|
309
|
+
|
|
310
|
+
conversation.metadata.update(metadata_update)
|
|
311
|
+
|
|
312
|
+
# Save conversation
|
|
313
|
+
self.conversation_manager.save_conversation(conversation)
|
|
314
|
+
|
|
315
|
+
def _build_conversation_context(self, conversation: Conversation) -> str:
|
|
316
|
+
"""
|
|
317
|
+
Build conversation context for AI from message history.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
conversation: The conversation with message history
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
str: Formatted conversation context for AI
|
|
324
|
+
"""
|
|
325
|
+
if not conversation.messages:
|
|
326
|
+
return ""
|
|
327
|
+
|
|
328
|
+
# For continuous conversation, include recent message history
|
|
329
|
+
# Limit to last 10 messages to manage context window
|
|
330
|
+
recent_messages = conversation.messages[-10:]
|
|
331
|
+
|
|
332
|
+
# Format messages for context
|
|
333
|
+
context_parts = []
|
|
334
|
+
for msg in recent_messages[:-1]: # Exclude the latest message (just added)
|
|
335
|
+
if msg.role == MessageRole.USER:
|
|
336
|
+
context_parts.append(f"User: {msg.content}")
|
|
337
|
+
elif msg.role == MessageRole.ASSISTANT:
|
|
338
|
+
context_parts.append(f"Assistant: {msg.content}")
|
|
339
|
+
|
|
340
|
+
# Add the current user message
|
|
341
|
+
latest_message = conversation.get_last_message()
|
|
342
|
+
if latest_message and latest_message.role == MessageRole.USER:
|
|
343
|
+
if context_parts:
|
|
344
|
+
# Include conversation history + current message
|
|
345
|
+
return "\n".join(context_parts) + f"\n\nUser: {latest_message.content}"
|
|
346
|
+
else:
|
|
347
|
+
# First message in conversation
|
|
348
|
+
return latest_message.content
|
|
349
|
+
|
|
350
|
+
return ""
|
|
351
|
+
|
|
352
|
+
def get_conversation(self, conversation_id: str) -> Conversation | None:
|
|
353
|
+
"""Get a conversation by ID."""
|
|
354
|
+
return self.conversation_manager.get_conversation(conversation_id)
|
|
355
|
+
|
|
356
|
+
def list_conversations(self, user_id: str = "default") -> list[Conversation]:
|
|
357
|
+
"""List all conversations for a user."""
|
|
358
|
+
return self.conversation_manager.list_conversations(user_id)
|
|
359
|
+
|
|
360
|
+
def get_service_status(self) -> dict[str, Any]:
|
|
361
|
+
"""Get current service status and metrics."""
|
|
362
|
+
total_conversations = len(self.conversation_manager.conversations)
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
"enabled": self.config.enabled,
|
|
366
|
+
"provider": self.config.provider,
|
|
367
|
+
"model": self.config.model,
|
|
368
|
+
"agent_initialized": True, # Agents created per request, always available
|
|
369
|
+
"total_conversations": total_conversations,
|
|
370
|
+
"configuration_valid": len(
|
|
371
|
+
self.config.validate_configuration(self.settings)
|
|
372
|
+
)
|
|
373
|
+
== 0,
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
def validate_service(self) -> list[str]:
|
|
377
|
+
"""Validate service configuration and return any issues."""
|
|
378
|
+
errors = []
|
|
379
|
+
|
|
380
|
+
# Check configuration
|
|
381
|
+
config_errors = self.config.validate_configuration(self.settings)
|
|
382
|
+
errors.extend(config_errors)
|
|
383
|
+
|
|
384
|
+
# Check agent initialization (agents created per request,
|
|
385
|
+
# always available when enabled)
|
|
386
|
+
# No persistent agent check needed in agent-per-request pattern
|
|
387
|
+
|
|
388
|
+
return errors
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Authentication services."""
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Authentication service utilities."""
|
|
2
|
+
|
|
3
|
+
from fastapi import HTTPException, status
|
|
4
|
+
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
5
|
+
|
|
6
|
+
from app.core.security import verify_token
|
|
7
|
+
from app.models.user import User
|
|
8
|
+
from app.services.auth.user_service import UserService
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def get_current_user_from_token(token: str, db: AsyncSession) -> User:
|
|
12
|
+
"""Get current user from JWT token."""
|
|
13
|
+
credentials_exception = HTTPException(
|
|
14
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
15
|
+
detail="Could not validate credentials",
|
|
16
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Verify token
|
|
20
|
+
payload = verify_token(token)
|
|
21
|
+
if payload is None:
|
|
22
|
+
raise credentials_exception
|
|
23
|
+
|
|
24
|
+
# Get user email from token
|
|
25
|
+
email = payload.get("sub")
|
|
26
|
+
if not isinstance(email, str) or email is None:
|
|
27
|
+
raise credentials_exception
|
|
28
|
+
|
|
29
|
+
# Get user from database
|
|
30
|
+
user_service = UserService(db)
|
|
31
|
+
user = await user_service.get_user_by_email(email)
|
|
32
|
+
if user is None:
|
|
33
|
+
raise credentials_exception
|
|
34
|
+
|
|
35
|
+
# Check if user is active
|
|
36
|
+
if not user.is_active:
|
|
37
|
+
raise HTTPException(
|
|
38
|
+
status_code=status.HTTP_401_UNAUTHORIZED, detail="Inactive user"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return user
|
aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/health.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auth service health check functions.
|
|
3
|
+
|
|
4
|
+
Health monitoring for authentication and authorization service functionality.
|
|
5
|
+
Checks JWT configuration, database connectivity, and service-specific metrics.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from app.core.config import settings
|
|
9
|
+
from app.core.log import logger
|
|
10
|
+
from app.services.system.models import ComponentStatus, ComponentStatusType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def check_auth_service_health() -> ComponentStatus:
|
|
14
|
+
"""
|
|
15
|
+
Check auth service health including JWT configuration and dependencies.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
ComponentStatus indicating auth service health
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
# Check JWT configuration
|
|
22
|
+
jwt_errors = []
|
|
23
|
+
|
|
24
|
+
# Verify JWT secret key is configured
|
|
25
|
+
# Note: 32 characters = 256 bits, which is secure for HS256 HMAC-SHA256 signing
|
|
26
|
+
if not hasattr(settings, "SECRET_KEY") or not settings.SECRET_KEY:
|
|
27
|
+
jwt_errors.append("SECRET_KEY not configured")
|
|
28
|
+
elif len(settings.SECRET_KEY) < 32:
|
|
29
|
+
jwt_errors.append("SECRET_KEY too short (minimum 32 characters for HS256)")
|
|
30
|
+
|
|
31
|
+
# Verify JWT algorithm is supported
|
|
32
|
+
algorithm = getattr(settings, "ALGORITHM", "HS256")
|
|
33
|
+
supported_algorithms = ["HS256", "HS384", "HS512", "RS256", "RS384", "RS512"]
|
|
34
|
+
if algorithm not in supported_algorithms:
|
|
35
|
+
jwt_errors.append(f"Unsupported JWT algorithm: {algorithm}")
|
|
36
|
+
|
|
37
|
+
# Verify token expiration is configured
|
|
38
|
+
access_token_expire = getattr(settings, "ACCESS_TOKEN_EXPIRE_MINUTES", None)
|
|
39
|
+
if access_token_expire is None or access_token_expire <= 0:
|
|
40
|
+
jwt_errors.append("ACCESS_TOKEN_EXPIRE_MINUTES not properly configured")
|
|
41
|
+
|
|
42
|
+
# Check database dependency for user storage
|
|
43
|
+
database_available = True
|
|
44
|
+
try:
|
|
45
|
+
from sqlalchemy import text
|
|
46
|
+
|
|
47
|
+
from app.core.db import db_session
|
|
48
|
+
|
|
49
|
+
with db_session() as session:
|
|
50
|
+
# Test database connectivity with a simple query
|
|
51
|
+
session.execute(text("SELECT 1"))
|
|
52
|
+
except ImportError:
|
|
53
|
+
database_available = False
|
|
54
|
+
jwt_errors.append("Database module not available for user storage")
|
|
55
|
+
except Exception as e:
|
|
56
|
+
database_available = False
|
|
57
|
+
jwt_errors.append(f"Database connectivity issue: {str(e)}")
|
|
58
|
+
|
|
59
|
+
# Determine service status
|
|
60
|
+
if jwt_errors:
|
|
61
|
+
if not database_available:
|
|
62
|
+
status = ComponentStatusType.UNHEALTHY
|
|
63
|
+
message = f"Auth service misconfigured: {'; '.join(jwt_errors)}"
|
|
64
|
+
else:
|
|
65
|
+
# Some JWT config issues but database is available
|
|
66
|
+
status = ComponentStatusType.WARNING
|
|
67
|
+
message = (
|
|
68
|
+
f"Auth service has configuration warnings: {'; '.join(jwt_errors)}"
|
|
69
|
+
)
|
|
70
|
+
else:
|
|
71
|
+
status = ComponentStatusType.HEALTHY
|
|
72
|
+
message = "Auth service configured and ready"
|
|
73
|
+
|
|
74
|
+
# Get user count for display (limited to avoid performance issues)
|
|
75
|
+
user_count = 0
|
|
76
|
+
user_count_display = "0"
|
|
77
|
+
if database_available:
|
|
78
|
+
try:
|
|
79
|
+
from sqlmodel import select
|
|
80
|
+
|
|
81
|
+
from app.core.db import db_session
|
|
82
|
+
from app.models.user import User
|
|
83
|
+
|
|
84
|
+
with db_session() as session:
|
|
85
|
+
# Count up to 101 users to determine if we should show "100+"
|
|
86
|
+
statement = select(User).limit(101)
|
|
87
|
+
result = session.exec(statement)
|
|
88
|
+
users = list(result.all())
|
|
89
|
+
user_count = len(users)
|
|
90
|
+
|
|
91
|
+
user_count_display = "100+" if user_count > 100 else str(user_count)
|
|
92
|
+
except Exception:
|
|
93
|
+
# If user counting fails, leave as 0
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
# Format token expiry for display
|
|
97
|
+
token_expiry_display = "30 min" # Default
|
|
98
|
+
if access_token_expire:
|
|
99
|
+
if access_token_expire >= 60:
|
|
100
|
+
hours = access_token_expire // 60
|
|
101
|
+
token_expiry_display = "1 hour" if hours == 1 else f"{hours} hours"
|
|
102
|
+
else:
|
|
103
|
+
token_expiry_display = f"{access_token_expire} min"
|
|
104
|
+
|
|
105
|
+
# Determine security level based on configuration
|
|
106
|
+
security_level = "standard"
|
|
107
|
+
if jwt_errors:
|
|
108
|
+
security_level = "basic"
|
|
109
|
+
elif (
|
|
110
|
+
hasattr(settings, "SECRET_KEY")
|
|
111
|
+
and settings.SECRET_KEY
|
|
112
|
+
and len(settings.SECRET_KEY) >= 64
|
|
113
|
+
and algorithm in ["RS256", "RS384", "RS512"]
|
|
114
|
+
):
|
|
115
|
+
security_level = "high"
|
|
116
|
+
|
|
117
|
+
# Collect metadata
|
|
118
|
+
metadata = {
|
|
119
|
+
"service_type": "auth",
|
|
120
|
+
"jwt_algorithm": algorithm,
|
|
121
|
+
"token_expiry_minutes": access_token_expire,
|
|
122
|
+
"token_expiry_display": token_expiry_display,
|
|
123
|
+
"database_available": database_available,
|
|
124
|
+
"secret_key_configured": hasattr(settings, "SECRET_KEY")
|
|
125
|
+
and bool(settings.SECRET_KEY),
|
|
126
|
+
"secret_key_length": len(getattr(settings, "SECRET_KEY", ""))
|
|
127
|
+
if hasattr(settings, "SECRET_KEY")
|
|
128
|
+
else 0,
|
|
129
|
+
"user_count": user_count,
|
|
130
|
+
"user_count_display": user_count_display,
|
|
131
|
+
"security_level": security_level,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# Add configuration issues to metadata if any
|
|
135
|
+
if jwt_errors:
|
|
136
|
+
metadata["configuration_issues"] = jwt_errors
|
|
137
|
+
|
|
138
|
+
# Add dependency status
|
|
139
|
+
metadata["dependencies"] = {
|
|
140
|
+
"database": "available" if database_available else "unavailable",
|
|
141
|
+
"backend": "required", # Auth service always requires backend
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return ComponentStatus(
|
|
145
|
+
name="auth",
|
|
146
|
+
status=status,
|
|
147
|
+
message=message,
|
|
148
|
+
response_time_ms=None, # Will be set by caller
|
|
149
|
+
metadata=metadata,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.error(f"Auth service health check failed: {e}")
|
|
154
|
+
return ComponentStatus(
|
|
155
|
+
name="auth",
|
|
156
|
+
status=ComponentStatusType.UNHEALTHY,
|
|
157
|
+
message=f"Auth service health check failed: {str(e)}",
|
|
158
|
+
response_time_ms=None,
|
|
159
|
+
metadata={
|
|
160
|
+
"service_type": "auth",
|
|
161
|
+
"error": str(e),
|
|
162
|
+
"error_type": "health_check_failure",
|
|
163
|
+
},
|
|
164
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""User management service."""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
|
|
5
|
+
from sqlmodel import select
|
|
6
|
+
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
7
|
+
|
|
8
|
+
from app.core.security import get_password_hash
|
|
9
|
+
from app.models.user import User, UserCreate
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UserService:
|
|
13
|
+
"""Service for managing users with async database operations."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, db: AsyncSession):
|
|
16
|
+
self.db = db
|
|
17
|
+
|
|
18
|
+
async def create_user(self, user_data: UserCreate) -> User:
|
|
19
|
+
"""Create a new user asynchronously."""
|
|
20
|
+
# Hash the password
|
|
21
|
+
hashed_password = get_password_hash(user_data.password)
|
|
22
|
+
|
|
23
|
+
# Create user object
|
|
24
|
+
user = User(
|
|
25
|
+
email=user_data.email,
|
|
26
|
+
full_name=user_data.full_name,
|
|
27
|
+
hashed_password=hashed_password,
|
|
28
|
+
is_active=user_data.is_active,
|
|
29
|
+
created_at=datetime.now(UTC),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Save to database
|
|
33
|
+
self.db.add(user)
|
|
34
|
+
await self.db.commit()
|
|
35
|
+
await self.db.refresh(user)
|
|
36
|
+
|
|
37
|
+
return user
|
|
38
|
+
|
|
39
|
+
async def get_user_by_email(self, email: str) -> User | None:
|
|
40
|
+
"""Get user by email address asynchronously."""
|
|
41
|
+
statement = select(User).where(User.email == email)
|
|
42
|
+
result = await self.db.exec(statement)
|
|
43
|
+
return result.first()
|
|
44
|
+
|
|
45
|
+
async def get_user_by_id(self, user_id: int) -> User | None:
|
|
46
|
+
"""Get user by ID asynchronously."""
|
|
47
|
+
return await self.db.get(User, user_id)
|
|
48
|
+
|
|
49
|
+
async def update_user(self, user_id: int, **updates) -> User | None:
|
|
50
|
+
"""Update user data asynchronously."""
|
|
51
|
+
user = await self.get_user_by_id(user_id)
|
|
52
|
+
if not user:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
for field, value in updates.items():
|
|
56
|
+
if hasattr(user, field):
|
|
57
|
+
setattr(user, field, value)
|
|
58
|
+
|
|
59
|
+
user.updated_at = datetime.now(UTC)
|
|
60
|
+
self.db.add(user)
|
|
61
|
+
await self.db.commit()
|
|
62
|
+
await self.db.refresh(user)
|
|
63
|
+
|
|
64
|
+
return user
|
|
65
|
+
|
|
66
|
+
async def deactivate_user(self, user_id: int) -> User | None:
|
|
67
|
+
"""Deactivate a user account asynchronously."""
|
|
68
|
+
return await self.update_user(user_id, is_active=False)
|
|
69
|
+
|
|
70
|
+
async def list_users(self) -> list[User]:
|
|
71
|
+
"""List all users in the system asynchronously."""
|
|
72
|
+
statement = select(User).order_by(User.created_at.desc())
|
|
73
|
+
result = await self.db.exec(statement)
|
|
74
|
+
return list(result.all())
|
|
75
|
+
|
|
76
|
+
async def find_existing_emails_with_prefix(
|
|
77
|
+
self, prefix: str, domain: str
|
|
78
|
+
) -> list[str]:
|
|
79
|
+
"""Find existing emails that match the pattern prefix{number}@domain async."""
|
|
80
|
+
pattern = f"{prefix}%@{domain}"
|
|
81
|
+
statement = select(User.email).where(User.email.like(pattern))
|
|
82
|
+
result = await self.db.exec(statement)
|
|
83
|
+
return list(result.all())
|