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