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,850 @@
1
+ {%- if cookiecutter.enable_conversation_persistence and cookiecutter.use_postgresql %}
2
+ """Conversation service (PostgreSQL async).
3
+
4
+ Contains business logic for conversation, message, and tool call operations.
5
+ """
6
+
7
+ from datetime import datetime
8
+ from uuid import UUID
9
+
10
+ from sqlalchemy.ext.asyncio import AsyncSession
11
+
12
+ from app.core.exceptions import NotFoundError
13
+ from app.db.models.conversation import Conversation, Message, ToolCall
14
+ from app.repositories import conversation_repo
15
+ from app.schemas.conversation import (
16
+ ConversationCreate,
17
+ ConversationUpdate,
18
+ MessageCreate,
19
+ ToolCallCreate,
20
+ ToolCallComplete,
21
+ )
22
+
23
+
24
+ class ConversationService:
25
+ """Service for conversation-related business logic."""
26
+
27
+ def __init__(self, db: AsyncSession):
28
+ self.db = db
29
+
30
+ # =========================================================================
31
+ # Conversation Methods
32
+ # =========================================================================
33
+
34
+ async def get_conversation(
35
+ self,
36
+ conversation_id: UUID,
37
+ *,
38
+ include_messages: bool = False,
39
+ ) -> Conversation:
40
+ """Get conversation by ID.
41
+
42
+ Raises:
43
+ NotFoundError: If conversation does not exist.
44
+ """
45
+ conversation = await conversation_repo.get_conversation_by_id(
46
+ self.db, conversation_id, include_messages=include_messages
47
+ )
48
+ if not conversation:
49
+ raise NotFoundError(
50
+ message="Conversation not found",
51
+ details={"conversation_id": str(conversation_id)},
52
+ )
53
+ return conversation
54
+
55
+ async def list_conversations(
56
+ self,
57
+ {%- if cookiecutter.use_jwt %}
58
+ user_id: UUID | None = None,
59
+ {%- endif %}
60
+ *,
61
+ skip: int = 0,
62
+ limit: int = 50,
63
+ include_archived: bool = False,
64
+ ) -> tuple[list[Conversation], int]:
65
+ """List conversations with pagination.
66
+
67
+ Returns:
68
+ Tuple of (conversations, total_count).
69
+ """
70
+ items = await conversation_repo.get_conversations_by_user(
71
+ self.db,
72
+ {%- if cookiecutter.use_jwt %}
73
+ user_id=user_id,
74
+ {%- endif %}
75
+ skip=skip,
76
+ limit=limit,
77
+ include_archived=include_archived,
78
+ )
79
+ total = await conversation_repo.count_conversations(
80
+ self.db,
81
+ {%- if cookiecutter.use_jwt %}
82
+ user_id=user_id,
83
+ {%- endif %}
84
+ include_archived=include_archived,
85
+ )
86
+ return items, total
87
+
88
+ async def create_conversation(
89
+ self,
90
+ data: ConversationCreate,
91
+ ) -> Conversation:
92
+ """Create a new conversation."""
93
+ return await conversation_repo.create_conversation(
94
+ self.db,
95
+ {%- if cookiecutter.use_jwt %}
96
+ user_id=data.user_id,
97
+ {%- endif %}
98
+ title=data.title,
99
+ )
100
+
101
+ async def update_conversation(
102
+ self,
103
+ conversation_id: UUID,
104
+ data: ConversationUpdate,
105
+ ) -> Conversation:
106
+ """Update a conversation.
107
+
108
+ Raises:
109
+ NotFoundError: If conversation does not exist.
110
+ """
111
+ conversation = await self.get_conversation(conversation_id)
112
+ update_data = data.model_dump(exclude_unset=True)
113
+ return await conversation_repo.update_conversation(
114
+ self.db, db_conversation=conversation, update_data=update_data
115
+ )
116
+
117
+ async def archive_conversation(self, conversation_id: UUID) -> Conversation:
118
+ """Archive a conversation.
119
+
120
+ Raises:
121
+ NotFoundError: If conversation does not exist.
122
+ """
123
+ conversation = await conversation_repo.archive_conversation(
124
+ self.db, conversation_id
125
+ )
126
+ if not conversation:
127
+ raise NotFoundError(
128
+ message="Conversation not found",
129
+ details={"conversation_id": str(conversation_id)},
130
+ )
131
+ return conversation
132
+
133
+ async def delete_conversation(self, conversation_id: UUID) -> bool:
134
+ """Delete a conversation.
135
+
136
+ Raises:
137
+ NotFoundError: If conversation does not exist.
138
+ """
139
+ deleted = await conversation_repo.delete_conversation(self.db, conversation_id)
140
+ if not deleted:
141
+ raise NotFoundError(
142
+ message="Conversation not found",
143
+ details={"conversation_id": str(conversation_id)},
144
+ )
145
+ return True
146
+
147
+ # =========================================================================
148
+ # Message Methods
149
+ # =========================================================================
150
+
151
+ async def get_message(self, message_id: UUID) -> Message:
152
+ """Get message by ID.
153
+
154
+ Raises:
155
+ NotFoundError: If message does not exist.
156
+ """
157
+ message = await conversation_repo.get_message_by_id(self.db, message_id)
158
+ if not message:
159
+ raise NotFoundError(
160
+ message="Message not found",
161
+ details={"message_id": str(message_id)},
162
+ )
163
+ return message
164
+
165
+ async def list_messages(
166
+ self,
167
+ conversation_id: UUID,
168
+ *,
169
+ skip: int = 0,
170
+ limit: int = 100,
171
+ include_tool_calls: bool = False,
172
+ ) -> tuple[list[Message], int]:
173
+ """List messages in a conversation.
174
+
175
+ Returns:
176
+ Tuple of (messages, total_count).
177
+ """
178
+ # Verify conversation exists
179
+ await self.get_conversation(conversation_id)
180
+ items = await conversation_repo.get_messages_by_conversation(
181
+ self.db,
182
+ conversation_id,
183
+ skip=skip,
184
+ limit=limit,
185
+ include_tool_calls=include_tool_calls,
186
+ )
187
+ total = await conversation_repo.count_messages(self.db, conversation_id)
188
+ return items, total
189
+
190
+ async def add_message(
191
+ self,
192
+ conversation_id: UUID,
193
+ data: MessageCreate,
194
+ ) -> Message:
195
+ """Add a message to a conversation.
196
+
197
+ Raises:
198
+ NotFoundError: If conversation does not exist.
199
+ """
200
+ # Verify conversation exists
201
+ await self.get_conversation(conversation_id)
202
+ return await conversation_repo.create_message(
203
+ self.db,
204
+ conversation_id=conversation_id,
205
+ role=data.role,
206
+ content=data.content,
207
+ model_name=data.model_name,
208
+ tokens_used=data.tokens_used,
209
+ )
210
+
211
+ async def delete_message(self, message_id: UUID) -> bool:
212
+ """Delete a message.
213
+
214
+ Raises:
215
+ NotFoundError: If message does not exist.
216
+ """
217
+ deleted = await conversation_repo.delete_message(self.db, message_id)
218
+ if not deleted:
219
+ raise NotFoundError(
220
+ message="Message not found",
221
+ details={"message_id": str(message_id)},
222
+ )
223
+ return True
224
+
225
+ # =========================================================================
226
+ # Tool Call Methods
227
+ # =========================================================================
228
+
229
+ async def get_tool_call(self, tool_call_id: UUID) -> ToolCall:
230
+ """Get tool call by ID.
231
+
232
+ Raises:
233
+ NotFoundError: If tool call does not exist.
234
+ """
235
+ tool_call = await conversation_repo.get_tool_call_by_id(self.db, tool_call_id)
236
+ if not tool_call:
237
+ raise NotFoundError(
238
+ message="Tool call not found",
239
+ details={"tool_call_id": str(tool_call_id)},
240
+ )
241
+ return tool_call
242
+
243
+ async def list_tool_calls(self, message_id: UUID) -> list[ToolCall]:
244
+ """List tool calls for a message."""
245
+ # Verify message exists
246
+ await self.get_message(message_id)
247
+ return await conversation_repo.get_tool_calls_by_message(self.db, message_id)
248
+
249
+ async def start_tool_call(
250
+ self,
251
+ message_id: UUID,
252
+ data: ToolCallCreate,
253
+ ) -> ToolCall:
254
+ """Record the start of a tool call.
255
+
256
+ Raises:
257
+ NotFoundError: If message does not exist.
258
+ """
259
+ # Verify message exists
260
+ await self.get_message(message_id)
261
+ return await conversation_repo.create_tool_call(
262
+ self.db,
263
+ message_id=message_id,
264
+ tool_call_id=data.tool_call_id,
265
+ tool_name=data.tool_name,
266
+ args=data.args,
267
+ started_at=data.started_at or datetime.utcnow(),
268
+ )
269
+
270
+ async def complete_tool_call(
271
+ self,
272
+ tool_call_id: UUID,
273
+ data: ToolCallComplete,
274
+ ) -> ToolCall:
275
+ """Mark a tool call as completed.
276
+
277
+ Raises:
278
+ NotFoundError: If tool call does not exist.
279
+ """
280
+ tool_call = await self.get_tool_call(tool_call_id)
281
+ return await conversation_repo.complete_tool_call(
282
+ self.db,
283
+ db_tool_call=tool_call,
284
+ result=data.result,
285
+ completed_at=data.completed_at or datetime.utcnow(),
286
+ success=data.success,
287
+ )
288
+
289
+
290
+ {%- elif cookiecutter.enable_conversation_persistence and cookiecutter.use_sqlite %}
291
+ """Conversation service (SQLite sync).
292
+
293
+ Contains business logic for conversation, message, and tool call operations.
294
+ """
295
+
296
+ from datetime import datetime
297
+
298
+ from sqlalchemy.orm import Session
299
+
300
+ from app.core.exceptions import NotFoundError
301
+ from app.db.models.conversation import Conversation, Message, ToolCall
302
+ from app.repositories import conversation_repo
303
+ from app.schemas.conversation import (
304
+ ConversationCreate,
305
+ ConversationUpdate,
306
+ MessageCreate,
307
+ ToolCallCreate,
308
+ ToolCallComplete,
309
+ )
310
+
311
+
312
+ class ConversationService:
313
+ """Service for conversation-related business logic."""
314
+
315
+ def __init__(self, db: Session):
316
+ self.db = db
317
+
318
+ # =========================================================================
319
+ # Conversation Methods
320
+ # =========================================================================
321
+
322
+ def get_conversation(
323
+ self,
324
+ conversation_id: str,
325
+ *,
326
+ include_messages: bool = False,
327
+ ) -> Conversation:
328
+ """Get conversation by ID.
329
+
330
+ Raises:
331
+ NotFoundError: If conversation does not exist.
332
+ """
333
+ conversation = conversation_repo.get_conversation_by_id(
334
+ self.db, conversation_id, include_messages=include_messages
335
+ )
336
+ if not conversation:
337
+ raise NotFoundError(
338
+ message="Conversation not found",
339
+ details={"conversation_id": conversation_id},
340
+ )
341
+ return conversation
342
+
343
+ def list_conversations(
344
+ self,
345
+ {%- if cookiecutter.use_jwt %}
346
+ user_id: str | None = None,
347
+ {%- endif %}
348
+ *,
349
+ skip: int = 0,
350
+ limit: int = 50,
351
+ include_archived: bool = False,
352
+ ) -> tuple[list[Conversation], int]:
353
+ """List conversations with pagination.
354
+
355
+ Returns:
356
+ Tuple of (conversations, total_count).
357
+ """
358
+ items = conversation_repo.get_conversations_by_user(
359
+ self.db,
360
+ {%- if cookiecutter.use_jwt %}
361
+ user_id=user_id,
362
+ {%- endif %}
363
+ skip=skip,
364
+ limit=limit,
365
+ include_archived=include_archived,
366
+ )
367
+ total = conversation_repo.count_conversations(
368
+ self.db,
369
+ {%- if cookiecutter.use_jwt %}
370
+ user_id=user_id,
371
+ {%- endif %}
372
+ include_archived=include_archived,
373
+ )
374
+ return items, total
375
+
376
+ def create_conversation(
377
+ self,
378
+ data: ConversationCreate,
379
+ ) -> Conversation:
380
+ """Create a new conversation."""
381
+ return conversation_repo.create_conversation(
382
+ self.db,
383
+ {%- if cookiecutter.use_jwt %}
384
+ user_id=data.user_id,
385
+ {%- endif %}
386
+ title=data.title,
387
+ )
388
+
389
+ def update_conversation(
390
+ self,
391
+ conversation_id: str,
392
+ data: ConversationUpdate,
393
+ ) -> Conversation:
394
+ """Update a conversation.
395
+
396
+ Raises:
397
+ NotFoundError: If conversation does not exist.
398
+ """
399
+ conversation = self.get_conversation(conversation_id)
400
+ update_data = data.model_dump(exclude_unset=True)
401
+ return conversation_repo.update_conversation(
402
+ self.db, db_conversation=conversation, update_data=update_data
403
+ )
404
+
405
+ def archive_conversation(self, conversation_id: str) -> Conversation:
406
+ """Archive a conversation.
407
+
408
+ Raises:
409
+ NotFoundError: If conversation does not exist.
410
+ """
411
+ conversation = conversation_repo.archive_conversation(self.db, conversation_id)
412
+ if not conversation:
413
+ raise NotFoundError(
414
+ message="Conversation not found",
415
+ details={"conversation_id": conversation_id},
416
+ )
417
+ return conversation
418
+
419
+ def delete_conversation(self, conversation_id: str) -> bool:
420
+ """Delete a conversation.
421
+
422
+ Raises:
423
+ NotFoundError: If conversation does not exist.
424
+ """
425
+ deleted = conversation_repo.delete_conversation(self.db, conversation_id)
426
+ if not deleted:
427
+ raise NotFoundError(
428
+ message="Conversation not found",
429
+ details={"conversation_id": conversation_id},
430
+ )
431
+ return True
432
+
433
+ # =========================================================================
434
+ # Message Methods
435
+ # =========================================================================
436
+
437
+ def get_message(self, message_id: str) -> Message:
438
+ """Get message by ID.
439
+
440
+ Raises:
441
+ NotFoundError: If message does not exist.
442
+ """
443
+ message = conversation_repo.get_message_by_id(self.db, message_id)
444
+ if not message:
445
+ raise NotFoundError(
446
+ message="Message not found",
447
+ details={"message_id": message_id},
448
+ )
449
+ return message
450
+
451
+ def list_messages(
452
+ self,
453
+ conversation_id: str,
454
+ *,
455
+ skip: int = 0,
456
+ limit: int = 100,
457
+ include_tool_calls: bool = False,
458
+ ) -> tuple[list[Message], int]:
459
+ """List messages in a conversation.
460
+
461
+ Returns:
462
+ Tuple of (messages, total_count).
463
+ """
464
+ # Verify conversation exists
465
+ self.get_conversation(conversation_id)
466
+ items = conversation_repo.get_messages_by_conversation(
467
+ self.db,
468
+ conversation_id,
469
+ skip=skip,
470
+ limit=limit,
471
+ include_tool_calls=include_tool_calls,
472
+ )
473
+ total = conversation_repo.count_messages(self.db, conversation_id)
474
+ return items, total
475
+
476
+ def add_message(
477
+ self,
478
+ conversation_id: str,
479
+ data: MessageCreate,
480
+ ) -> Message:
481
+ """Add a message to a conversation.
482
+
483
+ Raises:
484
+ NotFoundError: If conversation does not exist.
485
+ """
486
+ # Verify conversation exists
487
+ self.get_conversation(conversation_id)
488
+ return conversation_repo.create_message(
489
+ self.db,
490
+ conversation_id=conversation_id,
491
+ role=data.role,
492
+ content=data.content,
493
+ model_name=data.model_name,
494
+ tokens_used=data.tokens_used,
495
+ )
496
+
497
+ def delete_message(self, message_id: str) -> bool:
498
+ """Delete a message.
499
+
500
+ Raises:
501
+ NotFoundError: If message does not exist.
502
+ """
503
+ deleted = conversation_repo.delete_message(self.db, message_id)
504
+ if not deleted:
505
+ raise NotFoundError(
506
+ message="Message not found",
507
+ details={"message_id": message_id},
508
+ )
509
+ return True
510
+
511
+ # =========================================================================
512
+ # Tool Call Methods
513
+ # =========================================================================
514
+
515
+ def get_tool_call(self, tool_call_id: str) -> ToolCall:
516
+ """Get tool call by ID.
517
+
518
+ Raises:
519
+ NotFoundError: If tool call does not exist.
520
+ """
521
+ tool_call = conversation_repo.get_tool_call_by_id(self.db, tool_call_id)
522
+ if not tool_call:
523
+ raise NotFoundError(
524
+ message="Tool call not found",
525
+ details={"tool_call_id": tool_call_id},
526
+ )
527
+ return tool_call
528
+
529
+ def list_tool_calls(self, message_id: str) -> list[ToolCall]:
530
+ """List tool calls for a message."""
531
+ # Verify message exists
532
+ self.get_message(message_id)
533
+ return conversation_repo.get_tool_calls_by_message(self.db, message_id)
534
+
535
+ def start_tool_call(
536
+ self,
537
+ message_id: str,
538
+ data: ToolCallCreate,
539
+ ) -> ToolCall:
540
+ """Record the start of a tool call.
541
+
542
+ Raises:
543
+ NotFoundError: If message does not exist.
544
+ """
545
+ # Verify message exists
546
+ self.get_message(message_id)
547
+ return conversation_repo.create_tool_call(
548
+ self.db,
549
+ message_id=message_id,
550
+ tool_call_id=data.tool_call_id,
551
+ tool_name=data.tool_name,
552
+ args=data.args,
553
+ started_at=data.started_at or datetime.utcnow(),
554
+ )
555
+
556
+ def complete_tool_call(
557
+ self,
558
+ tool_call_id: str,
559
+ data: ToolCallComplete,
560
+ ) -> ToolCall:
561
+ """Mark a tool call as completed.
562
+
563
+ Raises:
564
+ NotFoundError: If tool call does not exist.
565
+ """
566
+ tool_call = self.get_tool_call(tool_call_id)
567
+ return conversation_repo.complete_tool_call(
568
+ self.db,
569
+ db_tool_call=tool_call,
570
+ result=data.result,
571
+ completed_at=data.completed_at or datetime.utcnow(),
572
+ success=data.success,
573
+ )
574
+
575
+
576
+ {%- elif cookiecutter.enable_conversation_persistence and cookiecutter.use_mongodb %}
577
+ """Conversation service (MongoDB).
578
+
579
+ Contains business logic for conversation, message, and tool call operations.
580
+ """
581
+
582
+ from datetime import datetime
583
+
584
+ from app.core.exceptions import NotFoundError
585
+ from app.db.models.conversation import Conversation, Message, ToolCall
586
+ from app.repositories import conversation_repo
587
+ from app.schemas.conversation import (
588
+ ConversationCreate,
589
+ ConversationUpdate,
590
+ MessageCreate,
591
+ ToolCallCreate,
592
+ ToolCallComplete,
593
+ )
594
+
595
+
596
+ class ConversationService:
597
+ """Service for conversation-related business logic."""
598
+
599
+ # =========================================================================
600
+ # Conversation Methods
601
+ # =========================================================================
602
+
603
+ async def get_conversation(
604
+ self,
605
+ conversation_id: str,
606
+ *,
607
+ include_messages: bool = False,
608
+ ) -> Conversation:
609
+ """Get conversation by ID.
610
+
611
+ Raises:
612
+ NotFoundError: If conversation does not exist.
613
+ """
614
+ conversation = await conversation_repo.get_conversation_by_id(
615
+ conversation_id, include_messages=include_messages
616
+ )
617
+ if not conversation:
618
+ raise NotFoundError(
619
+ message="Conversation not found",
620
+ details={"conversation_id": conversation_id},
621
+ )
622
+ return conversation
623
+
624
+ async def list_conversations(
625
+ self,
626
+ {%- if cookiecutter.use_jwt %}
627
+ user_id: str | None = None,
628
+ {%- endif %}
629
+ *,
630
+ skip: int = 0,
631
+ limit: int = 50,
632
+ include_archived: bool = False,
633
+ ) -> tuple[list[Conversation], int]:
634
+ """List conversations with pagination.
635
+
636
+ Returns:
637
+ Tuple of (conversations, total_count).
638
+ """
639
+ items = await conversation_repo.get_conversations_by_user(
640
+ {%- if cookiecutter.use_jwt %}
641
+ user_id=user_id,
642
+ {%- endif %}
643
+ skip=skip,
644
+ limit=limit,
645
+ include_archived=include_archived,
646
+ )
647
+ total = await conversation_repo.count_conversations(
648
+ {%- if cookiecutter.use_jwt %}
649
+ user_id=user_id,
650
+ {%- endif %}
651
+ include_archived=include_archived,
652
+ )
653
+ return items, total
654
+
655
+ async def create_conversation(
656
+ self,
657
+ data: ConversationCreate,
658
+ ) -> Conversation:
659
+ """Create a new conversation."""
660
+ return await conversation_repo.create_conversation(
661
+ {%- if cookiecutter.use_jwt %}
662
+ user_id=data.user_id,
663
+ {%- endif %}
664
+ title=data.title,
665
+ )
666
+
667
+ async def update_conversation(
668
+ self,
669
+ conversation_id: str,
670
+ data: ConversationUpdate,
671
+ ) -> Conversation:
672
+ """Update a conversation.
673
+
674
+ Raises:
675
+ NotFoundError: If conversation does not exist.
676
+ """
677
+ conversation = await self.get_conversation(conversation_id)
678
+ update_data = data.model_dump(exclude_unset=True)
679
+ return await conversation_repo.update_conversation(
680
+ db_conversation=conversation, update_data=update_data
681
+ )
682
+
683
+ async def archive_conversation(self, conversation_id: str) -> Conversation:
684
+ """Archive a conversation.
685
+
686
+ Raises:
687
+ NotFoundError: If conversation does not exist.
688
+ """
689
+ conversation = await conversation_repo.archive_conversation(conversation_id)
690
+ if not conversation:
691
+ raise NotFoundError(
692
+ message="Conversation not found",
693
+ details={"conversation_id": conversation_id},
694
+ )
695
+ return conversation
696
+
697
+ async def delete_conversation(self, conversation_id: str) -> bool:
698
+ """Delete a conversation.
699
+
700
+ Raises:
701
+ NotFoundError: If conversation does not exist.
702
+ """
703
+ deleted = await conversation_repo.delete_conversation(conversation_id)
704
+ if not deleted:
705
+ raise NotFoundError(
706
+ message="Conversation not found",
707
+ details={"conversation_id": conversation_id},
708
+ )
709
+ return True
710
+
711
+ # =========================================================================
712
+ # Message Methods
713
+ # =========================================================================
714
+
715
+ async def get_message(self, message_id: str) -> Message:
716
+ """Get message by ID.
717
+
718
+ Raises:
719
+ NotFoundError: If message does not exist.
720
+ """
721
+ message = await conversation_repo.get_message_by_id(message_id)
722
+ if not message:
723
+ raise NotFoundError(
724
+ message="Message not found",
725
+ details={"message_id": message_id},
726
+ )
727
+ return message
728
+
729
+ async def list_messages(
730
+ self,
731
+ conversation_id: str,
732
+ *,
733
+ skip: int = 0,
734
+ limit: int = 100,
735
+ ) -> tuple[list[Message], int]:
736
+ """List messages in a conversation.
737
+
738
+ Returns:
739
+ Tuple of (messages, total_count).
740
+ """
741
+ # Verify conversation exists
742
+ await self.get_conversation(conversation_id)
743
+ items = await conversation_repo.get_messages_by_conversation(
744
+ conversation_id,
745
+ skip=skip,
746
+ limit=limit,
747
+ )
748
+ total = await conversation_repo.count_messages(conversation_id)
749
+ return items, total
750
+
751
+ async def add_message(
752
+ self,
753
+ conversation_id: str,
754
+ data: MessageCreate,
755
+ ) -> Message:
756
+ """Add a message to a conversation.
757
+
758
+ Raises:
759
+ NotFoundError: If conversation does not exist.
760
+ """
761
+ # Verify conversation exists
762
+ await self.get_conversation(conversation_id)
763
+ return await conversation_repo.create_message(
764
+ conversation_id=conversation_id,
765
+ role=data.role,
766
+ content=data.content,
767
+ model_name=data.model_name,
768
+ tokens_used=data.tokens_used,
769
+ )
770
+
771
+ async def delete_message(self, message_id: str) -> bool:
772
+ """Delete a message.
773
+
774
+ Raises:
775
+ NotFoundError: If message does not exist.
776
+ """
777
+ deleted = await conversation_repo.delete_message(message_id)
778
+ if not deleted:
779
+ raise NotFoundError(
780
+ message="Message not found",
781
+ details={"message_id": message_id},
782
+ )
783
+ return True
784
+
785
+ # =========================================================================
786
+ # Tool Call Methods
787
+ # =========================================================================
788
+
789
+ async def get_tool_call(self, tool_call_id: str) -> ToolCall:
790
+ """Get tool call by ID.
791
+
792
+ Raises:
793
+ NotFoundError: If tool call does not exist.
794
+ """
795
+ tool_call = await conversation_repo.get_tool_call_by_id(tool_call_id)
796
+ if not tool_call:
797
+ raise NotFoundError(
798
+ message="Tool call not found",
799
+ details={"tool_call_id": tool_call_id},
800
+ )
801
+ return tool_call
802
+
803
+ async def list_tool_calls(self, message_id: str) -> list[ToolCall]:
804
+ """List tool calls for a message."""
805
+ # Verify message exists
806
+ await self.get_message(message_id)
807
+ return await conversation_repo.get_tool_calls_by_message(message_id)
808
+
809
+ async def start_tool_call(
810
+ self,
811
+ message_id: str,
812
+ data: ToolCallCreate,
813
+ ) -> ToolCall:
814
+ """Record the start of a tool call.
815
+
816
+ Raises:
817
+ NotFoundError: If message does not exist.
818
+ """
819
+ # Verify message exists
820
+ await self.get_message(message_id)
821
+ return await conversation_repo.create_tool_call(
822
+ message_id=message_id,
823
+ tool_call_id=data.tool_call_id,
824
+ tool_name=data.tool_name,
825
+ args=data.args,
826
+ started_at=data.started_at or datetime.utcnow(),
827
+ )
828
+
829
+ async def complete_tool_call(
830
+ self,
831
+ tool_call_id: str,
832
+ data: ToolCallComplete,
833
+ ) -> ToolCall:
834
+ """Mark a tool call as completed.
835
+
836
+ Raises:
837
+ NotFoundError: If tool call does not exist.
838
+ """
839
+ tool_call = await self.get_tool_call(tool_call_id)
840
+ return await conversation_repo.complete_tool_call(
841
+ db_tool_call=tool_call,
842
+ result=data.result,
843
+ completed_at=data.completed_at or datetime.utcnow(),
844
+ success=data.success,
845
+ )
846
+
847
+
848
+ {%- else %}
849
+ """Conversation service - not configured."""
850
+ {%- endif %}