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