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.
- fastapi_fullstack-0.1.7.dist-info/METADATA +739 -0
- fastapi_fullstack-0.1.7.dist-info/RECORD +241 -0
- fastapi_fullstack-0.1.7.dist-info/WHEEL +4 -0
- fastapi_fullstack-0.1.7.dist-info/entry_points.txt +2 -0
- fastapi_fullstack-0.1.7.dist-info/licenses/LICENSE +21 -0
- fastapi_gen/__init__.py +3 -0
- fastapi_gen/cli.py +442 -0
- fastapi_gen/config.py +356 -0
- fastapi_gen/generator.py +207 -0
- fastapi_gen/prompts.py +874 -0
- fastapi_gen/template/VARIABLES.md +276 -0
- fastapi_gen/template/cookiecutter.json +93 -0
- fastapi_gen/template/hooks/post_gen_project.py +355 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.env.prod.example +56 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.github/workflows/ci.yml +150 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.gitignore +109 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/AGENTS.md +55 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md +99 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/Makefile +315 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/README.md +768 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.dockerignore +60 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.env.example +155 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.pre-commit-config.yaml +32 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/Dockerfile +56 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py +76 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/script.py.mako +30 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/versions/.gitkeep +0 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic.ini +48 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/__init__.py +3 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/admin.py +447 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/__init__.py +23 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/assistant.py +226 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langchain_assistant.py +226 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/prompts.py +10 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/__init__.py +13 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/datetime_tool.py +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/__init__.py +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/deps.py +541 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/exception_handlers.py +98 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/router.py +10 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/__init__.py +9 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/__init__.py +87 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/agent.py +902 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/auth.py +395 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/conversations.py +498 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/health.py +227 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/items.py +275 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/oauth.py +205 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/sessions.py +168 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/users.py +333 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/webhooks.py +477 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/ws.py +46 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/versioning.py +221 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/clients/__init__.py +14 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/clients/redis.py +88 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/__init__.py +117 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/cleanup.py +75 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/example.py +28 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/seed.py +266 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/__init__.py +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/cache.py +23 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/config.py +267 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/csrf.py +153 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/exceptions.py +122 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/logfire_setup.py +101 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/middleware.py +99 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/oauth.py +23 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/rate_limit.py +58 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/sanitize.py +271 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/security.py +102 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/__init__.py +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/base.py +41 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/__init__.py +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/conversation.py +319 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/item.py +96 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/session.py +126 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/user.py +218 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/webhook.py +244 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/session.py +130 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/main.py +334 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/pipelines/__init__.py +9 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/pipelines/base.py +73 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/__init__.py +49 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/base.py +154 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/conversation.py +838 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/item.py +222 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/session.py +318 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/user.py +322 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/webhook.py +358 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/__init__.py +50 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/base.py +57 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/conversation.py +192 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/item.py +52 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/session.py +42 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/token.py +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/user.py +64 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/webhook.py +89 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/__init__.py +38 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/conversation.py +850 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/item.py +246 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/session.py +333 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/user.py +432 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py +561 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/__init__.py +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/celery_app.py +64 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/taskiq_app.py +38 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/__init__.py +25 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/examples.py +106 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/schedules.py +29 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/taskiq_examples.py +92 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/cli/__init__.py +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/cli/commands.py +438 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +180 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/scripts/.gitkeep +0 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/__init__.py +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/__init__.py +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_auth.py +242 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_exceptions.py +151 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_health.py +113 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_items.py +310 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_users.py +253 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/conftest.py +151 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_admin.py +890 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_agents.py +261 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_clients.py +183 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_commands.py +173 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_core.py +143 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_pipelines.py +118 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_repositories.py +181 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_security.py +124 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_services.py +363 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_worker.py +85 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml +242 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.frontend.yml +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml +435 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml +241 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/adding_features.md +132 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/architecture.md +63 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/patterns.md +161 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/testing.md +120 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.dockerignore +40 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.env.example +12 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.gitignore +45 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.prettierignore +19 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.prettierrc +11 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/Dockerfile +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/README.md +648 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/auth.setup.ts +49 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/auth.spec.ts +134 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/chat.spec.ts +207 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/home.spec.ts +73 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/instrumentation.ts +14 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/messages/en.json +84 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/messages/pl.json +84 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/next.config.ts +76 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/package.json +69 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/playwright.config.ts +101 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/postcss.config.mjs +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/layout.tsx +11 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/login/page.tsx +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/register/page.tsx +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/chat/page.tsx +48 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/dashboard/page.tsx +99 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/layout.tsx +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/profile/page.tsx +152 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/auth/callback/page.tsx +113 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/layout.tsx +46 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/page.tsx +73 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/login/route.ts +58 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/logout/route.ts +24 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/me/route.ts +39 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/oauth-callback/route.ts +50 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/refresh/route.ts +54 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/register/route.ts +26 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/[id]/messages/route.ts +41 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/[id]/route.ts +108 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/route.ts +73 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/health/route.ts +21 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/globals.css +323 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/layout.tsx +22 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/providers.tsx +29 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/index.ts +2 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/login-form.tsx +120 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/register-form.tsx +153 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-container.tsx +234 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-input.tsx +72 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/conversation-sidebar.tsx +328 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/copy-button.tsx +46 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/index.ts +11 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/local-conversation-sidebar.tsx +295 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/markdown-content.tsx +167 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-item.tsx +79 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-list.tsx +18 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-call-card.tsx +79 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/icons/google-icon.tsx +32 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/icons/index.ts +3 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/language-switcher.tsx +97 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/header.tsx +65 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/index.ts +2 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/sidebar.tsx +82 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/index.ts +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-provider.tsx +53 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-toggle.tsx +105 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/badge.tsx +35 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/button.test.tsx +75 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/button.tsx +56 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/card.tsx +82 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/index.ts +13 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/input.tsx +21 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/label.tsx +21 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/sheet.tsx +109 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/index.ts +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-auth.ts +97 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-chat.ts +203 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-conversations.ts +181 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-local-chat.ts +165 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-websocket.ts +105 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/i18n.ts +37 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/api-client.ts +90 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/constants.ts +39 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/server-api.ts +78 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/utils.test.ts +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/utils.ts +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/middleware.ts +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/auth-store.test.ts +72 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/auth-store.ts +64 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/chat-sidebar-store.ts +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/chat-store.ts +65 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/conversation-store.ts +76 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/index.ts +9 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/local-chat-store.ts +255 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/sidebar-store.ts +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/theme-store.ts +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/api.ts +27 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/auth.ts +52 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/chat.ts +83 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/conversation.ts +49 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/index.ts +10 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/tsconfig.json +28 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/vitest.config.ts +36 -0
- 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 %}
|