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 @@
1
+ """Tests for AI service components."""
@@ -0,0 +1,78 @@
1
+ """Shared fixtures for AI service tests."""
2
+
3
+ from unittest.mock import MagicMock
4
+
5
+ import pytest
6
+ from app.services.ai.models import AIProvider, Conversation, MessageRole
7
+ from app.services.ai.service import AIService
8
+
9
+
10
+ @pytest.fixture
11
+ def mock_ai_settings():
12
+ """Create mock settings for AI service testing."""
13
+ settings = MagicMock()
14
+ settings.AI_ENABLED = True
15
+ settings.AI_PROVIDER = "public"
16
+ settings.AI_MODEL = "gpt-3.5-turbo"
17
+ settings.AI_TEMPERATURE = 0.7
18
+ settings.AI_MAX_TOKENS = 1000
19
+ settings.AI_TIMEOUT_SECONDS = 30.0
20
+
21
+ # Provider API keys (None for PUBLIC)
22
+ settings.OPENAI_API_KEY = None
23
+ settings.ANTHROPIC_API_KEY = None
24
+ settings.GOOGLE_API_KEY = None
25
+ settings.GROQ_API_KEY = None
26
+ settings.MISTRAL_API_KEY = None
27
+ settings.COHERE_API_KEY = None
28
+
29
+ return settings
30
+
31
+
32
+ @pytest.fixture
33
+ def ai_service(mock_ai_settings):
34
+ """Create AI service instance for testing."""
35
+ return AIService(mock_ai_settings)
36
+
37
+
38
+ @pytest.fixture
39
+ def sample_conversation():
40
+ """Create a sample conversation for testing."""
41
+ return Conversation(
42
+ id="test-conversation-123",
43
+ provider=AIProvider.PUBLIC,
44
+ model="gpt-3.5-turbo",
45
+ title="Test Conversation",
46
+ )
47
+
48
+
49
+ @pytest.fixture
50
+ def conversation_with_messages(sample_conversation):
51
+ """Create a conversation with sample messages."""
52
+ sample_conversation.add_message(MessageRole.USER, "Hello, how are you?")
53
+ sample_conversation.add_message(MessageRole.ASSISTANT, "I'm doing well, thank you!")
54
+ sample_conversation.add_message(MessageRole.USER, "What's the weather like?")
55
+ return sample_conversation
56
+
57
+
58
+ @pytest.fixture
59
+ def free_provider_settings(mock_ai_settings):
60
+ """Create settings with a free provider."""
61
+ mock_ai_settings.AI_PROVIDER = "public"
62
+ return mock_ai_settings
63
+
64
+
65
+ @pytest.fixture
66
+ def paid_provider_settings(mock_ai_settings):
67
+ """Create settings with a paid provider (no API key)."""
68
+ mock_ai_settings.AI_PROVIDER = "openai"
69
+ mock_ai_settings.OPENAI_API_KEY = None # Missing API key
70
+ return mock_ai_settings
71
+
72
+
73
+ @pytest.fixture
74
+ def paid_provider_with_key_settings(mock_ai_settings):
75
+ """Create settings with a paid provider and API key."""
76
+ mock_ai_settings.AI_PROVIDER = "openai"
77
+ mock_ai_settings.OPENAI_API_KEY = "sk-test-key-123"
78
+ return mock_ai_settings
@@ -0,0 +1,157 @@
1
+ """Tests for AI service health checks."""
2
+
3
+ import pytest
4
+ from app.core.config import settings
5
+ from app.services.ai.health import check_ai_service_health
6
+ from app.services.ai.models import AIProvider
7
+ from app.services.system.models import ComponentStatusType
8
+
9
+
10
+ class TestAIHealthCheck:
11
+ """Test AI service health check functionality."""
12
+
13
+ @pytest.mark.asyncio
14
+ async def test_health_check_returns_component_status(self) -> None:
15
+ """Test that health check returns valid ComponentStatus."""
16
+ status = await check_ai_service_health()
17
+
18
+ assert status.name == "ai"
19
+ assert isinstance(status.status, ComponentStatusType)
20
+ assert isinstance(status.message, str)
21
+ assert isinstance(status.metadata, dict)
22
+
23
+ @pytest.mark.asyncio
24
+ async def test_health_check_metadata_structure(self) -> None:
25
+ """Test that health check includes expected metadata fields."""
26
+ status = await check_ai_service_health()
27
+
28
+ # Core metadata fields
29
+ assert "service_type" in status.metadata
30
+ assert status.metadata["service_type"] == "ai"
31
+ assert "engine" in status.metadata
32
+ assert status.metadata["engine"] == "pydantic-ai"
33
+ assert "enabled" in status.metadata
34
+ assert "provider" in status.metadata
35
+ assert "model" in status.metadata
36
+
37
+ @pytest.mark.asyncio
38
+ async def test_health_check_provider_metadata(self) -> None:
39
+ """Test provider-specific metadata in health check.
40
+
41
+ This test would have caught the bug where provider
42
+ was returned as enum when it should be string.
43
+ """
44
+ status = await check_ai_service_health()
45
+
46
+ # Provider should be a string (due to use_enum_values=True in config)
47
+ provider = status.metadata.get("provider")
48
+ assert isinstance(provider, str)
49
+ assert provider in [p.value for p in AIProvider]
50
+
51
+ @pytest.mark.asyncio
52
+ async def test_health_check_free_tier_metadata(self) -> None:
53
+ """Test that free tier status is correctly reported.
54
+
55
+ This test would have caught the bug where provider enum
56
+ was compared to list of strings.
57
+ """
58
+ status = await check_ai_service_health()
59
+
60
+ # Should have free tier info when service is enabled
61
+ if status.metadata.get("enabled"):
62
+ assert "provider_free_tier" in status.metadata
63
+ assert isinstance(status.metadata["provider_free_tier"], bool)
64
+
65
+ # PUBLIC provider should be free
66
+ if status.metadata["provider"] == "public":
67
+ assert status.metadata["provider_free_tier"] is True
68
+
69
+ @pytest.mark.asyncio
70
+ async def test_health_check_streaming_capability(self) -> None:
71
+ """Test that streaming capability is correctly reported."""
72
+ status = await check_ai_service_health()
73
+
74
+ if status.metadata.get("enabled"):
75
+ assert "provider_supports_streaming" in status.metadata
76
+ assert isinstance(status.metadata["provider_supports_streaming"], bool)
77
+
78
+ @pytest.mark.asyncio
79
+ async def test_health_check_when_enabled(self) -> None:
80
+ """Test health check when AI service is enabled."""
81
+ # Assuming default config has AI enabled
82
+ if settings.AI_ENABLED:
83
+ status = await check_ai_service_health()
84
+
85
+ assert status.metadata["enabled"] is True
86
+ assert status.status in [
87
+ ComponentStatusType.HEALTHY,
88
+ ComponentStatusType.UNHEALTHY,
89
+ ]
90
+
91
+ @pytest.mark.asyncio
92
+ async def test_health_check_dependencies_metadata(self) -> None:
93
+ """Test that dependencies are listed in metadata."""
94
+ status = await check_ai_service_health()
95
+
96
+ assert "dependencies" in status.metadata
97
+ dependencies = status.metadata["dependencies"]
98
+
99
+ assert "backend" in dependencies
100
+ assert "pydantic_ai" in dependencies
101
+
102
+ @pytest.mark.asyncio
103
+ async def test_health_check_validation_errors(self) -> None:
104
+ """Test that validation errors are counted in metadata."""
105
+ status = await check_ai_service_health()
106
+
107
+ assert "validation_errors_count" in status.metadata
108
+ assert isinstance(status.metadata["validation_errors_count"], int)
109
+ assert status.metadata["validation_errors_count"] >= 0
110
+
111
+ @pytest.mark.asyncio
112
+ async def test_health_check_handles_errors_gracefully(self) -> None:
113
+ """Test that health check doesn't crash on errors."""
114
+ # This should always succeed, even with invalid config
115
+ status = await check_ai_service_health()
116
+
117
+ assert status is not None
118
+ assert status.name == "ai"
119
+ # Either healthy or unhealthy, but shouldn't crash
120
+ assert status.status in [
121
+ ComponentStatusType.HEALTHY,
122
+ ComponentStatusType.UNHEALTHY,
123
+ ]
124
+
125
+
126
+ class TestAIHealthMetadataAccuracy:
127
+ """Test accuracy of AI health metadata values."""
128
+
129
+ @pytest.mark.asyncio
130
+ async def test_public_provider_metadata_accuracy(self) -> None:
131
+ """Test that PUBLIC provider metadata is accurate."""
132
+ # Assuming default config uses PUBLIC provider
133
+ status = await check_ai_service_health()
134
+
135
+ if status.metadata.get("provider") == "public":
136
+ # PUBLIC should be free
137
+ assert status.metadata.get("provider_free_tier") is True
138
+ # PUBLIC doesn't support streaming
139
+ assert status.metadata.get("provider_supports_streaming") is False
140
+
141
+ @pytest.mark.asyncio
142
+ async def test_agent_ready_metadata(self) -> None:
143
+ """Test that agent readiness is reported."""
144
+ status = await check_ai_service_health()
145
+
146
+ assert "agent_ready" in status.metadata
147
+ # Agents are created per-request, should always be ready when enabled
148
+ if status.metadata.get("enabled"):
149
+ assert status.metadata["agent_ready"] is True
150
+
151
+ @pytest.mark.asyncio
152
+ async def test_configuration_valid_metadata(self) -> None:
153
+ """Test that configuration validity is reported."""
154
+ status = await check_ai_service_health()
155
+
156
+ assert "configuration_valid" in status.metadata
157
+ assert isinstance(status.metadata["configuration_valid"], bool)
@@ -0,0 +1,164 @@
1
+ """Tests for AI service models and provider functions."""
2
+
3
+ from app.services.ai.models import (
4
+ AIProvider,
5
+ ProviderCapabilities,
6
+ get_free_providers,
7
+ get_provider_capabilities,
8
+ )
9
+
10
+
11
+ class TestProviderFunctions:
12
+ """Test provider utility functions."""
13
+
14
+ def test_get_free_providers_returns_enum_list(self) -> None:
15
+ """Ensure get_free_providers returns list of AIProvider enums, not strings."""
16
+ providers = get_free_providers()
17
+
18
+ assert isinstance(providers, list)
19
+ assert len(providers) > 0
20
+ # CRITICAL: Must return AIProvider enums, not strings
21
+ assert all(isinstance(p, AIProvider) for p in providers)
22
+
23
+ def test_free_providers_enum_membership(self) -> None:
24
+ """Test that AIProvider enum membership checks work correctly.
25
+
26
+ This test would have caught the bug where get_free_providers
27
+ returned list[str] instead of list[AIProvider].
28
+ """
29
+ free_providers = get_free_providers()
30
+
31
+ # These comparisons must work (enum in list[enum])
32
+ assert AIProvider.PUBLIC in free_providers
33
+ assert AIProvider.GROQ in free_providers
34
+ assert AIProvider.GOOGLE in free_providers
35
+ assert AIProvider.COHERE in free_providers
36
+
37
+ # These should NOT be free (paid only)
38
+ assert AIProvider.OPENAI not in free_providers
39
+ assert AIProvider.ANTHROPIC not in free_providers
40
+ assert AIProvider.MISTRAL not in free_providers
41
+
42
+ def test_free_providers_string_join(self) -> None:
43
+ """Test that free providers can be joined as strings for display."""
44
+ free_providers = get_free_providers()
45
+
46
+ # This is what the CLI does - must work with .value
47
+ providers_list = ", ".join(p.value for p in free_providers)
48
+
49
+ assert isinstance(providers_list, str)
50
+ assert "public" in providers_list
51
+ assert "groq" in providers_list
52
+
53
+ def test_get_provider_capabilities_public(self) -> None:
54
+ """Test capabilities for PUBLIC provider."""
55
+ caps = get_provider_capabilities(AIProvider.PUBLIC)
56
+
57
+ assert isinstance(caps, ProviderCapabilities)
58
+ assert caps.provider == AIProvider.PUBLIC
59
+ assert caps.free_tier_available is True
60
+ assert caps.supports_streaming is False
61
+ assert caps.supports_function_calling is False
62
+ assert caps.supports_vision is False
63
+
64
+ def test_get_provider_capabilities_openai(self) -> None:
65
+ """Test capabilities for OpenAI provider."""
66
+ caps = get_provider_capabilities(AIProvider.OPENAI)
67
+
68
+ assert caps.provider == AIProvider.OPENAI
69
+ assert caps.free_tier_available is False
70
+ assert caps.supports_streaming is True
71
+ assert caps.supports_function_calling is True
72
+ assert caps.supports_vision is True
73
+
74
+ def test_get_provider_capabilities_groq(self) -> None:
75
+ """Test capabilities for Groq provider (free tier)."""
76
+ caps = get_provider_capabilities(AIProvider.GROQ)
77
+
78
+ assert caps.provider == AIProvider.GROQ
79
+ assert caps.free_tier_available is True
80
+ assert caps.supports_streaming is True
81
+
82
+ def test_all_providers_have_capabilities(self) -> None:
83
+ """Ensure all providers have defined capabilities."""
84
+ for provider in AIProvider:
85
+ caps = get_provider_capabilities(provider)
86
+ assert isinstance(caps, ProviderCapabilities)
87
+ assert caps.provider == provider
88
+
89
+ def test_free_tier_consistency(self) -> None:
90
+ """Ensure free_tier_available matches get_free_providers result."""
91
+ free_providers = get_free_providers()
92
+
93
+ for provider in AIProvider:
94
+ caps = get_provider_capabilities(provider)
95
+
96
+ if provider in free_providers:
97
+ assert caps.free_tier_available is True, (
98
+ f"{provider} should have free_tier_available=True"
99
+ )
100
+ else:
101
+ assert caps.free_tier_available is False, (
102
+ f"{provider} should have free_tier_available=False"
103
+ )
104
+
105
+
106
+ class TestAIProviderEnum:
107
+ """Test AIProvider enum behavior."""
108
+
109
+ def test_provider_enum_values(self) -> None:
110
+ """Test that provider enums have correct string values."""
111
+ assert AIProvider.OPENAI.value == "openai"
112
+ assert AIProvider.ANTHROPIC.value == "anthropic"
113
+ assert AIProvider.GOOGLE.value == "google"
114
+ assert AIProvider.GROQ.value == "groq"
115
+ assert AIProvider.MISTRAL.value == "mistral"
116
+ assert AIProvider.COHERE.value == "cohere"
117
+ assert AIProvider.PUBLIC.value == "public"
118
+
119
+ def test_provider_enum_from_string(self) -> None:
120
+ """Test creating provider from string value."""
121
+ assert AIProvider("openai") == AIProvider.OPENAI
122
+ assert AIProvider("public") == AIProvider.PUBLIC
123
+
124
+ def test_provider_enum_comparison(self) -> None:
125
+ """Test that provider enum comparisons work correctly."""
126
+ provider = AIProvider.PUBLIC
127
+
128
+ # Enum equality
129
+ assert provider == AIProvider.PUBLIC
130
+ assert provider != AIProvider.OPENAI
131
+
132
+ # String comparison should work (due to str inheritance)
133
+ assert provider == "public"
134
+ assert provider != "openai"
135
+
136
+
137
+ class TestProviderCapabilitiesModel:
138
+ """Test ProviderCapabilities Pydantic model."""
139
+
140
+ def test_provider_capabilities_creation(self) -> None:
141
+ """Test creating ProviderCapabilities instance."""
142
+ caps = ProviderCapabilities(
143
+ provider=AIProvider.OPENAI,
144
+ supports_streaming=True,
145
+ supports_function_calling=True,
146
+ supports_vision=True,
147
+ free_tier_available=False,
148
+ )
149
+
150
+ assert caps.provider == AIProvider.OPENAI
151
+ assert caps.supports_streaming is True
152
+ assert caps.supports_function_calling is True
153
+ assert caps.supports_vision is True
154
+ assert caps.free_tier_available is False
155
+
156
+ def test_provider_capabilities_defaults(self) -> None:
157
+ """Test default values for ProviderCapabilities."""
158
+ caps = ProviderCapabilities(provider=AIProvider.PUBLIC)
159
+
160
+ # Defaults from model definition
161
+ assert caps.supports_streaming is True
162
+ assert caps.supports_function_calling is False
163
+ assert caps.supports_vision is False
164
+ assert caps.free_tier_available is False
@@ -0,0 +1,198 @@
1
+ """Tests for AI service core functionality."""
2
+
3
+ import pytest
4
+ from app.core.config import settings
5
+ from app.services.ai.models import AIProvider
6
+ from app.services.ai.service import AIService
7
+
8
+
9
+ class TestAIServiceInitialization:
10
+ """Test AI service initialization."""
11
+
12
+ def test_service_initialization(self) -> None:
13
+ """Test that AIService initializes correctly."""
14
+ service = AIService(settings)
15
+
16
+ assert service.settings == settings
17
+ assert service.config is not None
18
+ assert service.conversation_manager is not None
19
+
20
+ def test_service_config_loaded(self) -> None:
21
+ """Test that configuration is loaded from settings."""
22
+ service = AIService(settings)
23
+
24
+ assert hasattr(service.config, "enabled")
25
+ assert hasattr(service.config, "provider")
26
+ assert hasattr(service.config, "model")
27
+
28
+
29
+ class TestAIServiceStatus:
30
+ """Test AI service status reporting."""
31
+
32
+ def test_get_service_status_structure(self) -> None:
33
+ """Test that service status has expected structure."""
34
+ service = AIService(settings)
35
+ status = service.get_service_status()
36
+
37
+ assert isinstance(status, dict)
38
+ assert "enabled" in status
39
+ assert "provider" in status
40
+ assert "model" in status
41
+ assert "agent_initialized" in status
42
+ assert "total_conversations" in status
43
+ assert "configuration_valid" in status
44
+
45
+ def test_get_service_status_provider_type(self) -> None:
46
+ """Test that provider is returned as string, not enum.
47
+
48
+ This test would have caught the bug where code tried to call
49
+ .value on provider when it was already a string due to
50
+ use_enum_values=True in AIServiceConfig.
51
+ """
52
+ service = AIService(settings)
53
+ status = service.get_service_status()
54
+
55
+ provider = status["provider"]
56
+
57
+ # Provider should be string (Pydantic converts enum to string)
58
+ assert isinstance(provider, str)
59
+ # Should be a valid provider value
60
+ assert provider in [p.value for p in AIProvider]
61
+
62
+ def test_get_service_status_enabled_bool(self) -> None:
63
+ """Test that enabled is a boolean."""
64
+ service = AIService(settings)
65
+ status = service.get_service_status()
66
+
67
+ assert isinstance(status["enabled"], bool)
68
+
69
+ def test_get_service_status_agent_initialized(self) -> None:
70
+ """Test that agent_initialized is always True."""
71
+ service = AIService(settings)
72
+ status = service.get_service_status()
73
+
74
+ # Agents created per request, always available
75
+ assert status["agent_initialized"] is True
76
+
77
+ def test_get_service_status_configuration_valid(self) -> None:
78
+ """Test that configuration_valid reflects validation state."""
79
+ service = AIService(settings)
80
+ status = service.get_service_status()
81
+
82
+ assert isinstance(status["configuration_valid"], bool)
83
+
84
+ def test_get_service_status_conversation_count(self) -> None:
85
+ """Test that total_conversations starts at 0."""
86
+ service = AIService(settings)
87
+ status = service.get_service_status()
88
+
89
+ # New service should have no conversations
90
+ assert status["total_conversations"] == 0
91
+
92
+
93
+ class TestAIServiceValidation:
94
+ """Test AI service validation."""
95
+
96
+ def test_validate_service_returns_list(self) -> None:
97
+ """Test that validate_service returns list of errors."""
98
+ service = AIService(settings)
99
+ errors = service.validate_service()
100
+
101
+ assert isinstance(errors, list)
102
+ assert all(isinstance(e, str) for e in errors)
103
+
104
+ def test_validate_service_with_valid_config(self) -> None:
105
+ """Test validation with valid configuration."""
106
+ service = AIService(settings)
107
+ errors = service.validate_service()
108
+
109
+ # PUBLIC provider should have no errors (no API key required)
110
+ if service.config.provider == AIProvider.PUBLIC:
111
+ assert len(errors) == 0
112
+
113
+
114
+ class TestAIServiceConversationManagement:
115
+ """Test conversation management methods."""
116
+
117
+ def test_get_conversation_not_found(self) -> None:
118
+ """Test getting non-existent conversation."""
119
+ service = AIService(settings)
120
+ conversation = service.get_conversation("nonexistent-id")
121
+
122
+ assert conversation is None
123
+
124
+ def test_list_conversations_empty(self) -> None:
125
+ """Test listing conversations when none exist."""
126
+ service = AIService(settings)
127
+ conversations = service.list_conversations("test-user")
128
+
129
+ assert isinstance(conversations, list)
130
+ assert len(conversations) == 0
131
+
132
+ def test_conversation_manager_integration(self) -> None:
133
+ """Test that conversation manager is properly integrated."""
134
+ service = AIService(settings)
135
+
136
+ # ConversationManager should be accessible
137
+ assert hasattr(service, "conversation_manager")
138
+ assert hasattr(service.conversation_manager, "conversations")
139
+ assert isinstance(service.conversation_manager.conversations, dict)
140
+
141
+
142
+ class TestAIServiceConfigIntegration:
143
+ """Test integration with configuration."""
144
+
145
+ def test_config_provider_enum_to_string_conversion(self) -> None:
146
+ """Test that Pydantic converts provider enum to string.
147
+
148
+ This documents the behavior that caused the .value bug:
149
+ AIServiceConfig has use_enum_values=True, which means
150
+ self.config.provider is already a string, not an enum.
151
+ """
152
+ service = AIService(settings)
153
+
154
+ # Config provider is a string due to use_enum_values=True
155
+ assert isinstance(service.config.provider, str)
156
+
157
+ # Should be able to compare with enum values
158
+ assert service.config.provider == AIProvider.PUBLIC.value
159
+
160
+ def test_service_uses_config_correctly(self) -> None:
161
+ """Test that service uses config values correctly."""
162
+ service = AIService(settings)
163
+
164
+ # Service should use config values
165
+ assert service.config.enabled is not None
166
+ assert service.config.model is not None
167
+ assert service.config.temperature is not None
168
+
169
+
170
+ class TestAIServiceErrorHandling:
171
+ """Test error handling in AI service."""
172
+
173
+ @pytest.mark.asyncio
174
+ async def test_chat_disabled_service_error(self, mock_ai_settings) -> None:
175
+ """Test that chat raises error when service is disabled."""
176
+ # Disable AI service
177
+ mock_ai_settings.AI_ENABLED = False
178
+
179
+ service = AIService(mock_ai_settings)
180
+
181
+ with pytest.raises(Exception) as exc_info:
182
+ await service.chat("test message")
183
+
184
+ assert "disabled" in str(exc_info.value).lower()
185
+
186
+ @pytest.mark.asyncio
187
+ async def test_stream_chat_disabled_service_error(self, mock_ai_settings) -> None:
188
+ """Test that stream_chat raises error when service is disabled."""
189
+ # Disable AI service
190
+ mock_ai_settings.AI_ENABLED = False
191
+
192
+ service = AIService(mock_ai_settings)
193
+
194
+ with pytest.raises(Exception) as exc_info:
195
+ async for _ in service.stream_chat("test message"):
196
+ pass
197
+
198
+ assert "disabled" in str(exc_info.value).lower()