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