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