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.
Files changed (392) hide show
  1. aegis/__init__.py +5 -0
  2. aegis/__main__.py +51 -0
  3. aegis/cli/__init__.py +6 -0
  4. aegis/cli/callbacks.py +114 -0
  5. aegis/cli/interactive.py +611 -0
  6. aegis/cli/utils.py +70 -0
  7. aegis/cli/validators.py +34 -0
  8. aegis/commands/__init__.py +6 -0
  9. aegis/commands/add.py +353 -0
  10. aegis/commands/add_service.py +332 -0
  11. aegis/commands/components.py +35 -0
  12. aegis/commands/init.py +370 -0
  13. aegis/commands/remove.py +227 -0
  14. aegis/commands/services.py +52 -0
  15. aegis/commands/update.py +252 -0
  16. aegis/commands/version.py +12 -0
  17. aegis/config/__init__.py +1 -0
  18. aegis/config/shared_files.py +136 -0
  19. aegis/core/CLAUDE.md +377 -0
  20. aegis/core/__init__.py +6 -0
  21. aegis/core/component_files.py +228 -0
  22. aegis/core/component_utils.py +220 -0
  23. aegis/core/components.py +127 -0
  24. aegis/core/copier_manager.py +315 -0
  25. aegis/core/copier_updater.py +475 -0
  26. aegis/core/dependency_resolver.py +119 -0
  27. aegis/core/manual_updater.py +554 -0
  28. aegis/core/post_gen_tasks.py +547 -0
  29. aegis/core/service_resolver.py +261 -0
  30. aegis/core/services.py +157 -0
  31. aegis/core/template_generator.py +266 -0
  32. aegis/core/version_compatibility.py +259 -0
  33. aegis/templates/CLAUDE.md +591 -0
  34. aegis/templates/cookiecutter-aegis-project/cookiecutter.json +39 -0
  35. aegis/templates/cookiecutter-aegis-project/hooks/post_gen_project.py +214 -0
  36. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.dockerignore +71 -0
  37. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.env.example.j2 +130 -0
  38. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.gitignore +131 -0
  39. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
  40. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Makefile +236 -0
  41. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/README.md.j2 +196 -0
  42. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/alembic.ini.j2 +111 -0
  43. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/env.py.j2 +91 -0
  44. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/script.py.mako +25 -0
  45. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/versions/001_initial_auth.py.j2 +51 -0
  46. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/__init__.py +5 -0
  47. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/__init__.py +6 -0
  48. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/ai.py.j2 +700 -0
  49. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/ai_rendering.py +361 -0
  50. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/auth.py.j2 +253 -0
  51. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/health.py.j2 +419 -0
  52. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/load_test.py.j2 +656 -0
  53. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/main.py.j2 +65 -0
  54. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/marko_terminal_renderer.py +489 -0
  55. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/tasks.py.j2 +328 -0
  56. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/{% if cookiecutter.include_scheduler == /"yes/" %}tasks.py{% endif %}" +340 -0
  57. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/__init__.py +0 -0
  58. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/__init__.py +0 -0
  59. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/ai/__init__.py +8 -0
  60. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/ai/router.py +329 -0
  61. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/auth/__init__.py +1 -0
  62. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/auth/router.py +64 -0
  63. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/deps.py +58 -0
  64. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/health.py +163 -0
  65. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/models.py.j2 +280 -0
  66. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/routing.py.j2 +32 -0
  67. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/scheduler.py.j2 +121 -0
  68. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/worker.py.j2 +478 -0
  69. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/hooks.py +144 -0
  70. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/main.py +31 -0
  71. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/__init__.py +1 -0
  72. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/cors.py +20 -0
  73. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/__init__.py +1 -0
  74. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/cleanup.py +14 -0
  75. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/__init__.py +1 -0
  76. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/component_health.py.j2 +418 -0
  77. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/database_init.py.j2 +83 -0
  78. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/__init__.py +5 -0
  79. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/__init__.py +27 -0
  80. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/table.py +78 -0
  81. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/text.py +142 -0
  82. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/__init__.py.j2 +47 -0
  83. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/ai_card.py +287 -0
  84. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/auth_card.py +198 -0
  85. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/base_card.py +256 -0
  86. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/card_factory.py +227 -0
  87. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/card_utils.py +333 -0
  88. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/database_card.py +420 -0
  89. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/fastapi_card.py +328 -0
  90. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/flet_card.py +267 -0
  91. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/redis_card.py +322 -0
  92. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/scheduler_card.py +352 -0
  93. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/services_card.py +233 -0
  94. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/worker_card.py +684 -0
  95. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/main.py.j2 +653 -0
  96. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/theme.py +48 -0
  97. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/__init__.py +1 -0
  98. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/main.py.j2 +156 -0
  99. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/CLAUDE.md.j2 +213 -0
  100. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/__init__.py +6 -0
  101. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/constants.py.j2 +30 -0
  102. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/pools.py +97 -0
  103. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/__init__.py +1 -0
  104. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/load_test.py +55 -0
  105. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/media.py +49 -0
  106. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/system.py +44 -0
  107. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/registry.py +139 -0
  108. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/__init__.py +120 -0
  109. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/load_tasks.py +507 -0
  110. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/simple_system_tasks.py +33 -0
  111. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/system_tasks.py +281 -0
  112. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/config.py.j2 +178 -0
  113. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/constants.py +58 -0
  114. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/db.py.j2 +176 -0
  115. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/log.py +92 -0
  116. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/security.py +62 -0
  117. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/__init__.py +1 -0
  118. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/webserver.py +40 -0
  119. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/{% if cookiecutter.include_scheduler == /"yes/" %}scheduler.py{% endif %}" +21 -0
  120. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/__init__.py +0 -0
  121. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/main.py +62 -0
  122. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/models/__init__.py +1 -0
  123. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/models/user.py +44 -0
  124. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/py.typed +0 -0
  125. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/__init__.py +1 -0
  126. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/__init__.py +8 -0
  127. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/config.py +130 -0
  128. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/conversation.py +213 -0
  129. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/health.py +96 -0
  130. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/models.py +229 -0
  131. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/providers.py +370 -0
  132. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/service.py +388 -0
  133. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/__init__.py +1 -0
  134. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/auth_service.py +41 -0
  135. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/health.py +164 -0
  136. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/user_service.py +83 -0
  137. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/middleware_inspector.py.j2 +223 -0
  138. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/models.py.j2 +70 -0
  139. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/route_inspector.py.j2 +155 -0
  140. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test.py +679 -0
  141. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test_models.py +266 -0
  142. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/__init__.py.j2 +21 -0
  143. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/models.py.j2 +119 -0
  144. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/scheduled_task_manager.py.j2 +273 -0
  145. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/task_monitor.py.j2 +189 -0
  146. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/__init__.py +15 -0
  147. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/models.py +26 -0
  148. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/__init__.py +52 -0
  149. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/alerts.py +94 -0
  150. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/backup.py.j2 +119 -0
  151. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/health.py.j2 +1333 -0
  152. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/models.py +243 -0
  153. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/ui.py +52 -0
  154. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto-dark.png +0 -0
  155. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto-square-backup.png +0 -0
  156. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto.png +0 -0
  157. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.dockerignore +71 -0
  158. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.env.example.j2 +64 -0
  159. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.gitignore +131 -0
  160. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/Dockerfile +53 -0
  161. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/Makefile +211 -0
  162. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/README.md.j2 +172 -0
  163. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/docker-compose.yml.j2 +78 -0
  164. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/mkdocs.yml.j2 +62 -0
  165. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/pyproject.toml.j2 +120 -0
  166. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/uv.lock +1673 -0
  167. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docker-compose.yml.j2 +200 -0
  168. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/api.md +191 -0
  169. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md +0 -0
  170. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md.j2 +621 -0
  171. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/development.md +215 -0
  172. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/health.md +240 -0
  173. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/javascripts/mermaid-config.js +62 -0
  174. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/stylesheets/mermaid.css +95 -0
  175. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/mkdocs.yml.j2 +62 -0
  176. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/pyproject.toml.j2 +131 -0
  177. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh +87 -0
  178. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh.j2 +93 -0
  179. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/gen_docs.py +16 -0
  180. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/__init__.py +1 -0
  181. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_auth_endpoints.py.j2 +307 -0
  182. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_health_endpoints.py.j2 +262 -0
  183. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_scheduler_endpoints.py.j2 +214 -0
  184. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_worker_endpoints.py.j2 +165 -0
  185. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/cli/test_ai_rendering.py +427 -0
  186. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/cli/test_conversation_memory.py +465 -0
  187. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/components/test_scheduler.py +43 -0
  188. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/conftest.py.j2 +195 -0
  189. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/__init__.py +1 -0
  190. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/__init__.py +1 -0
  191. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/conftest.py +78 -0
  192. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_health.py +157 -0
  193. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_models.py +164 -0
  194. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_service.py +198 -0
  195. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_auth_integration.py.j2 +528 -0
  196. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_component_integration.py.j2 +387 -0
  197. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_conversation_persistence.py +342 -0
  198. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_health_logic.py.j2 +663 -0
  199. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_models.py +619 -0
  200. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_service.py +603 -0
  201. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_middleware_inspector.py.j2 +248 -0
  202. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_scheduled_task_manager.py.j2 +292 -0
  203. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_system_service.py +98 -0
  204. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_worker_health_registration.py.j2 +257 -0
  205. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/test_core.py +49 -0
  206. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/uv.lock +1673 -0
  207. aegis/templates/copier-aegis-project/{{ project_slug }}/.copier-answers.yml.jinja +21 -0
  208. aegis/templates/copier-aegis-project/{{ project_slug }}/.dockerignore +71 -0
  209. aegis/templates/copier-aegis-project/{{ project_slug }}/.env.example.jinja +130 -0
  210. aegis/templates/copier-aegis-project/{{ project_slug }}/.gitignore +131 -0
  211. aegis/templates/copier-aegis-project/{{ project_slug }}/Dockerfile +53 -0
  212. aegis/templates/copier-aegis-project/{{ project_slug }}/Makefile.jinja +236 -0
  213. aegis/templates/copier-aegis-project/{{ project_slug }}/README.md.jinja +196 -0
  214. aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/alembic.ini.jinja +111 -0
  215. aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/env.py.jinja +91 -0
  216. aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/script.py.mako +25 -0
  217. aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/versions/001_initial_auth.py.jinja +51 -0
  218. aegis/templates/copier-aegis-project/{{ project_slug }}/app/__init__.py.jinja +5 -0
  219. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/__init__.py.jinja +6 -0
  220. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/ai.py.jinja +700 -0
  221. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/ai_rendering.py +360 -0
  222. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/auth.py.jinja +253 -0
  223. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/health.py.jinja +419 -0
  224. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/load_test.py.jinja +656 -0
  225. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/main.py.jinja +65 -0
  226. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/marko_terminal_renderer.py +489 -0
  227. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/tasks.py.jinja +328 -0
  228. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/__init__.py +0 -0
  229. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/__init__.py +0 -0
  230. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/ai/__init__.py +8 -0
  231. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/ai/router.py +329 -0
  232. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/auth/__init__.py +1 -0
  233. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/auth/router.py +64 -0
  234. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/deps.py +58 -0
  235. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/health.py.jinja +163 -0
  236. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/models.py.jinja +280 -0
  237. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/routing.py.jinja +32 -0
  238. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/scheduler.py.jinja +121 -0
  239. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/worker.py.jinja +478 -0
  240. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/hooks.py +144 -0
  241. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/main.py +31 -0
  242. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/middleware/__init__.py +1 -0
  243. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/middleware/cors.py +20 -0
  244. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/shutdown/__init__.py +1 -0
  245. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/shutdown/cleanup.py +14 -0
  246. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/__init__.py +1 -0
  247. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/component_health.py.jinja +418 -0
  248. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/database_init.py.jinja +83 -0
  249. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/__init__.py +5 -0
  250. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/__init__.py +27 -0
  251. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/table.py +78 -0
  252. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/text.py +142 -0
  253. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/__init__.py.jinja +47 -0
  254. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/ai_card.py +287 -0
  255. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/auth_card.py +198 -0
  256. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/base_card.py +256 -0
  257. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/card_factory.py +227 -0
  258. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/card_utils.py +333 -0
  259. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/database_card.py +420 -0
  260. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/fastapi_card.py +328 -0
  261. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/flet_card.py +267 -0
  262. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/redis_card.py +322 -0
  263. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/scheduler_card.py +352 -0
  264. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/services_card.py +233 -0
  265. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/worker_card.py +684 -0
  266. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/main.py.jinja +653 -0
  267. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/theme.py +48 -0
  268. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/scheduler/__init__.py +1 -0
  269. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/scheduler/main.py.jinja +156 -0
  270. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/CLAUDE.md.jinja +213 -0
  271. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/__init__.py +6 -0
  272. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/constants.py.jinja +30 -0
  273. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/pools.py +97 -0
  274. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/__init__.py +1 -0
  275. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/load_test.py +55 -0
  276. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/media.py +49 -0
  277. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/system.py +44 -0
  278. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/registry.py +139 -0
  279. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/__init__.py +120 -0
  280. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/load_tasks.py +507 -0
  281. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/simple_system_tasks.py +33 -0
  282. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/system_tasks.py +281 -0
  283. aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/config.py.jinja +178 -0
  284. aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/constants.py +58 -0
  285. aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/db.py.jinja +176 -0
  286. aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/log.py +92 -0
  287. aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/security.py +62 -0
  288. aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/__init__.py +1 -0
  289. aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/scheduler.py.jinja +21 -0
  290. aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/webserver.py +39 -0
  291. aegis/templates/copier-aegis-project/{{ project_slug }}/app/integrations/__init__.py +0 -0
  292. aegis/templates/copier-aegis-project/{{ project_slug }}/app/integrations/main.py +61 -0
  293. aegis/templates/copier-aegis-project/{{ project_slug }}/app/models/__init__.py +1 -0
  294. aegis/templates/copier-aegis-project/{{ project_slug }}/app/models/user.py +44 -0
  295. aegis/templates/copier-aegis-project/{{ project_slug }}/app/py.typed +0 -0
  296. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/__init__.py +1 -0
  297. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/__init__.py +8 -0
  298. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/config.py +130 -0
  299. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/conversation.py +213 -0
  300. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/health.py +96 -0
  301. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/models.py +229 -0
  302. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/providers.py.jinja +370 -0
  303. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/service.py +387 -0
  304. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/__init__.py +1 -0
  305. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/auth_service.py +40 -0
  306. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/health.py +162 -0
  307. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/user_service.py +82 -0
  308. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/middleware_inspector.py.jinja +223 -0
  309. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/models.py.jinja +70 -0
  310. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/route_inspector.py.jinja +155 -0
  311. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/load_test.py +678 -0
  312. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/load_test_models.py +265 -0
  313. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/__init__.py.jinja +21 -0
  314. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/models.py.jinja +119 -0
  315. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/scheduled_task_manager.py.jinja +273 -0
  316. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/task_monitor.py.jinja +189 -0
  317. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/shared/__init__.py +15 -0
  318. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/shared/models.py +26 -0
  319. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/__init__.py +52 -0
  320. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/alerts.py +94 -0
  321. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/backup.py.jinja +119 -0
  322. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/health.py.jinja +1333 -0
  323. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/models.py +243 -0
  324. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/ui.py +52 -0
  325. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57223!aegis-manifesto.png +0 -0
  326. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57224!aegis-manifesto-dark.png +0 -0
  327. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57225!aegis-manifesto-square-backup.png +0 -0
  328. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57533!aegis-manifesto.png +0 -0
  329. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57534!aegis-manifesto-dark.png +0 -0
  330. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57538!aegis-manifesto-square-backup.png +0 -0
  331. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57897!aegis-manifesto.png +0 -0
  332. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57898!aegis-manifesto-dark.png +0 -0
  333. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57904!aegis-manifesto-square-backup.png +0 -0
  334. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58315!aegis-manifesto.png +0 -0
  335. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58316!aegis-manifesto-dark.png +0 -0
  336. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58324!aegis-manifesto-square-backup.png +0 -0
  337. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58837!aegis-manifesto.png +0 -0
  338. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58838!aegis-manifesto-dark.png +0 -0
  339. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58849!aegis-manifesto-square-backup.png +0 -0
  340. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto-dark.png +0 -0
  341. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto-square-backup.png +0 -0
  342. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto.png +0 -0
  343. aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/.env.example.jinja +64 -0
  344. aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/README.md.jinja +172 -0
  345. aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/docker-compose.yml.jinja +78 -0
  346. aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/mkdocs.yml.jinja +62 -0
  347. aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/pyproject.toml.jinja +120 -0
  348. aegis/templates/copier-aegis-project/{{ project_slug }}/docker-compose.yml.jinja +200 -0
  349. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/api.md.jinja +191 -0
  350. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/components/scheduler.md +0 -0
  351. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/components/scheduler.md.jinja +621 -0
  352. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/development.md.jinja +215 -0
  353. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/health.md.jinja +240 -0
  354. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/javascripts/mermaid-config.js +62 -0
  355. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/stylesheets/mermaid.css +95 -0
  356. aegis/templates/copier-aegis-project/{{ project_slug }}/mkdocs.yml.jinja +62 -0
  357. aegis/templates/copier-aegis-project/{{ project_slug }}/pyproject.toml.jinja +131 -0
  358. aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/entrypoint.sh +87 -0
  359. aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/entrypoint.sh.jinja +93 -0
  360. aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/gen_docs.py +16 -0
  361. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/__init__.py +1 -0
  362. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_auth_endpoints.py.jinja +307 -0
  363. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_health_endpoints.py.jinja +262 -0
  364. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_scheduler_endpoints.py.jinja +214 -0
  365. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_worker_endpoints.py.jinja +165 -0
  366. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/cli/test_ai_rendering.py +427 -0
  367. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/cli/test_conversation_memory.py +465 -0
  368. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/components/test_scheduler.py +43 -0
  369. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/conftest.py.jinja +195 -0
  370. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/__init__.py +1 -0
  371. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/__init__.py +1 -0
  372. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/conftest.py +78 -0
  373. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_health.py +157 -0
  374. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_models.py +164 -0
  375. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_service.py +198 -0
  376. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_auth_integration.py.jinja +528 -0
  377. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_component_integration.py.jinja +387 -0
  378. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_conversation_persistence.py +342 -0
  379. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_health_logic.py.jinja +663 -0
  380. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_load_test_models.py +619 -0
  381. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_load_test_service.py +603 -0
  382. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_middleware_inspector.py.jinja +248 -0
  383. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_scheduled_task_manager.py.jinja +292 -0
  384. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_system_service.py +98 -0
  385. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_worker_health_registration.py.jinja +257 -0
  386. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/test_core.py +49 -0
  387. aegis/templates/copier-aegis-project/{{ project_slug }}/uv.lock +1673 -0
  388. aegis_stack-0.2.0rc2.dist-info/METADATA +165 -0
  389. aegis_stack-0.2.0rc2.dist-info/RECORD +392 -0
  390. aegis_stack-0.2.0rc2.dist-info/WHEEL +4 -0
  391. aegis_stack-0.2.0rc2.dist-info/entry_points.txt +3 -0
  392. 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()