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