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,902 @@
1
+ {%- if cookiecutter.enable_ai_agent and cookiecutter.use_pydantic_ai %}
2
+ """AI Agent WebSocket routes with streaming support (PydanticAI)."""
3
+
4
+ import logging
5
+ from typing import Any
6
+ {%- if cookiecutter.enable_conversation_persistence and cookiecutter.use_database %}
7
+ from datetime import datetime, UTC
8
+ {%- if cookiecutter.use_postgresql %}
9
+ from uuid import UUID
10
+ {%- endif %}
11
+ {%- endif %}
12
+
13
+ from fastapi import APIRouter, WebSocket, WebSocketDisconnect{%- if cookiecutter.websocket_auth_jwt %}, Depends{%- endif %}{%- if cookiecutter.websocket_auth_api_key %}, Query{%- endif %}
14
+
15
+ from pydantic_ai import (
16
+ Agent,
17
+ FinalResultEvent,
18
+ FunctionToolCallEvent,
19
+ FunctionToolResultEvent,
20
+ PartDeltaEvent,
21
+ PartStartEvent,
22
+ TextPartDelta,
23
+ ToolCallPartDelta,
24
+ )
25
+ from pydantic_ai.messages import (
26
+ ModelRequest,
27
+ ModelResponse,
28
+ SystemPromptPart,
29
+ TextPart,
30
+ UserPromptPart,
31
+ )
32
+
33
+ from app.agents.assistant import Deps, get_agent
34
+ {%- if cookiecutter.websocket_auth_jwt %}
35
+ from app.api.deps import get_current_user_ws
36
+ from app.db.models.user import User
37
+ {%- endif %}
38
+ {%- if cookiecutter.websocket_auth_api_key %}
39
+ from app.core.config import settings
40
+ {%- endif %}
41
+ {%- if cookiecutter.enable_conversation_persistence and (cookiecutter.use_postgresql or cookiecutter.use_sqlite) %}
42
+ from app.db.session import get_db_context
43
+ from app.api.deps import ConversationSvc, get_conversation_service
44
+ from app.schemas.conversation import ConversationCreate, MessageCreate, ToolCallCreate, ToolCallComplete
45
+ {%- elif cookiecutter.enable_conversation_persistence and cookiecutter.use_mongodb %}
46
+ from app.api.deps import ConversationSvc, get_conversation_service
47
+ from app.schemas.conversation import ConversationCreate, MessageCreate, ToolCallCreate, ToolCallComplete
48
+ {%- endif %}
49
+
50
+ logger = logging.getLogger(__name__)
51
+
52
+ router = APIRouter()
53
+
54
+
55
+ class AgentConnectionManager:
56
+ """WebSocket connection manager for AI agent."""
57
+
58
+ def __init__(self) -> None:
59
+ self.active_connections: list[WebSocket] = []
60
+
61
+ async def connect(self, websocket: WebSocket) -> None:
62
+ """Accept and store a new WebSocket connection."""
63
+ await websocket.accept()
64
+ self.active_connections.append(websocket)
65
+ logger.info(f"Agent WebSocket connected. Total connections: {len(self.active_connections)}")
66
+
67
+ def disconnect(self, websocket: WebSocket) -> None:
68
+ """Remove a WebSocket connection."""
69
+ if websocket in self.active_connections:
70
+ self.active_connections.remove(websocket)
71
+ logger.info(f"Agent WebSocket disconnected. Total connections: {len(self.active_connections)}")
72
+
73
+ async def send_event(self, websocket: WebSocket, event_type: str, data: Any) -> bool:
74
+ """Send a JSON event to a specific WebSocket client.
75
+
76
+ Returns True if sent successfully, False if connection is closed.
77
+ """
78
+ try:
79
+ await websocket.send_json({"type": event_type, "data": data})
80
+ return True
81
+ except (WebSocketDisconnect, RuntimeError):
82
+ # Connection already closed
83
+ return False
84
+
85
+
86
+ manager = AgentConnectionManager()
87
+
88
+
89
+ def build_message_history(history: list[dict[str, str]]) -> list[ModelRequest | ModelResponse]:
90
+ """Convert conversation history to PydanticAI message format."""
91
+ model_history: list[ModelRequest | ModelResponse] = []
92
+
93
+ for msg in history:
94
+ if msg["role"] == "user":
95
+ model_history.append(ModelRequest(parts=[UserPromptPart(content=msg["content"])]))
96
+ elif msg["role"] == "assistant":
97
+ model_history.append(ModelResponse(parts=[TextPart(content=msg["content"])]))
98
+ elif msg["role"] == "system":
99
+ model_history.append(ModelRequest(parts=[SystemPromptPart(content=msg["content"])]))
100
+
101
+ return model_history
102
+
103
+ {%- if cookiecutter.websocket_auth_api_key %}
104
+
105
+
106
+ async def verify_api_key(api_key: str) -> bool:
107
+ """Verify the API key for WebSocket authentication."""
108
+ return api_key == settings.API_KEY
109
+ {%- endif %}
110
+
111
+
112
+ @router.websocket("/ws/agent")
113
+ async def agent_websocket(
114
+ websocket: WebSocket,
115
+ {%- if cookiecutter.websocket_auth_jwt %}
116
+ user: User = Depends(get_current_user_ws),
117
+ {%- elif cookiecutter.websocket_auth_api_key %}
118
+ api_key: str = Query(..., alias="api_key"),
119
+ {%- endif %}
120
+ ) -> None:
121
+ """WebSocket endpoint for AI agent with full event streaming.
122
+
123
+ Uses PydanticAI iter() to stream all agent events including:
124
+ - user_prompt: When user input is received
125
+ - model_request_start: When model request begins
126
+ - text_delta: Streaming text from the model
127
+ - tool_call_delta: Streaming tool call arguments
128
+ - tool_call: When a tool is called (with full args)
129
+ - tool_result: When a tool returns a result
130
+ - final_result: When the final result is ready
131
+ - complete: When processing is complete
132
+ - error: When an error occurs
133
+
134
+ Expected input message format:
135
+ {
136
+ "message": "user message here",
137
+ "history": [{"role": "user|assistant|system", "content": "..."}]{% if cookiecutter.enable_conversation_persistence and cookiecutter.use_database %},
138
+ "conversation_id": "optional-uuid-to-continue-existing-conversation"{% endif %}
139
+ }
140
+ {%- if cookiecutter.websocket_auth_jwt %}
141
+
142
+ Authentication: Requires a valid JWT token passed as a query parameter or header.
143
+ {%- elif cookiecutter.websocket_auth_api_key %}
144
+
145
+ Authentication: Requires a valid API key passed as 'api_key' query parameter.
146
+ Example: ws://localhost:{{ cookiecutter.backend_port }}/api/v1/ws/agent?api_key=your-api-key
147
+ {%- endif %}
148
+ {%- if cookiecutter.enable_conversation_persistence and cookiecutter.use_database %}
149
+
150
+ Persistence: Set 'conversation_id' to continue an existing conversation.
151
+ If not provided, a new conversation is created. The conversation_id is
152
+ returned in the 'conversation_created' event.
153
+ {%- endif %}
154
+ """
155
+ {%- if cookiecutter.websocket_auth_api_key %}
156
+ # Verify API key before accepting connection
157
+ if not await verify_api_key(api_key):
158
+ await websocket.close(code=4001, reason="Invalid API key")
159
+ return
160
+ {%- endif %}
161
+
162
+ await manager.connect(websocket)
163
+
164
+ # Conversation state per connection
165
+ conversation_history: list[dict[str, str]] = []
166
+ deps = Deps()
167
+ {%- if cookiecutter.enable_conversation_persistence and cookiecutter.use_database %}
168
+ current_conversation_id: str | None = None
169
+ {%- endif %}
170
+
171
+ try:
172
+ while True:
173
+ # Receive user message
174
+ data = await websocket.receive_json()
175
+ user_message = data.get("message", "")
176
+ # Optionally accept history from client (or use server-side tracking)
177
+ if "history" in data:
178
+ conversation_history = data["history"]
179
+
180
+ if not user_message:
181
+ await manager.send_event(websocket, "error", {"message": "Empty message"})
182
+ continue
183
+
184
+ {%- if cookiecutter.enable_conversation_persistence and (cookiecutter.use_postgresql or cookiecutter.use_sqlite) %}
185
+
186
+ # Handle conversation persistence
187
+ try:
188
+ {%- if cookiecutter.use_postgresql %}
189
+ async with get_db_context() as db:
190
+ conv_service = get_conversation_service(db)
191
+
192
+ # Get or create conversation
193
+ requested_conv_id = data.get("conversation_id")
194
+ if requested_conv_id:
195
+ current_conversation_id = requested_conv_id
196
+ # Verify conversation exists
197
+ await conv_service.get_conversation(UUID(requested_conv_id))
198
+ elif not current_conversation_id:
199
+ # Create new conversation
200
+ conv_data = ConversationCreate(
201
+ {%- if cookiecutter.websocket_auth_jwt %}
202
+ user_id=user.id,
203
+ {%- endif %}
204
+ title=user_message[:50] if len(user_message) > 50 else user_message,
205
+ )
206
+ conversation = await conv_service.create_conversation(conv_data)
207
+ current_conversation_id = str(conversation.id)
208
+ await manager.send_event(
209
+ websocket,
210
+ "conversation_created",
211
+ {"conversation_id": current_conversation_id},
212
+ )
213
+
214
+ # Save user message
215
+ await conv_service.add_message(
216
+ UUID(current_conversation_id),
217
+ MessageCreate(role="user", content=user_message),
218
+ )
219
+ {%- else %}
220
+ with get_db_session() as db:
221
+ conv_service = get_conversation_service(db)
222
+
223
+ # Get or create conversation
224
+ requested_conv_id = data.get("conversation_id")
225
+ if requested_conv_id:
226
+ current_conversation_id = requested_conv_id
227
+ conv_service.get_conversation(requested_conv_id)
228
+ elif not current_conversation_id:
229
+ # Create new conversation
230
+ conv_data = ConversationCreate(
231
+ {%- if cookiecutter.websocket_auth_jwt %}
232
+ user_id=str(user.id),
233
+ {%- endif %}
234
+ title=user_message[:50] if len(user_message) > 50 else user_message,
235
+ )
236
+ conversation = conv_service.create_conversation(conv_data)
237
+ current_conversation_id = str(conversation.id)
238
+ await manager.send_event(
239
+ websocket,
240
+ "conversation_created",
241
+ {"conversation_id": current_conversation_id},
242
+ )
243
+
244
+ # Save user message
245
+ conv_service.add_message(
246
+ current_conversation_id,
247
+ MessageCreate(role="user", content=user_message),
248
+ )
249
+ {%- endif %}
250
+ except Exception as e:
251
+ logger.warning(f"Failed to persist conversation: {e}")
252
+ # Continue without persistence
253
+ {%- elif cookiecutter.enable_conversation_persistence and cookiecutter.use_mongodb %}
254
+
255
+ # Handle conversation persistence (MongoDB)
256
+ conv_service = get_conversation_service()
257
+
258
+ requested_conv_id = data.get("conversation_id")
259
+ if requested_conv_id:
260
+ current_conversation_id = requested_conv_id
261
+ await conv_service.get_conversation(requested_conv_id)
262
+ elif not current_conversation_id:
263
+ conv_data = ConversationCreate(
264
+ {%- if cookiecutter.websocket_auth_jwt %}
265
+ user_id=str(user.id),
266
+ {%- endif %}
267
+ title=user_message[:50] if len(user_message) > 50 else user_message,
268
+ )
269
+ conversation = await conv_service.create_conversation(conv_data)
270
+ current_conversation_id = str(conversation.id)
271
+ await manager.send_event(
272
+ websocket,
273
+ "conversation_created",
274
+ {"conversation_id": current_conversation_id},
275
+ )
276
+
277
+ # Save user message
278
+ await conv_service.add_message(
279
+ current_conversation_id,
280
+ MessageCreate(role="user", content=user_message),
281
+ )
282
+ {%- endif %}
283
+
284
+ await manager.send_event(websocket, "user_prompt", {"content": user_message})
285
+
286
+ try:
287
+ assistant = get_agent()
288
+ model_history = build_message_history(conversation_history)
289
+
290
+ # Use iter() on the underlying PydanticAI agent to stream all events
291
+ async with assistant.agent.iter(
292
+ user_message,
293
+ deps=deps,
294
+ message_history=model_history,
295
+ ) as agent_run:
296
+ async for node in agent_run:
297
+ if Agent.is_user_prompt_node(node):
298
+ await manager.send_event(
299
+ websocket,
300
+ "user_prompt_processed",
301
+ {"prompt": node.user_prompt},
302
+ )
303
+
304
+ elif Agent.is_model_request_node(node):
305
+ await manager.send_event(websocket, "model_request_start", {})
306
+
307
+ async with node.stream(agent_run.ctx) as request_stream:
308
+ async for event in request_stream:
309
+ if isinstance(event, PartStartEvent):
310
+ await manager.send_event(
311
+ websocket,
312
+ "part_start",
313
+ {
314
+ "index": event.index,
315
+ "part_type": type(event.part).__name__,
316
+ },
317
+ )
318
+ # Send initial content from TextPart if present
319
+ if isinstance(event.part, TextPart) and event.part.content:
320
+ await manager.send_event(
321
+ websocket,
322
+ "text_delta",
323
+ {
324
+ "index": event.index,
325
+ "content": event.part.content,
326
+ },
327
+ )
328
+
329
+ elif isinstance(event, PartDeltaEvent):
330
+ if isinstance(event.delta, TextPartDelta):
331
+ await manager.send_event(
332
+ websocket,
333
+ "text_delta",
334
+ {
335
+ "index": event.index,
336
+ "content": event.delta.content_delta,
337
+ },
338
+ )
339
+ elif isinstance(event.delta, ToolCallPartDelta):
340
+ await manager.send_event(
341
+ websocket,
342
+ "tool_call_delta",
343
+ {
344
+ "index": event.index,
345
+ "args_delta": event.delta.args_delta,
346
+ },
347
+ )
348
+
349
+ elif isinstance(event, FinalResultEvent):
350
+ await manager.send_event(
351
+ websocket,
352
+ "final_result_start",
353
+ {"tool_name": event.tool_name},
354
+ )
355
+
356
+ elif Agent.is_call_tools_node(node):
357
+ await manager.send_event(websocket, "call_tools_start", {})
358
+
359
+ async with node.stream(agent_run.ctx) as handle_stream:
360
+ async for event in handle_stream:
361
+ if isinstance(event, FunctionToolCallEvent):
362
+ await manager.send_event(
363
+ websocket,
364
+ "tool_call",
365
+ {
366
+ "tool_name": event.part.tool_name,
367
+ "args": event.part.args,
368
+ "tool_call_id": event.part.tool_call_id,
369
+ },
370
+ )
371
+
372
+ elif isinstance(event, FunctionToolResultEvent):
373
+ await manager.send_event(
374
+ websocket,
375
+ "tool_result",
376
+ {
377
+ "tool_call_id": event.tool_call_id,
378
+ "content": str(event.result.content),
379
+ },
380
+ )
381
+
382
+ elif Agent.is_end_node(node) and agent_run.result is not None:
383
+ await manager.send_event(
384
+ websocket,
385
+ "final_result",
386
+ {"output": agent_run.result.output},
387
+ )
388
+
389
+ # Update conversation history
390
+ conversation_history.append({"role": "user", "content": user_message})
391
+ if agent_run.result:
392
+ conversation_history.append(
393
+ {"role": "assistant", "content": agent_run.result.output}
394
+ )
395
+
396
+ {%- if cookiecutter.enable_conversation_persistence and (cookiecutter.use_postgresql or cookiecutter.use_sqlite) %}
397
+
398
+ # Save assistant response to database
399
+ if current_conversation_id and agent_run.result:
400
+ try:
401
+ {%- if cookiecutter.use_postgresql %}
402
+ async with get_db_context() as db:
403
+ conv_service = get_conversation_service(db)
404
+ await conv_service.add_message(
405
+ UUID(current_conversation_id),
406
+ MessageCreate(
407
+ role="assistant",
408
+ content=agent_run.result.output,
409
+ model_name=assistant.model_name if hasattr(assistant, "model_name") else None,
410
+ ),
411
+ )
412
+ {%- else %}
413
+ with get_db_session() as db:
414
+ conv_service = get_conversation_service(db)
415
+ conv_service.add_message(
416
+ current_conversation_id,
417
+ MessageCreate(
418
+ role="assistant",
419
+ content=agent_run.result.output,
420
+ model_name=assistant.model_name if hasattr(assistant, "model_name") else None,
421
+ ),
422
+ )
423
+ {%- endif %}
424
+ except Exception as e:
425
+ logger.warning(f"Failed to persist assistant response: {e}")
426
+ {%- elif cookiecutter.enable_conversation_persistence and cookiecutter.use_mongodb %}
427
+
428
+ # Save assistant response to database
429
+ if current_conversation_id and agent_run.result:
430
+ try:
431
+ await conv_service.add_message(
432
+ current_conversation_id,
433
+ MessageCreate(
434
+ role="assistant",
435
+ content=agent_run.result.output,
436
+ model_name=assistant.model_name if hasattr(assistant, "model_name") else None,
437
+ ),
438
+ )
439
+ except Exception as e:
440
+ logger.warning(f"Failed to persist assistant response: {e}")
441
+ {%- endif %}
442
+
443
+ await manager.send_event(websocket, "complete", {
444
+ {%- if cookiecutter.enable_conversation_persistence and cookiecutter.use_database %}
445
+ "conversation_id": current_conversation_id,
446
+ {%- endif %}
447
+ })
448
+
449
+ except WebSocketDisconnect:
450
+ # Client disconnected during processing - this is normal
451
+ logger.info("Client disconnected during agent processing")
452
+ break
453
+ except Exception as e:
454
+ logger.exception(f"Error processing agent request: {e}")
455
+ # Try to send error, but don't fail if connection is closed
456
+ await manager.send_event(websocket, "error", {"message": str(e)})
457
+
458
+ except WebSocketDisconnect:
459
+ pass # Normal disconnect
460
+ finally:
461
+ manager.disconnect(websocket)
462
+ {%- elif cookiecutter.enable_ai_agent and cookiecutter.use_langchain %}
463
+ """AI Agent WebSocket routes with streaming support (LangChain)."""
464
+
465
+ import logging
466
+ from typing import Any
467
+ {%- if cookiecutter.enable_conversation_persistence and cookiecutter.use_database %}
468
+ from datetime import datetime, UTC
469
+ {%- if cookiecutter.use_postgresql %}
470
+ from uuid import UUID
471
+ {%- endif %}
472
+ {%- endif %}
473
+
474
+ from fastapi import APIRouter, WebSocket, WebSocketDisconnect{%- if cookiecutter.websocket_auth_jwt %}, Depends{%- endif %}{%- if cookiecutter.websocket_auth_api_key %}, Query{%- endif %}
475
+
476
+ from langchain.messages import AIMessage, AIMessageChunk, HumanMessage, SystemMessage, ToolMessage
477
+
478
+ from app.agents.langchain_assistant import AgentContext, get_agent
479
+ {%- if cookiecutter.websocket_auth_jwt %}
480
+ from app.api.deps import get_current_user_ws
481
+ from app.db.models.user import User
482
+ {%- endif %}
483
+ {%- if cookiecutter.websocket_auth_api_key %}
484
+ from app.core.config import settings
485
+ {%- endif %}
486
+ {%- if cookiecutter.enable_conversation_persistence and (cookiecutter.use_postgresql or cookiecutter.use_sqlite) %}
487
+ from app.db.session import get_db_context
488
+ from app.api.deps import ConversationSvc, get_conversation_service
489
+ from app.schemas.conversation import ConversationCreate, MessageCreate, ToolCallCreate, ToolCallComplete
490
+ {%- elif cookiecutter.enable_conversation_persistence and cookiecutter.use_mongodb %}
491
+ from app.api.deps import ConversationSvc, get_conversation_service
492
+ from app.schemas.conversation import ConversationCreate, MessageCreate, ToolCallCreate, ToolCallComplete
493
+ {%- endif %}
494
+
495
+ logger = logging.getLogger(__name__)
496
+
497
+ router = APIRouter()
498
+
499
+
500
+ class AgentConnectionManager:
501
+ """WebSocket connection manager for AI agent."""
502
+
503
+ def __init__(self) -> None:
504
+ self.active_connections: list[WebSocket] = []
505
+
506
+ async def connect(self, websocket: WebSocket) -> None:
507
+ """Accept and store a new WebSocket connection."""
508
+ await websocket.accept()
509
+ self.active_connections.append(websocket)
510
+ logger.info(f"Agent WebSocket connected. Total connections: {len(self.active_connections)}")
511
+
512
+ def disconnect(self, websocket: WebSocket) -> None:
513
+ """Remove a WebSocket connection."""
514
+ if websocket in self.active_connections:
515
+ self.active_connections.remove(websocket)
516
+ logger.info(f"Agent WebSocket disconnected. Total connections: {len(self.active_connections)}")
517
+
518
+ async def send_event(self, websocket: WebSocket, event_type: str, data: Any) -> bool:
519
+ """Send a JSON event to a specific WebSocket client.
520
+
521
+ Returns True if sent successfully, False if connection is closed.
522
+ """
523
+ try:
524
+ await websocket.send_json({"type": event_type, "data": data})
525
+ return True
526
+ except (WebSocketDisconnect, RuntimeError):
527
+ # Connection already closed
528
+ return False
529
+
530
+
531
+ manager = AgentConnectionManager()
532
+
533
+
534
+ def build_message_history(
535
+ history: list[dict[str, str]]
536
+ ) -> list[HumanMessage | AIMessage | SystemMessage]:
537
+ """Convert conversation history to LangChain message format."""
538
+ messages: list[HumanMessage | AIMessage | SystemMessage] = []
539
+
540
+ for msg in history:
541
+ if msg["role"] == "user":
542
+ messages.append(HumanMessage(content=msg["content"]))
543
+ elif msg["role"] == "assistant":
544
+ messages.append(AIMessage(content=msg["content"]))
545
+ elif msg["role"] == "system":
546
+ messages.append(SystemMessage(content=msg["content"]))
547
+
548
+ return messages
549
+
550
+ {%- if cookiecutter.websocket_auth_api_key %}
551
+
552
+
553
+ async def verify_api_key(api_key: str) -> bool:
554
+ """Verify the API key for WebSocket authentication."""
555
+ return api_key == settings.API_KEY
556
+ {%- endif %}
557
+
558
+
559
+ @router.websocket("/ws/agent")
560
+ async def agent_websocket(
561
+ websocket: WebSocket,
562
+ {%- if cookiecutter.websocket_auth_jwt %}
563
+ user: User = Depends(get_current_user_ws),
564
+ {%- elif cookiecutter.websocket_auth_api_key %}
565
+ api_key: str = Query(..., alias="api_key"),
566
+ {%- endif %}
567
+ ) -> None:
568
+ """WebSocket endpoint for AI agent with streaming support.
569
+
570
+ Uses LangChain stream() to stream agent events including:
571
+ - user_prompt: When user input is received
572
+ - text_delta: Streaming text from the model
573
+ - tool_call: When a tool is called
574
+ - tool_result: When a tool returns a result
575
+ - final_result: When the final result is ready
576
+ - complete: When processing is complete
577
+ - error: When an error occurs
578
+
579
+ Expected input message format:
580
+ {
581
+ "message": "user message here",
582
+ "history": [{"role": "user|assistant|system", "content": "..."}]{% if cookiecutter.enable_conversation_persistence and cookiecutter.use_database %},
583
+ "conversation_id": "optional-uuid-to-continue-existing-conversation"{% endif %}
584
+ }
585
+ {%- if cookiecutter.websocket_auth_jwt %}
586
+
587
+ Authentication: Requires a valid JWT token passed as a query parameter or header.
588
+ {%- elif cookiecutter.websocket_auth_api_key %}
589
+
590
+ Authentication: Requires a valid API key passed as 'api_key' query parameter.
591
+ Example: ws://localhost:{{ cookiecutter.backend_port }}/api/v1/ws/agent?api_key=your-api-key
592
+ {%- endif %}
593
+ {%- if cookiecutter.enable_conversation_persistence and cookiecutter.use_database %}
594
+
595
+ Persistence: Set 'conversation_id' to continue an existing conversation.
596
+ If not provided, a new conversation is created. The conversation_id is
597
+ returned in the 'conversation_created' event.
598
+ {%- endif %}
599
+ """
600
+ {%- if cookiecutter.websocket_auth_api_key %}
601
+ # Verify API key before accepting connection
602
+ if not await verify_api_key(api_key):
603
+ await websocket.close(code=4001, reason="Invalid API key")
604
+ return
605
+ {%- endif %}
606
+
607
+ await manager.connect(websocket)
608
+
609
+ # Conversation state per connection
610
+ conversation_history: list[dict[str, str]] = []
611
+ context: AgentContext = {}
612
+ {%- if cookiecutter.websocket_auth_jwt %}
613
+ context["user_id"] = str(user.id) if user else None
614
+ context["user_name"] = user.email if user else None
615
+ {%- endif %}
616
+ {%- if cookiecutter.enable_conversation_persistence and cookiecutter.use_database %}
617
+ current_conversation_id: str | None = None
618
+ {%- endif %}
619
+
620
+ try:
621
+ while True:
622
+ # Receive user message
623
+ data = await websocket.receive_json()
624
+ user_message = data.get("message", "")
625
+ # Optionally accept history from client (or use server-side tracking)
626
+ if "history" in data:
627
+ conversation_history = data["history"]
628
+
629
+ if not user_message:
630
+ await manager.send_event(websocket, "error", {"message": "Empty message"})
631
+ continue
632
+
633
+ {%- if cookiecutter.enable_conversation_persistence and (cookiecutter.use_postgresql or cookiecutter.use_sqlite) %}
634
+
635
+ # Handle conversation persistence
636
+ try:
637
+ {%- if cookiecutter.use_postgresql %}
638
+ async with get_db_context() as db:
639
+ conv_service = get_conversation_service(db)
640
+
641
+ # Get or create conversation
642
+ requested_conv_id = data.get("conversation_id")
643
+ if requested_conv_id:
644
+ current_conversation_id = requested_conv_id
645
+ # Verify conversation exists
646
+ await conv_service.get_conversation(UUID(requested_conv_id))
647
+ elif not current_conversation_id:
648
+ # Create new conversation
649
+ conv_data = ConversationCreate(
650
+ {%- if cookiecutter.websocket_auth_jwt %}
651
+ user_id=user.id,
652
+ {%- endif %}
653
+ title=user_message[:50] if len(user_message) > 50 else user_message,
654
+ )
655
+ conversation = await conv_service.create_conversation(conv_data)
656
+ current_conversation_id = str(conversation.id)
657
+ await manager.send_event(
658
+ websocket,
659
+ "conversation_created",
660
+ {"conversation_id": current_conversation_id},
661
+ )
662
+
663
+ # Save user message
664
+ await conv_service.add_message(
665
+ UUID(current_conversation_id),
666
+ MessageCreate(role="user", content=user_message),
667
+ )
668
+ {%- else %}
669
+ with get_db_session() as db:
670
+ conv_service = get_conversation_service(db)
671
+
672
+ # Get or create conversation
673
+ requested_conv_id = data.get("conversation_id")
674
+ if requested_conv_id:
675
+ current_conversation_id = requested_conv_id
676
+ conv_service.get_conversation(requested_conv_id)
677
+ elif not current_conversation_id:
678
+ # Create new conversation
679
+ conv_data = ConversationCreate(
680
+ {%- if cookiecutter.websocket_auth_jwt %}
681
+ user_id=str(user.id),
682
+ {%- endif %}
683
+ title=user_message[:50] if len(user_message) > 50 else user_message,
684
+ )
685
+ conversation = conv_service.create_conversation(conv_data)
686
+ current_conversation_id = str(conversation.id)
687
+ await manager.send_event(
688
+ websocket,
689
+ "conversation_created",
690
+ {"conversation_id": current_conversation_id},
691
+ )
692
+
693
+ # Save user message
694
+ conv_service.add_message(
695
+ current_conversation_id,
696
+ MessageCreate(role="user", content=user_message),
697
+ )
698
+ {%- endif %}
699
+ except Exception as e:
700
+ logger.warning(f"Failed to persist conversation: {e}")
701
+ # Continue without persistence
702
+ {%- elif cookiecutter.enable_conversation_persistence and cookiecutter.use_mongodb %}
703
+
704
+ # Handle conversation persistence (MongoDB)
705
+ conv_service = get_conversation_service()
706
+
707
+ requested_conv_id = data.get("conversation_id")
708
+ if requested_conv_id:
709
+ current_conversation_id = requested_conv_id
710
+ await conv_service.get_conversation(requested_conv_id)
711
+ elif not current_conversation_id:
712
+ conv_data = ConversationCreate(
713
+ {%- if cookiecutter.websocket_auth_jwt %}
714
+ user_id=str(user.id),
715
+ {%- endif %}
716
+ title=user_message[:50] if len(user_message) > 50 else user_message,
717
+ )
718
+ conversation = await conv_service.create_conversation(conv_data)
719
+ current_conversation_id = str(conversation.id)
720
+ await manager.send_event(
721
+ websocket,
722
+ "conversation_created",
723
+ {"conversation_id": current_conversation_id},
724
+ )
725
+
726
+ # Save user message
727
+ await conv_service.add_message(
728
+ current_conversation_id,
729
+ MessageCreate(role="user", content=user_message),
730
+ )
731
+ {%- endif %}
732
+
733
+ await manager.send_event(websocket, "user_prompt", {"content": user_message})
734
+
735
+ try:
736
+ assistant = get_agent()
737
+ model_history = build_message_history(conversation_history)
738
+ model_history.append(HumanMessage(content=user_message))
739
+
740
+ final_output = ""
741
+ tool_events: list[Any] = []
742
+ seen_tool_call_ids: set[str] = set()
743
+
744
+ await manager.send_event(websocket, "model_request_start", {})
745
+
746
+ for stream_mode, data in assistant.agent.stream(
747
+ {"messages": model_history},
748
+ stream_mode=["messages", "updates"],
749
+ config={"configurable": context} if context else None,
750
+ ):
751
+ if stream_mode == "messages":
752
+ token, metadata = data
753
+
754
+ if isinstance(token, AIMessageChunk):
755
+ if token.content:
756
+ text_content = ""
757
+ if isinstance(token.content, str):
758
+ text_content = token.content
759
+ elif isinstance(token.content, list):
760
+ for block in token.content:
761
+ if isinstance(block, dict) and block.get("type") == "text":
762
+ text_content += block.get("text", "")
763
+ elif isinstance(block, str):
764
+ text_content += block
765
+
766
+ if text_content:
767
+ await manager.send_event(
768
+ websocket,
769
+ "text_delta",
770
+ {"content": text_content},
771
+ )
772
+ final_output += text_content
773
+
774
+ if token.tool_call_chunks:
775
+ for tc_chunk in token.tool_call_chunks:
776
+ tc_id = tc_chunk.get("id")
777
+ tc_name = tc_chunk.get("name")
778
+ if tc_id and tc_name and tc_id not in seen_tool_call_ids:
779
+ seen_tool_call_ids.add(tc_id)
780
+ await manager.send_event(
781
+ websocket,
782
+ "tool_call",
783
+ {
784
+ "tool_name": tc_name,
785
+ "args": {},
786
+ "tool_call_id": tc_id,
787
+ },
788
+ )
789
+
790
+ elif stream_mode == "updates":
791
+ for node_name, update in data.items():
792
+ if node_name == "tools":
793
+ for msg in update.get("messages", []):
794
+ if isinstance(msg, ToolMessage):
795
+ await manager.send_event(
796
+ websocket,
797
+ "tool_result",
798
+ {
799
+ "tool_call_id": msg.tool_call_id,
800
+ "content": msg.content,
801
+ },
802
+ )
803
+ elif node_name == "model":
804
+ for msg in update.get("messages", []):
805
+ if isinstance(msg, AIMessage) and msg.tool_calls:
806
+ for tc in msg.tool_calls:
807
+ tc_id = tc.get("id", "")
808
+ if tc_id not in seen_tool_call_ids:
809
+ seen_tool_call_ids.add(tc_id)
810
+ tool_events.append(tc)
811
+ await manager.send_event(
812
+ websocket,
813
+ "tool_call",
814
+ {
815
+ "tool_name": tc.get("name", ""),
816
+ "args": tc.get("args", {}),
817
+ "tool_call_id": tc_id,
818
+ },
819
+ )
820
+
821
+ await manager.send_event(
822
+ websocket,
823
+ "final_result",
824
+ {"output": final_output},
825
+ )
826
+
827
+ # Update conversation history
828
+ conversation_history.append({"role": "user", "content": user_message})
829
+ if final_output:
830
+ conversation_history.append(
831
+ {"role": "assistant", "content": final_output}
832
+ )
833
+
834
+ {%- if cookiecutter.enable_conversation_persistence and (cookiecutter.use_postgresql or cookiecutter.use_sqlite) %}
835
+
836
+ # Save assistant response to database
837
+ if current_conversation_id and final_output:
838
+ try:
839
+ {%- if cookiecutter.use_postgresql %}
840
+ async with get_db_context() as db:
841
+ conv_service = get_conversation_service(db)
842
+ await conv_service.add_message(
843
+ UUID(current_conversation_id),
844
+ MessageCreate(
845
+ role="assistant",
846
+ content=final_output,
847
+ model_name=assistant.model_name if hasattr(assistant, "model_name") else None,
848
+ ),
849
+ )
850
+ {%- else %}
851
+ with get_db_session() as db:
852
+ conv_service = get_conversation_service(db)
853
+ conv_service.add_message(
854
+ current_conversation_id,
855
+ MessageCreate(
856
+ role="assistant",
857
+ content=final_output,
858
+ model_name=assistant.model_name if hasattr(assistant, "model_name") else None,
859
+ ),
860
+ )
861
+ {%- endif %}
862
+ except Exception as e:
863
+ logger.warning(f"Failed to persist assistant response: {e}")
864
+ {%- elif cookiecutter.enable_conversation_persistence and cookiecutter.use_mongodb %}
865
+
866
+ # Save assistant response to database
867
+ if current_conversation_id and final_output:
868
+ try:
869
+ await conv_service.add_message(
870
+ current_conversation_id,
871
+ MessageCreate(
872
+ role="assistant",
873
+ content=final_output,
874
+ model_name=assistant.model_name if hasattr(assistant, "model_name") else None,
875
+ ),
876
+ )
877
+ except Exception as e:
878
+ logger.warning(f"Failed to persist assistant response: {e}")
879
+ {%- endif %}
880
+
881
+ await manager.send_event(websocket, "complete", {
882
+ {%- if cookiecutter.enable_conversation_persistence and cookiecutter.use_database %}
883
+ "conversation_id": current_conversation_id,
884
+ {%- endif %}
885
+ })
886
+
887
+ except WebSocketDisconnect:
888
+ # Client disconnected during processing - this is normal
889
+ logger.info("Client disconnected during agent processing")
890
+ break
891
+ except Exception as e:
892
+ logger.exception(f"Error processing agent request: {e}")
893
+ # Try to send error, but don't fail if connection is closed
894
+ await manager.send_event(websocket, "error", {"message": str(e)})
895
+
896
+ except WebSocketDisconnect:
897
+ pass # Normal disconnect
898
+ finally:
899
+ manager.disconnect(websocket)
900
+ {%- else %}
901
+ """AI Agent routes - not configured."""
902
+ {%- endif %}