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
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI service CLI commands.
|
|
3
|
+
|
|
4
|
+
Command-line interface for AI service management and chat functionality.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.prompt import Prompt
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
from ..core.config import settings
|
|
17
|
+
from ..services.ai.config import get_ai_config
|
|
18
|
+
from ..services.ai.models import (
|
|
19
|
+
AIProvider,
|
|
20
|
+
MessageRole,
|
|
21
|
+
get_free_providers,
|
|
22
|
+
get_provider_capabilities,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
app = typer.Typer(help="AI service management and chat commands")
|
|
26
|
+
console = Console()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@contextmanager
|
|
30
|
+
def suppress_logs(level: int = logging.ERROR):
|
|
31
|
+
"""
|
|
32
|
+
Context manager to temporarily suppress logs during interactive chat.
|
|
33
|
+
|
|
34
|
+
Sets the root logger to ERROR level to hide INFO/DEBUG/WARNING logs while
|
|
35
|
+
preserving ERROR logs for critical issues.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
level: Minimum log level to show (default: ERROR)
|
|
39
|
+
"""
|
|
40
|
+
# Get the root logger and remember original level
|
|
41
|
+
root_logger = logging.getLogger()
|
|
42
|
+
original_level = root_logger.level
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
# Temporarily raise log level to suppress info/debug logs
|
|
46
|
+
root_logger.setLevel(level)
|
|
47
|
+
yield
|
|
48
|
+
finally:
|
|
49
|
+
# Restore original log level
|
|
50
|
+
root_logger.setLevel(original_level)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Configuration command group
|
|
55
|
+
config_app = typer.Typer(help="AI service configuration commands")
|
|
56
|
+
app.add_typer(config_app, name="config")
|
|
57
|
+
|
|
58
|
+
# Provider command group
|
|
59
|
+
providers_app = typer.Typer(help="AI provider management commands")
|
|
60
|
+
app.add_typer(providers_app, name="providers")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@app.command()
|
|
64
|
+
def version() -> None:
|
|
65
|
+
"""Show AI service version and configuration."""
|
|
66
|
+
ai_config = get_ai_config(settings)
|
|
67
|
+
|
|
68
|
+
typer.echo("🤖 AI Service Configuration System")
|
|
69
|
+
typer.echo("Engine: PydanticAI")
|
|
70
|
+
typer.echo(f"Status: {'✅ Enabled' if ai_config.enabled else '❌ Disabled'}")
|
|
71
|
+
typer.echo(f"Provider: {ai_config.provider}")
|
|
72
|
+
typer.echo(f"Model: {ai_config.model}")
|
|
73
|
+
typer.echo("")
|
|
74
|
+
typer.echo("Available commands:")
|
|
75
|
+
typer.echo(" • ai chat \"message\" - Send a chat message")
|
|
76
|
+
typer.echo(" • ai config show - Show detailed configuration")
|
|
77
|
+
typer.echo(" • ai config validate - Validate current configuration")
|
|
78
|
+
typer.echo(" • ai providers list - List available providers")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Chat command group
|
|
82
|
+
chat_app = typer.Typer(help="AI chat commands")
|
|
83
|
+
app.add_typer(chat_app, name="chat")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@chat_app.callback(invoke_without_command=True)
|
|
87
|
+
def chat_main(
|
|
88
|
+
ctx: typer.Context,
|
|
89
|
+
message: str | None = typer.Option(
|
|
90
|
+
None, "--message", "-m", help="Send single message"
|
|
91
|
+
),
|
|
92
|
+
stream: bool = typer.Option(
|
|
93
|
+
True, "--stream/--no-stream", help="Enable streaming output"
|
|
94
|
+
),
|
|
95
|
+
conversation_id: str | None = typer.Option(
|
|
96
|
+
None, "--conversation-id", "-c", help="Conversation ID"
|
|
97
|
+
),
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Start interactive chat session or send single message."""
|
|
100
|
+
# If a subcommand was invoked, don't run the chat logic
|
|
101
|
+
if ctx.invoked_subcommand is not None:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
import asyncio
|
|
105
|
+
|
|
106
|
+
from app.services.ai.service import AIService
|
|
107
|
+
|
|
108
|
+
async def run_chat() -> None:
|
|
109
|
+
try:
|
|
110
|
+
with suppress_logs():
|
|
111
|
+
ai_service = AIService(settings)
|
|
112
|
+
if message:
|
|
113
|
+
# Single message mode
|
|
114
|
+
await _single_message(ai_service, message, conversation_id, stream)
|
|
115
|
+
else:
|
|
116
|
+
# Interactive session mode
|
|
117
|
+
await _interactive_chat_session(ai_service, conversation_id)
|
|
118
|
+
except KeyboardInterrupt:
|
|
119
|
+
typer.echo("\n⚠️ Chat interrupted by user", err=True)
|
|
120
|
+
raise typer.Exit(1)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
typer.echo(f"❌ Error: {e}", err=True)
|
|
123
|
+
raise typer.Exit(1)
|
|
124
|
+
|
|
125
|
+
asyncio.run(run_chat())
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@chat_app.command("send")
|
|
129
|
+
def chat_send(
|
|
130
|
+
message: str = typer.Argument(..., help="Message to send to AI"),
|
|
131
|
+
stream: bool = typer.Option(
|
|
132
|
+
True, "--stream/--no-stream", help="Enable streaming output"
|
|
133
|
+
),
|
|
134
|
+
conversation_id: str | None = typer.Option(
|
|
135
|
+
None, "--conversation-id", "-c", help="Conversation ID"
|
|
136
|
+
),
|
|
137
|
+
user_id: str = typer.Option("cli-user", "--user-id", "-u", help="User identifier"),
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Send a chat message and get AI response."""
|
|
140
|
+
import asyncio
|
|
141
|
+
|
|
142
|
+
from app.services.ai.service import AIService
|
|
143
|
+
|
|
144
|
+
async def send_message() -> None:
|
|
145
|
+
try:
|
|
146
|
+
with suppress_logs():
|
|
147
|
+
ai_service = AIService(settings)
|
|
148
|
+
|
|
149
|
+
# Disable streaming for PUBLIC provider
|
|
150
|
+
# (fake streaming causes duplicates)
|
|
151
|
+
use_streaming = stream
|
|
152
|
+
if ai_service.config.provider == AIProvider.PUBLIC:
|
|
153
|
+
use_streaming = False
|
|
154
|
+
|
|
155
|
+
if use_streaming:
|
|
156
|
+
await _stream_chat_response(
|
|
157
|
+
ai_service, message, conversation_id, user_id, verbose=True
|
|
158
|
+
)
|
|
159
|
+
else:
|
|
160
|
+
response = await ai_service.chat(
|
|
161
|
+
message=message,
|
|
162
|
+
conversation_id=conversation_id,
|
|
163
|
+
user_id=user_id
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Use new shared rendering functions
|
|
167
|
+
from app.cli.ai_rendering import (
|
|
168
|
+
render_ai_header,
|
|
169
|
+
render_conversation_metadata,
|
|
170
|
+
render_markdown_response,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Show conversation info
|
|
174
|
+
conv_id = response.metadata.get("conversation_id", "unknown")
|
|
175
|
+
conversation = ai_service.get_conversation(conv_id)
|
|
176
|
+
if conversation:
|
|
177
|
+
typer.echo(f"💬 Conversation: {conversation.id}")
|
|
178
|
+
if conversation.title:
|
|
179
|
+
typer.echo(f"📝 Title: {conversation.title}")
|
|
180
|
+
|
|
181
|
+
console.print()
|
|
182
|
+
|
|
183
|
+
# Render response using new shared functions
|
|
184
|
+
render_ai_header(console, inline=True)
|
|
185
|
+
render_markdown_response(console, response.content)
|
|
186
|
+
|
|
187
|
+
# Show response metadata using new shared function
|
|
188
|
+
if conversation:
|
|
189
|
+
response_time = conversation.metadata.get(
|
|
190
|
+
"last_response_time_ms"
|
|
191
|
+
)
|
|
192
|
+
render_conversation_metadata(
|
|
193
|
+
console,
|
|
194
|
+
conversation.id,
|
|
195
|
+
message_count=conversation.get_message_count(),
|
|
196
|
+
response_time=response_time,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
except KeyboardInterrupt:
|
|
200
|
+
typer.echo("\n⚠️ Chat interrupted by user", err=True)
|
|
201
|
+
raise typer.Exit(1)
|
|
202
|
+
except Exception as e:
|
|
203
|
+
typer.echo(f"❌ Error: {e}", err=True)
|
|
204
|
+
raise typer.Exit(1)
|
|
205
|
+
|
|
206
|
+
asyncio.run(send_message())
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@chat_app.command("list")
|
|
210
|
+
def chat_list(
|
|
211
|
+
user_id: str = typer.Option("cli-user", "--user-id", "-u", help="User identifier"),
|
|
212
|
+
limit: int = typer.Option(
|
|
213
|
+
10, "--limit", "-l", help="Number of conversations to show"
|
|
214
|
+
),
|
|
215
|
+
) -> None:
|
|
216
|
+
"""List conversations for a user."""
|
|
217
|
+
from app.services.ai.service import AIService
|
|
218
|
+
|
|
219
|
+
with suppress_logs():
|
|
220
|
+
ai_service = AIService(settings)
|
|
221
|
+
conversations = ai_service.list_conversations(user_id)[:limit]
|
|
222
|
+
|
|
223
|
+
if not conversations:
|
|
224
|
+
typer.echo(f"No conversations found for user: {user_id}")
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
typer.echo(f"💬 Conversations for {user_id}:")
|
|
228
|
+
typer.echo("")
|
|
229
|
+
|
|
230
|
+
for conv in conversations:
|
|
231
|
+
title = conv.title or "Untitled"
|
|
232
|
+
messages = conv.get_message_count()
|
|
233
|
+
updated = conv.updated_at.strftime("%Y-%m-%d %H:%M")
|
|
234
|
+
|
|
235
|
+
typer.echo(f"• {conv.id[:8]}... - {title}")
|
|
236
|
+
typer.echo(f" 📊 {messages} messages | 🕒 {updated}")
|
|
237
|
+
typer.echo("")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@chat_app.command("history")
|
|
241
|
+
def chat_history(
|
|
242
|
+
conversation_id: str = typer.Argument(..., help="Conversation ID"),
|
|
243
|
+
user_id: str = typer.Option("cli-user", "--user-id", "-u", help="User identifier"),
|
|
244
|
+
) -> None:
|
|
245
|
+
"""View conversation history."""
|
|
246
|
+
from app.services.ai.service import AIService
|
|
247
|
+
|
|
248
|
+
with suppress_logs():
|
|
249
|
+
ai_service = AIService(settings)
|
|
250
|
+
conversation = ai_service.get_conversation(conversation_id)
|
|
251
|
+
|
|
252
|
+
if not conversation:
|
|
253
|
+
typer.echo(f"❌ Conversation not found: {conversation_id}")
|
|
254
|
+
raise typer.Exit(1)
|
|
255
|
+
|
|
256
|
+
# Check if user owns conversation
|
|
257
|
+
if conversation.metadata.get("user_id") != user_id:
|
|
258
|
+
typer.echo("❌ Access denied: You don't own this conversation")
|
|
259
|
+
raise typer.Exit(1)
|
|
260
|
+
|
|
261
|
+
typer.echo(f"💬 Conversation: {conversation_id}")
|
|
262
|
+
if conversation.title:
|
|
263
|
+
typer.echo(f"📝 Title: {conversation.title}")
|
|
264
|
+
typer.echo(f"🤖 Provider: {conversation.provider.value}")
|
|
265
|
+
typer.echo(f"📊 Messages: {conversation.get_message_count()}")
|
|
266
|
+
typer.echo("")
|
|
267
|
+
|
|
268
|
+
for i, message in enumerate(conversation.messages):
|
|
269
|
+
timestamp = message.timestamp.strftime("%H:%M:%S")
|
|
270
|
+
role_icon = "👤" if message.role == MessageRole.USER else "🤖"
|
|
271
|
+
|
|
272
|
+
typer.echo(f"{role_icon} [{timestamp}] {message.content}")
|
|
273
|
+
if i < len(conversation.messages) - 1:
|
|
274
|
+
typer.echo("")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@config_app.command("show")
|
|
278
|
+
def config_show() -> None:
|
|
279
|
+
"""Show detailed AI service configuration."""
|
|
280
|
+
ai_config = get_ai_config(settings)
|
|
281
|
+
|
|
282
|
+
typer.echo("🔧 AI Service Configuration")
|
|
283
|
+
typer.echo("=" * 40)
|
|
284
|
+
typer.echo(f"Enabled: {ai_config.enabled}")
|
|
285
|
+
typer.echo(f"Provider: {ai_config.provider}")
|
|
286
|
+
typer.echo(f"Model: {ai_config.model}")
|
|
287
|
+
typer.echo(f"Temperature: {ai_config.temperature}")
|
|
288
|
+
typer.echo(f"Max Tokens: {ai_config.max_tokens}")
|
|
289
|
+
typer.echo(f"Timeout: {ai_config.timeout_seconds}s")
|
|
290
|
+
|
|
291
|
+
# Provider-specific configuration
|
|
292
|
+
provider_config = ai_config.get_provider_config(settings)
|
|
293
|
+
typer.echo(f"\n🔐 Provider Configuration ({ai_config.provider}):")
|
|
294
|
+
typer.echo(f"API Key: {'✅ Set' if provider_config.api_key else '❌ Not set'}")
|
|
295
|
+
|
|
296
|
+
# Available providers
|
|
297
|
+
available = ai_config.get_available_providers(settings)
|
|
298
|
+
typer.echo(f"\n✅ Available Providers ({len(available)}):")
|
|
299
|
+
for provider in available:
|
|
300
|
+
typer.echo(f" • {provider.value}")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@config_app.command("validate")
|
|
304
|
+
def config_validate() -> None:
|
|
305
|
+
"""Validate AI service configuration."""
|
|
306
|
+
ai_config = get_ai_config(settings)
|
|
307
|
+
|
|
308
|
+
typer.echo("🔍 Validating AI Service Configuration...")
|
|
309
|
+
|
|
310
|
+
errors = ai_config.validate_configuration(settings)
|
|
311
|
+
|
|
312
|
+
if not errors:
|
|
313
|
+
typer.echo("✅ Configuration is valid!")
|
|
314
|
+
typer.echo(f" Provider: {ai_config.provider}")
|
|
315
|
+
typer.echo(f" Model: {ai_config.model}")
|
|
316
|
+
|
|
317
|
+
# Show provider capabilities
|
|
318
|
+
capabilities = get_provider_capabilities(ai_config.provider)
|
|
319
|
+
if capabilities.free_tier_available:
|
|
320
|
+
typer.echo(" 💰 Uses free tier")
|
|
321
|
+
else:
|
|
322
|
+
typer.echo(" 💳 Requires paid account")
|
|
323
|
+
|
|
324
|
+
else:
|
|
325
|
+
typer.echo("❌ Configuration has issues:")
|
|
326
|
+
for error in errors:
|
|
327
|
+
typer.echo(f" • {error}")
|
|
328
|
+
|
|
329
|
+
# Suggest free providers if API key issues
|
|
330
|
+
if any("API key" in error for error in errors):
|
|
331
|
+
free_providers = get_free_providers()
|
|
332
|
+
if free_providers:
|
|
333
|
+
providers_list = ', '.join(p.value for p in free_providers)
|
|
334
|
+
typer.echo(f"\n💡 Try these free providers: {providers_list}")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@providers_app.command("list")
|
|
338
|
+
def providers_list() -> None:
|
|
339
|
+
"""List all available AI providers."""
|
|
340
|
+
ai_config = get_ai_config(settings)
|
|
341
|
+
available = ai_config.get_available_providers(settings)
|
|
342
|
+
free_providers = get_free_providers()
|
|
343
|
+
|
|
344
|
+
table = Table(title="🤖 AI Providers", width=75)
|
|
345
|
+
table.add_column("Provider", style="cyan", width=9)
|
|
346
|
+
table.add_column("Status", style="green", width=26, no_wrap=True)
|
|
347
|
+
table.add_column("Free", style="yellow", width=4)
|
|
348
|
+
table.add_column("Features", style="blue", width=18)
|
|
349
|
+
|
|
350
|
+
for provider in AIProvider:
|
|
351
|
+
capabilities = get_provider_capabilities(provider)
|
|
352
|
+
is_available = provider in available
|
|
353
|
+
is_current = provider == ai_config.provider
|
|
354
|
+
|
|
355
|
+
if is_available:
|
|
356
|
+
status = "✅ Available"
|
|
357
|
+
else:
|
|
358
|
+
# Make status more informative about what's missing
|
|
359
|
+
if provider == AIProvider.PUBLIC:
|
|
360
|
+
status = "❌ Error" # Shouldn't happen for PUBLIC
|
|
361
|
+
else:
|
|
362
|
+
# Show abbreviated environment variable name
|
|
363
|
+
env_var = f"{provider.value.upper()}_API_KEY"
|
|
364
|
+
status = f"❌ Need {env_var}"
|
|
365
|
+
|
|
366
|
+
if is_current:
|
|
367
|
+
status += " (current)"
|
|
368
|
+
|
|
369
|
+
free_tier = "Yes" if provider in free_providers else "No"
|
|
370
|
+
|
|
371
|
+
features = []
|
|
372
|
+
if capabilities.supports_streaming:
|
|
373
|
+
features.append("Stream")
|
|
374
|
+
if capabilities.supports_function_calling:
|
|
375
|
+
features.append("Functions")
|
|
376
|
+
if capabilities.supports_vision:
|
|
377
|
+
features.append("Vision")
|
|
378
|
+
|
|
379
|
+
table.add_row(
|
|
380
|
+
provider.value,
|
|
381
|
+
status,
|
|
382
|
+
free_tier,
|
|
383
|
+
", ".join(features) if features else "Basic"
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
console.print(table)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
async def _single_message(
|
|
390
|
+
ai_service,
|
|
391
|
+
message: str,
|
|
392
|
+
conversation_id: str | None,
|
|
393
|
+
stream: bool,
|
|
394
|
+
) -> None:
|
|
395
|
+
"""Send a single message and get response (for scripting/CI)."""
|
|
396
|
+
|
|
397
|
+
# Disable streaming for PUBLIC provider (fake streaming causes duplicates)
|
|
398
|
+
use_streaming = stream
|
|
399
|
+
if ai_service.config.provider == AIProvider.PUBLIC:
|
|
400
|
+
use_streaming = False
|
|
401
|
+
|
|
402
|
+
if use_streaming:
|
|
403
|
+
await _stream_chat_response(ai_service, message, conversation_id, "cli-user")
|
|
404
|
+
else:
|
|
405
|
+
# Show thinking spinner for non-streaming responses
|
|
406
|
+
from rich.live import Live
|
|
407
|
+
from rich.spinner import Spinner
|
|
408
|
+
|
|
409
|
+
spinner = Spinner("dots", text="🤖 Thinking...", style="bright_blue")
|
|
410
|
+
spinner_live = Live(
|
|
411
|
+
spinner, console=console, refresh_per_second=20, transient=True
|
|
412
|
+
)
|
|
413
|
+
spinner_live.start()
|
|
414
|
+
|
|
415
|
+
try:
|
|
416
|
+
response = await ai_service.chat(
|
|
417
|
+
message=message,
|
|
418
|
+
conversation_id=conversation_id,
|
|
419
|
+
user_id="cli-user",
|
|
420
|
+
)
|
|
421
|
+
finally:
|
|
422
|
+
spinner_live.stop()
|
|
423
|
+
|
|
424
|
+
# Use new shared rendering functions
|
|
425
|
+
from app.cli.ai_rendering import (
|
|
426
|
+
render_ai_header,
|
|
427
|
+
render_markdown_response,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
render_ai_header(console, inline=True)
|
|
431
|
+
render_markdown_response(console, response.content)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
async def _interactive_chat_session(
|
|
435
|
+
ai_service,
|
|
436
|
+
conversation_id: str | None = None,
|
|
437
|
+
) -> None:
|
|
438
|
+
"""Start an interactive chat session with continuous conversation."""
|
|
439
|
+
|
|
440
|
+
# Show welcome banner
|
|
441
|
+
ai_config = get_ai_config(settings)
|
|
442
|
+
welcome_text = (
|
|
443
|
+
f"[bold cyan]🤖 AI Chat Session[/bold cyan]\n"
|
|
444
|
+
f"[dim]Provider: {ai_config.provider} | Model: {ai_config.model}[/dim]\n"
|
|
445
|
+
f"[dim]Type 'exit', 'quit', 'bye' or press Ctrl+C to end session[/dim]"
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
console.print(Panel(welcome_text, border_style="blue", expand=False))
|
|
449
|
+
console.print()
|
|
450
|
+
|
|
451
|
+
# Track conversation for context
|
|
452
|
+
current_conversation_id = conversation_id
|
|
453
|
+
|
|
454
|
+
while True:
|
|
455
|
+
try:
|
|
456
|
+
# Get user input with Rich prompt - handle keyboard interrupt here
|
|
457
|
+
try:
|
|
458
|
+
user_message = Prompt.ask(
|
|
459
|
+
"[bold green]You[/bold green]", console=console
|
|
460
|
+
)
|
|
461
|
+
except (KeyboardInterrupt, EOFError):
|
|
462
|
+
# Single Ctrl+C should exit immediately
|
|
463
|
+
console.print("\n[yellow]👋 Chat session ended[/yellow]")
|
|
464
|
+
break
|
|
465
|
+
|
|
466
|
+
# Check for exit commands
|
|
467
|
+
if user_message.lower().strip() in ["exit", "quit", "bye", "q"]:
|
|
468
|
+
console.print("[yellow]👋 Goodbye![/yellow]")
|
|
469
|
+
break
|
|
470
|
+
|
|
471
|
+
if not user_message.strip():
|
|
472
|
+
console.print("[dim]Please enter a message or 'exit' to quit.[/dim]")
|
|
473
|
+
continue
|
|
474
|
+
|
|
475
|
+
# Stream the response using our existing beautiful renderer
|
|
476
|
+
# Disable streaming for PUBLIC provider (fake streaming causes duplicates)
|
|
477
|
+
use_streaming = True
|
|
478
|
+
if ai_service.config.provider == AIProvider.PUBLIC:
|
|
479
|
+
use_streaming = False
|
|
480
|
+
|
|
481
|
+
try:
|
|
482
|
+
if use_streaming:
|
|
483
|
+
# Capture conversation_id from streaming response
|
|
484
|
+
# for memory continuity
|
|
485
|
+
returned_conversation_id = await _stream_chat_response(
|
|
486
|
+
ai_service,
|
|
487
|
+
user_message,
|
|
488
|
+
current_conversation_id,
|
|
489
|
+
"cli-user"
|
|
490
|
+
)
|
|
491
|
+
# Update conversation reference if streaming completed successfully
|
|
492
|
+
if returned_conversation_id:
|
|
493
|
+
current_conversation_id = returned_conversation_id
|
|
494
|
+
else:
|
|
495
|
+
# Show thinking spinner for non-streaming responses
|
|
496
|
+
from rich.live import Live
|
|
497
|
+
from rich.spinner import Spinner
|
|
498
|
+
|
|
499
|
+
spinner = Spinner(
|
|
500
|
+
"dots", text="🤖 Thinking...", style="bright_blue"
|
|
501
|
+
)
|
|
502
|
+
spinner_live = Live(
|
|
503
|
+
spinner,
|
|
504
|
+
console=console,
|
|
505
|
+
refresh_per_second=20,
|
|
506
|
+
transient=True
|
|
507
|
+
)
|
|
508
|
+
spinner_live.start()
|
|
509
|
+
|
|
510
|
+
try:
|
|
511
|
+
response = await ai_service.chat(
|
|
512
|
+
message=user_message,
|
|
513
|
+
conversation_id=current_conversation_id,
|
|
514
|
+
user_id="cli-user",
|
|
515
|
+
)
|
|
516
|
+
finally:
|
|
517
|
+
spinner_live.stop()
|
|
518
|
+
|
|
519
|
+
# Use new shared rendering functions
|
|
520
|
+
from app.cli.ai_rendering import (
|
|
521
|
+
render_ai_header,
|
|
522
|
+
render_markdown_response,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
render_ai_header(console, inline=True)
|
|
526
|
+
render_markdown_response(console, response.content)
|
|
527
|
+
|
|
528
|
+
# Update conversation reference
|
|
529
|
+
current_conversation_id = response.metadata.get(
|
|
530
|
+
"conversation_id", current_conversation_id
|
|
531
|
+
)
|
|
532
|
+
except Exception as stream_error:
|
|
533
|
+
console.print(f"[red]❌ Streaming failed: {stream_error}[/red]")
|
|
534
|
+
console.print(
|
|
535
|
+
"[dim]This might be a provider issue. "
|
|
536
|
+
"Try a different provider or check your connection.[/dim]"
|
|
537
|
+
)
|
|
538
|
+
console.print("[dim]Available providers: openai, groq, public[/dim]")
|
|
539
|
+
console.print(
|
|
540
|
+
"[dim]Set AI_PROVIDER=openai or AI_PROVIDER=groq "
|
|
541
|
+
"to try alternatives.[/dim]"
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
# For subsequent messages, we want to continue the conversation
|
|
545
|
+
# The conversation_id will be maintained by the AI service
|
|
546
|
+
console.print() # Add space after response
|
|
547
|
+
|
|
548
|
+
except Exception as e:
|
|
549
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
|
550
|
+
console.print(
|
|
551
|
+
"[dim]You can continue chatting or type 'exit' to quit.[/dim]"
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
async def _stream_chat_response(
|
|
556
|
+
ai_service,
|
|
557
|
+
message: str,
|
|
558
|
+
conversation_id: str | None,
|
|
559
|
+
user_id: str,
|
|
560
|
+
verbose: bool = False,
|
|
561
|
+
) -> str | None:
|
|
562
|
+
"""
|
|
563
|
+
Stream chat response with real-time markdown rendering using Rich components.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
ai_service: The AI service instance
|
|
567
|
+
message: User message
|
|
568
|
+
conversation_id: Optional conversation ID
|
|
569
|
+
user_id: User identifier
|
|
570
|
+
verbose: Whether to show detailed metadata
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
str | None: The conversation ID for continuing the conversation,
|
|
574
|
+
None if interrupted
|
|
575
|
+
"""
|
|
576
|
+
import signal
|
|
577
|
+
|
|
578
|
+
from rich.live import Live
|
|
579
|
+
|
|
580
|
+
from app.cli.ai_rendering import StreamingMarkdownRenderer
|
|
581
|
+
|
|
582
|
+
renderer = StreamingMarkdownRenderer(console)
|
|
583
|
+
conversation_info = None
|
|
584
|
+
response_time = None
|
|
585
|
+
|
|
586
|
+
# Set up signal handler for graceful interruption
|
|
587
|
+
interrupted = False
|
|
588
|
+
|
|
589
|
+
def signal_handler(signum, frame):
|
|
590
|
+
nonlocal interrupted
|
|
591
|
+
interrupted = True
|
|
592
|
+
|
|
593
|
+
old_handler = signal.signal(signal.SIGINT, signal_handler)
|
|
594
|
+
|
|
595
|
+
try:
|
|
596
|
+
# Track if we've shown the header yet
|
|
597
|
+
header_shown = False
|
|
598
|
+
|
|
599
|
+
# Stream without Live display to avoid accumulation duplication
|
|
600
|
+
# Temporarily removed suppress_logs to debug hanging issue
|
|
601
|
+
# Add timeout to prevent hanging
|
|
602
|
+
import asyncio
|
|
603
|
+
|
|
604
|
+
from rich.live import Live
|
|
605
|
+
from rich.spinner import Spinner
|
|
606
|
+
|
|
607
|
+
# Show thinking spinner initially
|
|
608
|
+
spinner = Spinner("dots", text="🤖 Thinking...", style="bright_blue")
|
|
609
|
+
spinner_live = Live(
|
|
610
|
+
spinner, console=console, refresh_per_second=20, transient=True
|
|
611
|
+
)
|
|
612
|
+
spinner_live.start()
|
|
613
|
+
|
|
614
|
+
try:
|
|
615
|
+
# Track processed content to prevent duplicates (fake streaming providers)
|
|
616
|
+
processed_content = set()
|
|
617
|
+
|
|
618
|
+
async with asyncio.timeout(30.0): # 30 second timeout
|
|
619
|
+
async for chunk in ai_service.stream_chat(
|
|
620
|
+
message=message,
|
|
621
|
+
conversation_id=conversation_id,
|
|
622
|
+
user_id=user_id,
|
|
623
|
+
stream_delta=True,
|
|
624
|
+
):
|
|
625
|
+
if interrupted:
|
|
626
|
+
spinner_live.stop()
|
|
627
|
+
console.print(
|
|
628
|
+
"\n⚠️ Streaming interrupted by user", style="yellow"
|
|
629
|
+
)
|
|
630
|
+
break
|
|
631
|
+
|
|
632
|
+
# Skip duplicate content (handles fake streaming providers)
|
|
633
|
+
if chunk.content in processed_content:
|
|
634
|
+
# Still store final chunk metadata
|
|
635
|
+
# if this is the final duplicate
|
|
636
|
+
if chunk.is_final:
|
|
637
|
+
conversation_info = chunk.conversation_id
|
|
638
|
+
response_time = chunk.metadata.get("response_time_ms")
|
|
639
|
+
continue
|
|
640
|
+
|
|
641
|
+
processed_content.add(chunk.content)
|
|
642
|
+
|
|
643
|
+
# Process only the new delta content with Rich components
|
|
644
|
+
if chunk.is_delta and chunk.content:
|
|
645
|
+
# Show compact inline header only when first content arrives
|
|
646
|
+
if not header_shown:
|
|
647
|
+
spinner_live.stop() # Stop spinner when content starts
|
|
648
|
+
console.print("🤖: ", style="bright_blue", end="")
|
|
649
|
+
header_shown = True
|
|
650
|
+
|
|
651
|
+
# Process delta with new streaming renderer
|
|
652
|
+
renderer.add_delta(chunk.content)
|
|
653
|
+
|
|
654
|
+
# Store final chunk metadata
|
|
655
|
+
if chunk.is_final:
|
|
656
|
+
conversation_info = chunk.conversation_id
|
|
657
|
+
response_time = chunk.metadata.get("response_time_ms")
|
|
658
|
+
break
|
|
659
|
+
except TimeoutError:
|
|
660
|
+
spinner_live.stop()
|
|
661
|
+
console.print("\n❌ Request timed out after 30 seconds", style="red")
|
|
662
|
+
return None
|
|
663
|
+
finally:
|
|
664
|
+
# Ensure spinner is stopped in all cases
|
|
665
|
+
if spinner_live.is_started:
|
|
666
|
+
spinner_live.stop()
|
|
667
|
+
|
|
668
|
+
# Finalize any remaining content and add spacing
|
|
669
|
+
if not interrupted:
|
|
670
|
+
renderer.finalize() # Process any remaining buffered content
|
|
671
|
+
console.print("\n")
|
|
672
|
+
|
|
673
|
+
if verbose and conversation_info:
|
|
674
|
+
# Get conversation details
|
|
675
|
+
conversation = ai_service.get_conversation(conversation_info)
|
|
676
|
+
if conversation:
|
|
677
|
+
console.print(f"💬 Conversation: {conversation.id}", style="dim")
|
|
678
|
+
console.print(
|
|
679
|
+
f"ℹ️ Messages: {conversation.get_message_count()}", style="dim"
|
|
680
|
+
)
|
|
681
|
+
if response_time:
|
|
682
|
+
console.print(
|
|
683
|
+
f"⏱️ Response time: {response_time:.1f}ms", style="dim"
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
except Exception as e:
|
|
687
|
+
if not interrupted:
|
|
688
|
+
console.print(f"❌ Streaming error: {e}", style="red")
|
|
689
|
+
raise
|
|
690
|
+
|
|
691
|
+
finally:
|
|
692
|
+
# Restore original signal handler
|
|
693
|
+
signal.signal(signal.SIGINT, old_handler)
|
|
694
|
+
|
|
695
|
+
# Return conversation_id for maintaining conversation context
|
|
696
|
+
return conversation_info if not interrupted else None
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
if __name__ == "__main__":
|
|
700
|
+
app()
|