fastapi-fullstack 0.1.7__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 (241) hide show
  1. fastapi_fullstack-0.1.7.dist-info/METADATA +739 -0
  2. fastapi_fullstack-0.1.7.dist-info/RECORD +241 -0
  3. fastapi_fullstack-0.1.7.dist-info/WHEEL +4 -0
  4. fastapi_fullstack-0.1.7.dist-info/entry_points.txt +2 -0
  5. fastapi_fullstack-0.1.7.dist-info/licenses/LICENSE +21 -0
  6. fastapi_gen/__init__.py +3 -0
  7. fastapi_gen/cli.py +442 -0
  8. fastapi_gen/config.py +356 -0
  9. fastapi_gen/generator.py +207 -0
  10. fastapi_gen/prompts.py +874 -0
  11. fastapi_gen/template/VARIABLES.md +276 -0
  12. fastapi_gen/template/cookiecutter.json +93 -0
  13. fastapi_gen/template/hooks/post_gen_project.py +355 -0
  14. fastapi_gen/template/{{cookiecutter.project_slug}}/.env.prod.example +56 -0
  15. fastapi_gen/template/{{cookiecutter.project_slug}}/.github/workflows/ci.yml +150 -0
  16. fastapi_gen/template/{{cookiecutter.project_slug}}/.gitignore +109 -0
  17. fastapi_gen/template/{{cookiecutter.project_slug}}/AGENTS.md +55 -0
  18. fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md +99 -0
  19. fastapi_gen/template/{{cookiecutter.project_slug}}/Makefile +315 -0
  20. fastapi_gen/template/{{cookiecutter.project_slug}}/README.md +768 -0
  21. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.dockerignore +60 -0
  22. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.env.example +155 -0
  23. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.pre-commit-config.yaml +32 -0
  24. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/Dockerfile +56 -0
  25. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py +76 -0
  26. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/script.py.mako +30 -0
  27. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/versions/.gitkeep +0 -0
  28. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic.ini +48 -0
  29. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/__init__.py +3 -0
  30. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/admin.py +447 -0
  31. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/__init__.py +23 -0
  32. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/assistant.py +226 -0
  33. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langchain_assistant.py +226 -0
  34. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/prompts.py +10 -0
  35. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/__init__.py +13 -0
  36. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/datetime_tool.py +17 -0
  37. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/__init__.py +1 -0
  38. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/deps.py +541 -0
  39. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/exception_handlers.py +98 -0
  40. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/router.py +10 -0
  41. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/__init__.py +9 -0
  42. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/__init__.py +87 -0
  43. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/agent.py +902 -0
  44. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/auth.py +395 -0
  45. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/conversations.py +498 -0
  46. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/health.py +227 -0
  47. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/items.py +275 -0
  48. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/oauth.py +205 -0
  49. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/sessions.py +168 -0
  50. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/users.py +333 -0
  51. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/webhooks.py +477 -0
  52. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/ws.py +46 -0
  53. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/versioning.py +221 -0
  54. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/clients/__init__.py +14 -0
  55. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/clients/redis.py +88 -0
  56. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/__init__.py +117 -0
  57. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/cleanup.py +75 -0
  58. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/example.py +28 -0
  59. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/seed.py +266 -0
  60. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/__init__.py +5 -0
  61. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/cache.py +23 -0
  62. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/config.py +267 -0
  63. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/csrf.py +153 -0
  64. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/exceptions.py +122 -0
  65. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/logfire_setup.py +101 -0
  66. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/middleware.py +99 -0
  67. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/oauth.py +23 -0
  68. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/rate_limit.py +58 -0
  69. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/sanitize.py +271 -0
  70. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/security.py +102 -0
  71. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/__init__.py +7 -0
  72. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/base.py +41 -0
  73. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/__init__.py +31 -0
  74. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/conversation.py +319 -0
  75. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/item.py +96 -0
  76. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/session.py +126 -0
  77. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/user.py +218 -0
  78. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/webhook.py +244 -0
  79. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/session.py +130 -0
  80. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/main.py +334 -0
  81. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/pipelines/__init__.py +9 -0
  82. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/pipelines/base.py +73 -0
  83. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/__init__.py +49 -0
  84. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/base.py +154 -0
  85. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/conversation.py +838 -0
  86. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/item.py +222 -0
  87. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/session.py +318 -0
  88. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/user.py +322 -0
  89. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/webhook.py +358 -0
  90. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/__init__.py +50 -0
  91. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/base.py +57 -0
  92. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/conversation.py +192 -0
  93. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/item.py +52 -0
  94. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/session.py +42 -0
  95. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/token.py +31 -0
  96. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/user.py +64 -0
  97. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/webhook.py +89 -0
  98. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/__init__.py +38 -0
  99. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/conversation.py +850 -0
  100. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/item.py +246 -0
  101. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/session.py +333 -0
  102. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/user.py +432 -0
  103. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py +561 -0
  104. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/__init__.py +5 -0
  105. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/celery_app.py +64 -0
  106. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/taskiq_app.py +38 -0
  107. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/__init__.py +25 -0
  108. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/examples.py +106 -0
  109. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/schedules.py +29 -0
  110. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/taskiq_examples.py +92 -0
  111. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/cli/__init__.py +1 -0
  112. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/cli/commands.py +438 -0
  113. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +180 -0
  114. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/scripts/.gitkeep +0 -0
  115. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/__init__.py +1 -0
  116. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/__init__.py +1 -0
  117. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_auth.py +242 -0
  118. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_exceptions.py +151 -0
  119. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_health.py +113 -0
  120. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_items.py +310 -0
  121. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_users.py +253 -0
  122. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/conftest.py +151 -0
  123. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_admin.py +890 -0
  124. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_agents.py +261 -0
  125. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_clients.py +183 -0
  126. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_commands.py +173 -0
  127. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_core.py +143 -0
  128. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_pipelines.py +118 -0
  129. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_repositories.py +181 -0
  130. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_security.py +124 -0
  131. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_services.py +363 -0
  132. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_worker.py +85 -0
  133. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml +242 -0
  134. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.frontend.yml +31 -0
  135. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml +435 -0
  136. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml +241 -0
  137. fastapi_gen/template/{{cookiecutter.project_slug}}/docs/adding_features.md +132 -0
  138. fastapi_gen/template/{{cookiecutter.project_slug}}/docs/architecture.md +63 -0
  139. fastapi_gen/template/{{cookiecutter.project_slug}}/docs/patterns.md +161 -0
  140. fastapi_gen/template/{{cookiecutter.project_slug}}/docs/testing.md +120 -0
  141. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.dockerignore +40 -0
  142. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.env.example +12 -0
  143. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.gitignore +45 -0
  144. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.prettierignore +19 -0
  145. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.prettierrc +11 -0
  146. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/Dockerfile +44 -0
  147. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/README.md +648 -0
  148. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/auth.setup.ts +49 -0
  149. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/auth.spec.ts +134 -0
  150. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/chat.spec.ts +207 -0
  151. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/home.spec.ts +73 -0
  152. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/instrumentation.ts +14 -0
  153. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/messages/en.json +84 -0
  154. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/messages/pl.json +84 -0
  155. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/next.config.ts +76 -0
  156. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/package.json +69 -0
  157. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/playwright.config.ts +101 -0
  158. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/postcss.config.mjs +7 -0
  159. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/layout.tsx +11 -0
  160. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/login/page.tsx +5 -0
  161. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/register/page.tsx +5 -0
  162. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/chat/page.tsx +48 -0
  163. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/dashboard/page.tsx +99 -0
  164. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/layout.tsx +17 -0
  165. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/profile/page.tsx +152 -0
  166. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/auth/callback/page.tsx +113 -0
  167. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/layout.tsx +46 -0
  168. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/page.tsx +73 -0
  169. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/login/route.ts +58 -0
  170. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/logout/route.ts +24 -0
  171. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/me/route.ts +39 -0
  172. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/oauth-callback/route.ts +50 -0
  173. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/refresh/route.ts +54 -0
  174. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/register/route.ts +26 -0
  175. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/[id]/messages/route.ts +41 -0
  176. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/[id]/route.ts +108 -0
  177. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/route.ts +73 -0
  178. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/health/route.ts +21 -0
  179. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/globals.css +323 -0
  180. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/layout.tsx +22 -0
  181. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/providers.tsx +29 -0
  182. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/index.ts +2 -0
  183. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/login-form.tsx +120 -0
  184. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/register-form.tsx +153 -0
  185. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-container.tsx +234 -0
  186. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-input.tsx +72 -0
  187. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/conversation-sidebar.tsx +328 -0
  188. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/copy-button.tsx +46 -0
  189. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/index.ts +11 -0
  190. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/local-conversation-sidebar.tsx +295 -0
  191. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/markdown-content.tsx +167 -0
  192. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-item.tsx +79 -0
  193. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-list.tsx +18 -0
  194. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-call-card.tsx +79 -0
  195. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/icons/google-icon.tsx +32 -0
  196. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/icons/index.ts +3 -0
  197. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/language-switcher.tsx +97 -0
  198. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/header.tsx +65 -0
  199. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/index.ts +2 -0
  200. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/sidebar.tsx +82 -0
  201. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/index.ts +7 -0
  202. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-provider.tsx +53 -0
  203. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-toggle.tsx +105 -0
  204. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/badge.tsx +35 -0
  205. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/button.test.tsx +75 -0
  206. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/button.tsx +56 -0
  207. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/card.tsx +82 -0
  208. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/index.ts +13 -0
  209. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/input.tsx +21 -0
  210. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/label.tsx +21 -0
  211. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/sheet.tsx +109 -0
  212. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/index.ts +7 -0
  213. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-auth.ts +97 -0
  214. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-chat.ts +203 -0
  215. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-conversations.ts +181 -0
  216. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-local-chat.ts +165 -0
  217. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-websocket.ts +105 -0
  218. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/i18n.ts +37 -0
  219. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/api-client.ts +90 -0
  220. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/constants.ts +39 -0
  221. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/server-api.ts +78 -0
  222. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/utils.test.ts +44 -0
  223. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/utils.ts +44 -0
  224. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/middleware.ts +31 -0
  225. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/auth-store.test.ts +72 -0
  226. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/auth-store.ts +64 -0
  227. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/chat-sidebar-store.ts +17 -0
  228. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/chat-store.ts +65 -0
  229. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/conversation-store.ts +76 -0
  230. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/index.ts +9 -0
  231. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/local-chat-store.ts +255 -0
  232. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/sidebar-store.ts +17 -0
  233. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/theme-store.ts +44 -0
  234. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/api.ts +27 -0
  235. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/auth.ts +52 -0
  236. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/chat.ts +83 -0
  237. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/conversation.ts +49 -0
  238. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/index.ts +10 -0
  239. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/tsconfig.json +28 -0
  240. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/vitest.config.ts +36 -0
  241. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/vitest.setup.ts +56 -0
@@ -0,0 +1,838 @@
1
+ {%- if cookiecutter.enable_conversation_persistence and cookiecutter.use_postgresql %}
2
+ """Conversation repository (PostgreSQL async).
3
+
4
+ Contains database operations for Conversation, Message, and ToolCall entities.
5
+ """
6
+
7
+ from datetime import datetime
8
+ from uuid import UUID
9
+
10
+ from sqlalchemy import func, select, update as sql_update
11
+ from sqlalchemy.ext.asyncio import AsyncSession
12
+ from sqlalchemy.orm import selectinload
13
+
14
+ from app.db.models.conversation import Conversation, Message, ToolCall
15
+
16
+
17
+ # =============================================================================
18
+ # Conversation Operations
19
+ # =============================================================================
20
+
21
+
22
+ async def get_conversation_by_id(
23
+ db: AsyncSession,
24
+ conversation_id: UUID,
25
+ *,
26
+ include_messages: bool = False,
27
+ ) -> Conversation | None:
28
+ """Get conversation by ID, optionally with messages."""
29
+ if include_messages:
30
+ query = (
31
+ select(Conversation)
32
+ .options(selectinload(Conversation.messages).selectinload(Message.tool_calls))
33
+ .where(Conversation.id == conversation_id)
34
+ )
35
+ result = await db.execute(query)
36
+ return result.scalar_one_or_none()
37
+ return await db.get(Conversation, conversation_id)
38
+
39
+
40
+ async def get_conversations_by_user(
41
+ db: AsyncSession,
42
+ {%- if cookiecutter.use_jwt %}
43
+ user_id: UUID | None = None,
44
+ {%- endif %}
45
+ *,
46
+ skip: int = 0,
47
+ limit: int = 50,
48
+ include_archived: bool = False,
49
+ ) -> list[Conversation]:
50
+ """Get conversations for a user with pagination."""
51
+ query = select(Conversation)
52
+ {%- if cookiecutter.use_jwt %}
53
+ if user_id:
54
+ query = query.where(Conversation.user_id == user_id)
55
+ {%- endif %}
56
+ if not include_archived:
57
+ query = query.where(Conversation.is_archived == False) # noqa: E712
58
+ query = query.order_by(Conversation.updated_at.desc()).offset(skip).limit(limit)
59
+ result = await db.execute(query)
60
+ return list(result.scalars().all())
61
+
62
+
63
+ async def count_conversations(
64
+ db: AsyncSession,
65
+ {%- if cookiecutter.use_jwt %}
66
+ user_id: UUID | None = None,
67
+ {%- endif %}
68
+ *,
69
+ include_archived: bool = False,
70
+ ) -> int:
71
+ """Count conversations for a user."""
72
+ query = select(func.count(Conversation.id))
73
+ {%- if cookiecutter.use_jwt %}
74
+ if user_id:
75
+ query = query.where(Conversation.user_id == user_id)
76
+ {%- endif %}
77
+ if not include_archived:
78
+ query = query.where(Conversation.is_archived == False) # noqa: E712
79
+ result = await db.execute(query)
80
+ return result.scalar() or 0
81
+
82
+
83
+ async def create_conversation(
84
+ db: AsyncSession,
85
+ *,
86
+ {%- if cookiecutter.use_jwt %}
87
+ user_id: UUID | None = None,
88
+ {%- endif %}
89
+ title: str | None = None,
90
+ ) -> Conversation:
91
+ """Create a new conversation."""
92
+ conversation = Conversation(
93
+ {%- if cookiecutter.use_jwt %}
94
+ user_id=user_id,
95
+ {%- endif %}
96
+ title=title,
97
+ )
98
+ db.add(conversation)
99
+ await db.flush()
100
+ await db.refresh(conversation)
101
+ return conversation
102
+
103
+
104
+ async def update_conversation(
105
+ db: AsyncSession,
106
+ *,
107
+ db_conversation: Conversation,
108
+ update_data: dict,
109
+ ) -> Conversation:
110
+ """Update a conversation."""
111
+ for field, value in update_data.items():
112
+ setattr(db_conversation, field, value)
113
+
114
+ db.add(db_conversation)
115
+ await db.flush()
116
+ await db.refresh(db_conversation)
117
+ return db_conversation
118
+
119
+
120
+ async def archive_conversation(
121
+ db: AsyncSession,
122
+ conversation_id: UUID,
123
+ ) -> Conversation | None:
124
+ """Archive a conversation."""
125
+ conversation = await get_conversation_by_id(db, conversation_id)
126
+ if conversation:
127
+ conversation.is_archived = True
128
+ db.add(conversation)
129
+ await db.flush()
130
+ await db.refresh(conversation)
131
+ return conversation
132
+
133
+
134
+ async def delete_conversation(db: AsyncSession, conversation_id: UUID) -> bool:
135
+ """Delete a conversation and all related messages/tool_calls (cascades)."""
136
+ conversation = await get_conversation_by_id(db, conversation_id)
137
+ if conversation:
138
+ await db.delete(conversation)
139
+ await db.flush()
140
+ return True
141
+ return False
142
+
143
+
144
+ # =============================================================================
145
+ # Message Operations
146
+ # =============================================================================
147
+
148
+
149
+ async def get_message_by_id(db: AsyncSession, message_id: UUID) -> Message | None:
150
+ """Get message by ID."""
151
+ return await db.get(Message, message_id)
152
+
153
+
154
+ async def get_messages_by_conversation(
155
+ db: AsyncSession,
156
+ conversation_id: UUID,
157
+ *,
158
+ skip: int = 0,
159
+ limit: int = 100,
160
+ include_tool_calls: bool = False,
161
+ ) -> list[Message]:
162
+ """Get messages for a conversation with pagination."""
163
+ query = select(Message).where(Message.conversation_id == conversation_id)
164
+ if include_tool_calls:
165
+ query = query.options(selectinload(Message.tool_calls))
166
+ query = query.order_by(Message.created_at.asc()).offset(skip).limit(limit)
167
+ result = await db.execute(query)
168
+ return list(result.scalars().all())
169
+
170
+
171
+ async def count_messages(db: AsyncSession, conversation_id: UUID) -> int:
172
+ """Count messages in a conversation."""
173
+ query = select(func.count(Message.id)).where(Message.conversation_id == conversation_id)
174
+ result = await db.execute(query)
175
+ return result.scalar() or 0
176
+
177
+
178
+ async def create_message(
179
+ db: AsyncSession,
180
+ *,
181
+ conversation_id: UUID,
182
+ role: str,
183
+ content: str,
184
+ model_name: str | None = None,
185
+ tokens_used: int | None = None,
186
+ ) -> Message:
187
+ """Create a new message."""
188
+ message = Message(
189
+ conversation_id=conversation_id,
190
+ role=role,
191
+ content=content,
192
+ model_name=model_name,
193
+ tokens_used=tokens_used,
194
+ )
195
+ db.add(message)
196
+ await db.flush()
197
+ await db.refresh(message)
198
+
199
+ # Update conversation's updated_at timestamp
200
+ await db.execute(
201
+ sql_update(Conversation)
202
+ .where(Conversation.id == conversation_id)
203
+ .values(updated_at=message.created_at)
204
+ )
205
+
206
+ return message
207
+
208
+
209
+ async def delete_message(db: AsyncSession, message_id: UUID) -> bool:
210
+ """Delete a message."""
211
+ message = await get_message_by_id(db, message_id)
212
+ if message:
213
+ await db.delete(message)
214
+ await db.flush()
215
+ return True
216
+ return False
217
+
218
+
219
+ # =============================================================================
220
+ # ToolCall Operations
221
+ # =============================================================================
222
+
223
+
224
+ async def get_tool_call_by_id(db: AsyncSession, tool_call_id: UUID) -> ToolCall | None:
225
+ """Get tool call by ID."""
226
+ return await db.get(ToolCall, tool_call_id)
227
+
228
+
229
+ async def get_tool_calls_by_message(
230
+ db: AsyncSession,
231
+ message_id: UUID,
232
+ ) -> list[ToolCall]:
233
+ """Get tool calls for a message."""
234
+ query = (
235
+ select(ToolCall)
236
+ .where(ToolCall.message_id == message_id)
237
+ .order_by(ToolCall.started_at.asc())
238
+ )
239
+ result = await db.execute(query)
240
+ return list(result.scalars().all())
241
+
242
+
243
+ async def create_tool_call(
244
+ db: AsyncSession,
245
+ *,
246
+ message_id: UUID,
247
+ tool_call_id: str,
248
+ tool_name: str,
249
+ args: dict,
250
+ started_at: datetime,
251
+ ) -> ToolCall:
252
+ """Create a new tool call record."""
253
+ tool_call = ToolCall(
254
+ message_id=message_id,
255
+ tool_call_id=tool_call_id,
256
+ tool_name=tool_name,
257
+ args=args,
258
+ started_at=started_at,
259
+ status="running",
260
+ )
261
+ db.add(tool_call)
262
+ await db.flush()
263
+ await db.refresh(tool_call)
264
+ return tool_call
265
+
266
+
267
+ async def complete_tool_call(
268
+ db: AsyncSession,
269
+ *,
270
+ db_tool_call: ToolCall,
271
+ result: str,
272
+ completed_at: datetime,
273
+ success: bool = True,
274
+ ) -> ToolCall:
275
+ """Mark a tool call as completed."""
276
+ db_tool_call.result = result
277
+ db_tool_call.completed_at = completed_at
278
+ db_tool_call.status = "completed" if success else "failed"
279
+
280
+ # Calculate duration
281
+ if db_tool_call.started_at:
282
+ delta = completed_at - db_tool_call.started_at
283
+ db_tool_call.duration_ms = int(delta.total_seconds() * 1000)
284
+
285
+ db.add(db_tool_call)
286
+ await db.flush()
287
+ await db.refresh(db_tool_call)
288
+ return db_tool_call
289
+
290
+
291
+ {%- elif cookiecutter.enable_conversation_persistence and cookiecutter.use_sqlite %}
292
+ """Conversation repository (SQLite sync).
293
+
294
+ Contains database operations for Conversation, Message, and ToolCall entities.
295
+ """
296
+
297
+ from datetime import datetime
298
+
299
+ from sqlalchemy import func, select, update as sql_update
300
+ from sqlalchemy.orm import Session, selectinload
301
+
302
+ from app.db.models.conversation import Conversation, Message, ToolCall
303
+
304
+
305
+ # =============================================================================
306
+ # Conversation Operations
307
+ # =============================================================================
308
+
309
+
310
+ def get_conversation_by_id(
311
+ db: Session,
312
+ conversation_id: str,
313
+ *,
314
+ include_messages: bool = False,
315
+ ) -> Conversation | None:
316
+ """Get conversation by ID, optionally with messages."""
317
+ if include_messages:
318
+ query = (
319
+ select(Conversation)
320
+ .options(selectinload(Conversation.messages).selectinload(Message.tool_calls))
321
+ .where(Conversation.id == conversation_id)
322
+ )
323
+ result = db.execute(query)
324
+ return result.scalar_one_or_none()
325
+ return db.get(Conversation, conversation_id)
326
+
327
+
328
+ def get_conversations_by_user(
329
+ db: Session,
330
+ {%- if cookiecutter.use_jwt %}
331
+ user_id: str | None = None,
332
+ {%- endif %}
333
+ *,
334
+ skip: int = 0,
335
+ limit: int = 50,
336
+ include_archived: bool = False,
337
+ ) -> list[Conversation]:
338
+ """Get conversations for a user with pagination."""
339
+ query = select(Conversation)
340
+ {%- if cookiecutter.use_jwt %}
341
+ if user_id:
342
+ query = query.where(Conversation.user_id == user_id)
343
+ {%- endif %}
344
+ if not include_archived:
345
+ query = query.where(Conversation.is_archived == False) # noqa: E712
346
+ query = query.order_by(Conversation.updated_at.desc()).offset(skip).limit(limit)
347
+ result = db.execute(query)
348
+ return list(result.scalars().all())
349
+
350
+
351
+ def count_conversations(
352
+ db: Session,
353
+ {%- if cookiecutter.use_jwt %}
354
+ user_id: str | None = None,
355
+ {%- endif %}
356
+ *,
357
+ include_archived: bool = False,
358
+ ) -> int:
359
+ """Count conversations for a user."""
360
+ query = select(func.count(Conversation.id))
361
+ {%- if cookiecutter.use_jwt %}
362
+ if user_id:
363
+ query = query.where(Conversation.user_id == user_id)
364
+ {%- endif %}
365
+ if not include_archived:
366
+ query = query.where(Conversation.is_archived == False) # noqa: E712
367
+ result = db.execute(query)
368
+ return result.scalar() or 0
369
+
370
+
371
+ def create_conversation(
372
+ db: Session,
373
+ *,
374
+ {%- if cookiecutter.use_jwt %}
375
+ user_id: str | None = None,
376
+ {%- endif %}
377
+ title: str | None = None,
378
+ ) -> Conversation:
379
+ """Create a new conversation."""
380
+ conversation = Conversation(
381
+ {%- if cookiecutter.use_jwt %}
382
+ user_id=user_id,
383
+ {%- endif %}
384
+ title=title,
385
+ )
386
+ db.add(conversation)
387
+ db.flush()
388
+ db.refresh(conversation)
389
+ return conversation
390
+
391
+
392
+ def update_conversation(
393
+ db: Session,
394
+ *,
395
+ db_conversation: Conversation,
396
+ update_data: dict,
397
+ ) -> Conversation:
398
+ """Update a conversation."""
399
+ for field, value in update_data.items():
400
+ setattr(db_conversation, field, value)
401
+
402
+ db.add(db_conversation)
403
+ db.flush()
404
+ db.refresh(db_conversation)
405
+ return db_conversation
406
+
407
+
408
+ def archive_conversation(
409
+ db: Session,
410
+ conversation_id: str,
411
+ ) -> Conversation | None:
412
+ """Archive a conversation."""
413
+ conversation = get_conversation_by_id(db, conversation_id)
414
+ if conversation:
415
+ conversation.is_archived = True
416
+ db.add(conversation)
417
+ db.flush()
418
+ db.refresh(conversation)
419
+ return conversation
420
+
421
+
422
+ def delete_conversation(db: Session, conversation_id: str) -> bool:
423
+ """Delete a conversation and all related messages/tool_calls (cascades)."""
424
+ conversation = get_conversation_by_id(db, conversation_id)
425
+ if conversation:
426
+ db.delete(conversation)
427
+ db.flush()
428
+ return True
429
+ return False
430
+
431
+
432
+ # =============================================================================
433
+ # Message Operations
434
+ # =============================================================================
435
+
436
+
437
+ def get_message_by_id(db: Session, message_id: str) -> Message | None:
438
+ """Get message by ID."""
439
+ return db.get(Message, message_id)
440
+
441
+
442
+ def get_messages_by_conversation(
443
+ db: Session,
444
+ conversation_id: str,
445
+ *,
446
+ skip: int = 0,
447
+ limit: int = 100,
448
+ include_tool_calls: bool = False,
449
+ ) -> list[Message]:
450
+ """Get messages for a conversation with pagination."""
451
+ query = select(Message).where(Message.conversation_id == conversation_id)
452
+ if include_tool_calls:
453
+ query = query.options(selectinload(Message.tool_calls))
454
+ query = query.order_by(Message.created_at.asc()).offset(skip).limit(limit)
455
+ result = db.execute(query)
456
+ return list(result.scalars().all())
457
+
458
+
459
+ def count_messages(db: Session, conversation_id: str) -> int:
460
+ """Count messages in a conversation."""
461
+ query = select(func.count(Message.id)).where(Message.conversation_id == conversation_id)
462
+ result = db.execute(query)
463
+ return result.scalar() or 0
464
+
465
+
466
+ def create_message(
467
+ db: Session,
468
+ *,
469
+ conversation_id: str,
470
+ role: str,
471
+ content: str,
472
+ model_name: str | None = None,
473
+ tokens_used: int | None = None,
474
+ ) -> Message:
475
+ """Create a new message."""
476
+ message = Message(
477
+ conversation_id=conversation_id,
478
+ role=role,
479
+ content=content,
480
+ model_name=model_name,
481
+ tokens_used=tokens_used,
482
+ )
483
+ db.add(message)
484
+ db.flush()
485
+ db.refresh(message)
486
+
487
+ # Update conversation's updated_at timestamp
488
+ db.execute(
489
+ sql_update(Conversation)
490
+ .where(Conversation.id == conversation_id)
491
+ .values(updated_at=message.created_at)
492
+ )
493
+
494
+ return message
495
+
496
+
497
+ def delete_message(db: Session, message_id: str) -> bool:
498
+ """Delete a message."""
499
+ message = get_message_by_id(db, message_id)
500
+ if message:
501
+ db.delete(message)
502
+ db.flush()
503
+ return True
504
+ return False
505
+
506
+
507
+ # =============================================================================
508
+ # ToolCall Operations
509
+ # =============================================================================
510
+
511
+
512
+ def get_tool_call_by_id(db: Session, tool_call_id: str) -> ToolCall | None:
513
+ """Get tool call by ID."""
514
+ return db.get(ToolCall, tool_call_id)
515
+
516
+
517
+ def get_tool_calls_by_message(
518
+ db: Session,
519
+ message_id: str,
520
+ ) -> list[ToolCall]:
521
+ """Get tool calls for a message."""
522
+ query = (
523
+ select(ToolCall)
524
+ .where(ToolCall.message_id == message_id)
525
+ .order_by(ToolCall.started_at.asc())
526
+ )
527
+ result = db.execute(query)
528
+ return list(result.scalars().all())
529
+
530
+
531
+ def create_tool_call(
532
+ db: Session,
533
+ *,
534
+ message_id: str,
535
+ tool_call_id: str,
536
+ tool_name: str,
537
+ args: dict,
538
+ started_at: datetime,
539
+ ) -> ToolCall:
540
+ """Create a new tool call record."""
541
+ import json
542
+
543
+ tool_call = ToolCall(
544
+ message_id=message_id,
545
+ tool_call_id=tool_call_id,
546
+ tool_name=tool_name,
547
+ args=json.dumps(args), # SQLite stores as JSON string
548
+ started_at=started_at,
549
+ status="running",
550
+ )
551
+ db.add(tool_call)
552
+ db.flush()
553
+ db.refresh(tool_call)
554
+ return tool_call
555
+
556
+
557
+ def complete_tool_call(
558
+ db: Session,
559
+ *,
560
+ db_tool_call: ToolCall,
561
+ result: str,
562
+ completed_at: datetime,
563
+ success: bool = True,
564
+ ) -> ToolCall:
565
+ """Mark a tool call as completed."""
566
+ db_tool_call.result = result
567
+ db_tool_call.completed_at = completed_at
568
+ db_tool_call.status = "completed" if success else "failed"
569
+
570
+ # Calculate duration
571
+ if db_tool_call.started_at:
572
+ delta = completed_at - db_tool_call.started_at
573
+ db_tool_call.duration_ms = int(delta.total_seconds() * 1000)
574
+
575
+ db.add(db_tool_call)
576
+ db.flush()
577
+ db.refresh(db_tool_call)
578
+ return db_tool_call
579
+
580
+
581
+ {%- elif cookiecutter.enable_conversation_persistence and cookiecutter.use_mongodb %}
582
+ """Conversation repository (MongoDB).
583
+
584
+ Contains database operations for Conversation, Message, and ToolCall entities.
585
+ """
586
+
587
+ from datetime import UTC, datetime
588
+
589
+ from app.db.models.conversation import Conversation, Message, ToolCall
590
+
591
+
592
+ # =============================================================================
593
+ # Conversation Operations
594
+ # =============================================================================
595
+
596
+
597
+ async def get_conversation_by_id(
598
+ conversation_id: str,
599
+ *,
600
+ include_messages: bool = False,
601
+ ) -> Conversation | None:
602
+ """Get conversation by ID."""
603
+ conversation = await Conversation.get(conversation_id)
604
+ # Note: MongoDB doesn't auto-load related documents; handle in service layer
605
+ return conversation
606
+
607
+
608
+ async def get_conversations_by_user(
609
+ {%- if cookiecutter.use_jwt %}
610
+ user_id: str | None = None,
611
+ {%- endif %}
612
+ *,
613
+ skip: int = 0,
614
+ limit: int = 50,
615
+ include_archived: bool = False,
616
+ ) -> list[Conversation]:
617
+ """Get conversations for a user with pagination."""
618
+ query_filter = {}
619
+ {%- if cookiecutter.use_jwt %}
620
+ if user_id:
621
+ query_filter["user_id"] = user_id
622
+ {%- endif %}
623
+ if not include_archived:
624
+ query_filter["is_archived"] = False
625
+
626
+ return await Conversation.find(query_filter).sort("-created_at").skip(skip).limit(limit).to_list()
627
+
628
+
629
+ async def count_conversations(
630
+ {%- if cookiecutter.use_jwt %}
631
+ user_id: str | None = None,
632
+ {%- endif %}
633
+ *,
634
+ include_archived: bool = False,
635
+ ) -> int:
636
+ """Count conversations for a user."""
637
+ query_filter = {}
638
+ {%- if cookiecutter.use_jwt %}
639
+ if user_id:
640
+ query_filter["user_id"] = user_id
641
+ {%- endif %}
642
+ if not include_archived:
643
+ query_filter["is_archived"] = False
644
+
645
+ return await Conversation.find(query_filter).count()
646
+
647
+
648
+ async def create_conversation(
649
+ *,
650
+ {%- if cookiecutter.use_jwt %}
651
+ user_id: str | None = None,
652
+ {%- endif %}
653
+ title: str | None = None,
654
+ ) -> Conversation:
655
+ """Create a new conversation."""
656
+ conversation = Conversation(
657
+ {%- if cookiecutter.use_jwt %}
658
+ user_id=user_id,
659
+ {%- endif %}
660
+ title=title,
661
+ )
662
+ await conversation.insert()
663
+ return conversation
664
+
665
+
666
+ async def update_conversation(
667
+ *,
668
+ db_conversation: Conversation,
669
+ update_data: dict,
670
+ ) -> Conversation:
671
+ """Update a conversation."""
672
+ for field, value in update_data.items():
673
+ setattr(db_conversation, field, value)
674
+ db_conversation.updated_at = datetime.now(UTC)
675
+ await db_conversation.save()
676
+ return db_conversation
677
+
678
+
679
+ async def archive_conversation(
680
+ conversation_id: str,
681
+ ) -> Conversation | None:
682
+ """Archive a conversation."""
683
+ conversation = await get_conversation_by_id(conversation_id)
684
+ if conversation:
685
+ conversation.is_archived = True
686
+ conversation.updated_at = datetime.now(UTC)
687
+ await conversation.save()
688
+ return conversation
689
+
690
+
691
+ async def delete_conversation(conversation_id: str) -> bool:
692
+ """Delete a conversation and all related messages/tool_calls."""
693
+ conversation = await get_conversation_by_id(conversation_id)
694
+ if conversation:
695
+ # Delete related messages and tool calls
696
+ messages = await get_messages_by_conversation(str(conversation.id))
697
+ for message in messages:
698
+ await ToolCall.find(ToolCall.message_id == str(message.id)).delete()
699
+ await Message.find(Message.conversation_id == str(conversation.id)).delete()
700
+ await conversation.delete()
701
+ return True
702
+ return False
703
+
704
+
705
+ # =============================================================================
706
+ # Message Operations
707
+ # =============================================================================
708
+
709
+
710
+ async def get_message_by_id(message_id: str) -> Message | None:
711
+ """Get message by ID."""
712
+ return await Message.get(message_id)
713
+
714
+
715
+ async def get_messages_by_conversation(
716
+ conversation_id: str,
717
+ *,
718
+ skip: int = 0,
719
+ limit: int = 100,
720
+ ) -> list[Message]:
721
+ """Get messages for a conversation with pagination."""
722
+ return await (
723
+ Message.find(Message.conversation_id == conversation_id)
724
+ .sort("created_at")
725
+ .skip(skip)
726
+ .limit(limit)
727
+ .to_list()
728
+ )
729
+
730
+
731
+ async def count_messages(conversation_id: str) -> int:
732
+ """Count messages in a conversation."""
733
+ return await Message.find(Message.conversation_id == conversation_id).count()
734
+
735
+
736
+ async def create_message(
737
+ *,
738
+ conversation_id: str,
739
+ role: str,
740
+ content: str,
741
+ model_name: str | None = None,
742
+ tokens_used: int | None = None,
743
+ ) -> Message:
744
+ """Create a new message."""
745
+ message = Message(
746
+ conversation_id=conversation_id,
747
+ role=role,
748
+ content=content,
749
+ model_name=model_name,
750
+ tokens_used=tokens_used,
751
+ )
752
+ await message.insert()
753
+
754
+ # Update conversation's updated_at timestamp
755
+ conversation = await get_conversation_by_id(conversation_id)
756
+ if conversation:
757
+ conversation.updated_at = datetime.now(UTC)
758
+ await conversation.save()
759
+
760
+ return message
761
+
762
+
763
+ async def delete_message(message_id: str) -> bool:
764
+ """Delete a message and its tool calls."""
765
+ message = await get_message_by_id(message_id)
766
+ if message:
767
+ await ToolCall.find(ToolCall.message_id == str(message.id)).delete()
768
+ await message.delete()
769
+ return True
770
+ return False
771
+
772
+
773
+ # =============================================================================
774
+ # ToolCall Operations
775
+ # =============================================================================
776
+
777
+
778
+ async def get_tool_call_by_id(tool_call_id: str) -> ToolCall | None:
779
+ """Get tool call by ID."""
780
+ return await ToolCall.get(tool_call_id)
781
+
782
+
783
+ async def get_tool_calls_by_message(
784
+ message_id: str,
785
+ ) -> list[ToolCall]:
786
+ """Get tool calls for a message."""
787
+ return await (
788
+ ToolCall.find(ToolCall.message_id == message_id)
789
+ .sort("started_at")
790
+ .to_list()
791
+ )
792
+
793
+
794
+ async def create_tool_call(
795
+ *,
796
+ message_id: str,
797
+ tool_call_id: str,
798
+ tool_name: str,
799
+ args: dict,
800
+ started_at: datetime,
801
+ ) -> ToolCall:
802
+ """Create a new tool call record."""
803
+ tool_call = ToolCall(
804
+ message_id=message_id,
805
+ tool_call_id=tool_call_id,
806
+ tool_name=tool_name,
807
+ args=args,
808
+ started_at=started_at,
809
+ status="running",
810
+ )
811
+ await tool_call.insert()
812
+ return tool_call
813
+
814
+
815
+ async def complete_tool_call(
816
+ *,
817
+ db_tool_call: ToolCall,
818
+ result: str,
819
+ completed_at: datetime,
820
+ success: bool = True,
821
+ ) -> ToolCall:
822
+ """Mark a tool call as completed."""
823
+ db_tool_call.result = result
824
+ db_tool_call.completed_at = completed_at
825
+ db_tool_call.status = "completed" if success else "failed"
826
+
827
+ # Calculate duration
828
+ if db_tool_call.started_at:
829
+ delta = completed_at - db_tool_call.started_at
830
+ db_tool_call.duration_ms = int(delta.total_seconds() * 1000)
831
+
832
+ await db_tool_call.save()
833
+ return db_tool_call
834
+
835
+
836
+ {%- else %}
837
+ """Conversation repository - not configured."""
838
+ {%- endif %}