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