fastapi-fullstack 0.1.7__py3-none-any.whl → 0.1.15__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 → fastapi_fullstack-0.1.15.dist-info}/METADATA +9 -2
- {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/RECORD +71 -55
- fastapi_gen/__init__.py +6 -1
- fastapi_gen/cli.py +9 -0
- fastapi_gen/config.py +154 -2
- fastapi_gen/generator.py +34 -14
- fastapi_gen/prompts.py +172 -31
- fastapi_gen/template/VARIABLES.md +33 -4
- fastapi_gen/template/cookiecutter.json +10 -0
- fastapi_gen/template/hooks/post_gen_project.py +87 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/.env.prod.example +9 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.gitlab-ci.yml +178 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md +3 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/README.md +334 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.env.example +32 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py +10 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/admin.py +1 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/__init__.py +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/crewai_assistant.py +563 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/deepagents_assistant.py +526 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langchain_assistant.py +4 -3
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langgraph_assistant.py +371 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/agent.py +1472 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/oauth.py +3 -7
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/cleanup.py +2 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/seed.py +7 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/config.py +44 -7
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/__init__.py +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/base.py +42 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/conversation.py +262 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/item.py +76 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/session.py +118 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/user.py +158 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/webhook.py +185 -3
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/main.py +29 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/base.py +6 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/session.py +4 -4
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/conversation.py +9 -9
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/session.py +6 -6
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py +7 -7
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/__init__.py +1 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/arq_app.py +165 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/__init__.py +10 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +40 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_metrics.py +53 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_agents.py +2 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml +6 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml +100 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml +39 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.env.example +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-container.tsx +28 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/index.ts +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-item.tsx +22 -4
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-list.tsx +23 -3
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-approval-dialog.tsx +138 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-chat.ts +242 -18
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-local-chat.ts +242 -17
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/constants.ts +1 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/chat.ts +57 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/configmap.yaml +63 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/deployment.yaml +242 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/ingress.yaml +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/kustomization.yaml +28 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/namespace.yaml +12 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/secret.yaml +59 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/service.yaml +23 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/nginx.conf +225 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/ssl/.gitkeep +18 -0
- {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/WHEEL +0 -0
- {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/entry_points.txt +0 -0
- {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -141,19 +141,15 @@ async def google_callback(request: Request, user_service: UserSvc):
|
|
|
141
141
|
|
|
142
142
|
|
|
143
143
|
@router.get("/google/callback")
|
|
144
|
-
def google_callback(request: Request, user_service: UserSvc):
|
|
144
|
+
async def google_callback(request: Request, user_service: UserSvc):
|
|
145
145
|
"""Handle Google OAuth2 callback.
|
|
146
146
|
|
|
147
147
|
Creates a new user if one doesn't exist with the Google email,
|
|
148
148
|
or returns tokens for existing user. Redirects to frontend with tokens.
|
|
149
149
|
"""
|
|
150
|
-
import asyncio
|
|
151
|
-
|
|
152
150
|
try:
|
|
153
|
-
#
|
|
154
|
-
|
|
155
|
-
token = loop.run_until_complete(oauth.google.authorize_access_token(request))
|
|
156
|
-
loop.close()
|
|
151
|
+
# OAuth token exchange is async
|
|
152
|
+
token = await oauth.google.authorize_access_token(request)
|
|
157
153
|
|
|
158
154
|
user_info = token.get("userinfo")
|
|
159
155
|
|
|
@@ -6,7 +6,7 @@ This command is useful for maintenance tasks.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
|
-
from datetime import datetime, timedelta
|
|
9
|
+
from datetime import UTC, datetime, timedelta
|
|
10
10
|
|
|
11
11
|
import click
|
|
12
12
|
|
|
@@ -26,7 +26,7 @@ def cleanup(days: int, dry_run: bool, force: bool) -> None:
|
|
|
26
26
|
project cmd cleanup --days 30 --dry-run
|
|
27
27
|
project cmd cleanup --days 7 --force
|
|
28
28
|
"""
|
|
29
|
-
cutoff_date = datetime.
|
|
29
|
+
cutoff_date = datetime.now(UTC) - timedelta(days=days)
|
|
30
30
|
|
|
31
31
|
if dry_run:
|
|
32
32
|
info(f"[DRY RUN] Would delete records older than {cutoff_date}")
|
|
@@ -7,13 +7,16 @@ This command is useful for development and testing.
|
|
|
7
7
|
Uses random data generation - install faker for better data:
|
|
8
8
|
uv add faker --group dev
|
|
9
9
|
"""
|
|
10
|
-
|
|
10
|
+
{% if cookiecutter.use_postgresql %}
|
|
11
11
|
import asyncio
|
|
12
|
+
{% endif %}
|
|
12
13
|
import random
|
|
13
14
|
import string
|
|
14
15
|
|
|
15
16
|
import click
|
|
17
|
+
{% if cookiecutter.use_jwt or cookiecutter.include_example_crud %}
|
|
16
18
|
from sqlalchemy import delete, select
|
|
19
|
+
{% endif %}
|
|
17
20
|
|
|
18
21
|
from app.commands import command, info, success, warning
|
|
19
22
|
|
|
@@ -109,7 +112,9 @@ def seed(
|
|
|
109
112
|
{%- endif %}
|
|
110
113
|
return
|
|
111
114
|
|
|
112
|
-
{%- if cookiecutter.
|
|
115
|
+
{%- if not cookiecutter.use_jwt and not cookiecutter.include_example_crud %}
|
|
116
|
+
info("No entities configured to seed. Enable JWT users or example CRUD to use this command.")
|
|
117
|
+
{%- elif cookiecutter.use_postgresql %}
|
|
113
118
|
from app.db.session import async_session_maker
|
|
114
119
|
{%- if cookiecutter.use_jwt %}
|
|
115
120
|
from app.db.models.user import User
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"""Application configuration using Pydantic BaseSettings."""
|
|
2
|
-
{% if cookiecutter.use_database -%}
|
|
2
|
+
{% if cookiecutter.use_database or cookiecutter.enable_redis -%}
|
|
3
3
|
# ruff: noqa: I001 - Imports structured for Jinja2 template conditionals
|
|
4
4
|
{% endif %}
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Literal
|
|
7
7
|
|
|
8
|
-
{% if cookiecutter.use_database -%}
|
|
8
|
+
{% if cookiecutter.use_database or cookiecutter.enable_redis -%}
|
|
9
9
|
from pydantic import computed_field, field_validator{% if cookiecutter.use_jwt or cookiecutter.use_api_key or cookiecutter.enable_cors %}, ValidationInfo{% endif %}
|
|
10
10
|
{% else -%}
|
|
11
11
|
from pydantic import field_validator{% if cookiecutter.use_jwt or cookiecutter.use_api_key or cookiecutter.enable_cors %}, ValidationInfo{% endif %}
|
|
@@ -109,13 +109,10 @@ class Settings(BaseSettings):
|
|
|
109
109
|
return f"sqlite:///{self.SQLITE_PATH}"
|
|
110
110
|
{%- endif %}
|
|
111
111
|
|
|
112
|
-
{%- if cookiecutter.use_jwt %}
|
|
112
|
+
{%- if cookiecutter.use_jwt or (cookiecutter.enable_admin_panel and cookiecutter.admin_require_auth) or cookiecutter.enable_oauth %}
|
|
113
113
|
|
|
114
|
-
# === Auth (JWT) ===
|
|
114
|
+
# === Auth (SECRET_KEY for JWT/Session/Admin) ===
|
|
115
115
|
SECRET_KEY: str = "change-me-in-production-use-openssl-rand-hex-32"
|
|
116
|
-
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 # 30 minutes
|
|
117
|
-
REFRESH_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 days
|
|
118
|
-
ALGORITHM: str = "HS256"
|
|
119
116
|
|
|
120
117
|
@field_validator("SECRET_KEY")
|
|
121
118
|
@classmethod
|
|
@@ -133,6 +130,14 @@ class Settings(BaseSettings):
|
|
|
133
130
|
return v
|
|
134
131
|
{%- endif %}
|
|
135
132
|
|
|
133
|
+
{%- if cookiecutter.use_jwt %}
|
|
134
|
+
|
|
135
|
+
# === JWT Settings ===
|
|
136
|
+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 # 30 minutes
|
|
137
|
+
REFRESH_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 days
|
|
138
|
+
ALGORITHM: str = "HS256"
|
|
139
|
+
{%- endif %}
|
|
140
|
+
|
|
136
141
|
{%- if cookiecutter.enable_oauth_google %}
|
|
137
142
|
|
|
138
143
|
# === OAuth2 (Google) ===
|
|
@@ -198,12 +203,28 @@ class Settings(BaseSettings):
|
|
|
198
203
|
TASKIQ_RESULT_BACKEND: str = "redis://localhost:6379/1"
|
|
199
204
|
{%- endif %}
|
|
200
205
|
|
|
206
|
+
{%- if cookiecutter.use_arq %}
|
|
207
|
+
|
|
208
|
+
# === ARQ (Async Redis Queue) ===
|
|
209
|
+
ARQ_REDIS_HOST: str = "localhost"
|
|
210
|
+
ARQ_REDIS_PORT: int = 6379
|
|
211
|
+
ARQ_REDIS_PASSWORD: str | None = None
|
|
212
|
+
ARQ_REDIS_DB: int = 2
|
|
213
|
+
{%- endif %}
|
|
214
|
+
|
|
201
215
|
{%- if cookiecutter.enable_sentry %}
|
|
202
216
|
|
|
203
217
|
# === Sentry ===
|
|
204
218
|
SENTRY_DSN: str | None = None
|
|
205
219
|
{%- endif %}
|
|
206
220
|
|
|
221
|
+
{%- if cookiecutter.enable_prometheus %}
|
|
222
|
+
|
|
223
|
+
# === Prometheus ===
|
|
224
|
+
PROMETHEUS_METRICS_PATH: str = "/metrics"
|
|
225
|
+
PROMETHEUS_INCLUDE_IN_SCHEMA: bool = False
|
|
226
|
+
{%- endif %}
|
|
227
|
+
|
|
207
228
|
{%- if cookiecutter.enable_file_storage %}
|
|
208
229
|
|
|
209
230
|
# === File Storage (S3/MinIO) ===
|
|
@@ -240,6 +261,22 @@ class Settings(BaseSettings):
|
|
|
240
261
|
LANGCHAIN_PROJECT: str = "{{ cookiecutter.project_slug }}"
|
|
241
262
|
LANGCHAIN_ENDPOINT: str = "https://api.smith.langchain.com"
|
|
242
263
|
{%- endif %}
|
|
264
|
+
{%- if cookiecutter.use_deepagents %}
|
|
265
|
+
|
|
266
|
+
# === DeepAgents Configuration ===
|
|
267
|
+
# Skills paths (comma-separated, relative to backend dir)
|
|
268
|
+
DEEPAGENTS_SKILLS_PATHS: str | None = None # e.g. "/skills/user/,/skills/project/"
|
|
269
|
+
# Enable built-in tools
|
|
270
|
+
DEEPAGENTS_ENABLE_FILESYSTEM: bool = True # ls, read_file, write_file, edit_file, glob, grep
|
|
271
|
+
DEEPAGENTS_ENABLE_EXECUTE: bool = False # shell execution (disabled by default for security)
|
|
272
|
+
DEEPAGENTS_ENABLE_TODOS: bool = True # write_todos tool
|
|
273
|
+
DEEPAGENTS_ENABLE_SUBAGENTS: bool = True # task tool for spawning subagents
|
|
274
|
+
# Human-in-the-loop: tools requiring approval (comma-separated)
|
|
275
|
+
# e.g. "write_file,edit_file,execute" or "all" for all tools
|
|
276
|
+
DEEPAGENTS_INTERRUPT_TOOLS: str | None = None
|
|
277
|
+
# Allowed decisions for interrupted tools: approve,edit,reject
|
|
278
|
+
DEEPAGENTS_ALLOWED_DECISIONS: str = "approve,edit,reject"
|
|
279
|
+
{%- endif %}
|
|
243
280
|
{%- endif %}
|
|
244
281
|
|
|
245
282
|
{%- if cookiecutter.enable_cors %}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
"""Database module."""
|
|
2
2
|
{%- if cookiecutter.use_postgresql or cookiecutter.use_sqlite %}
|
|
3
|
+
{%- if cookiecutter.use_sqlalchemy %}
|
|
3
4
|
|
|
4
5
|
from app.db.base import Base
|
|
5
6
|
|
|
6
7
|
__all__ = ["Base"]
|
|
8
|
+
{%- else %}
|
|
9
|
+
# SQLModel uses SQLModel class directly as base, no separate Base class needed
|
|
10
|
+
from app.db.base import TimestampMixin
|
|
11
|
+
|
|
12
|
+
__all__ = ["TimestampMixin"]
|
|
13
|
+
{%- endif %}
|
|
7
14
|
{%- endif %}
|
|
@@ -1,4 +1,45 @@
|
|
|
1
1
|
{%- if cookiecutter.use_postgresql or cookiecutter.use_sqlite %}
|
|
2
|
+
{%- if cookiecutter.use_sqlmodel %}
|
|
3
|
+
"""SQLModel base model."""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import Column, DateTime, MetaData, func
|
|
8
|
+
from sqlmodel import Field, SQLModel
|
|
9
|
+
|
|
10
|
+
# Naming convention for database constraints and indexes
|
|
11
|
+
# This ensures consistent naming across all migrations
|
|
12
|
+
NAMING_CONVENTION = {
|
|
13
|
+
"ix": "%(column_0_label)s_idx",
|
|
14
|
+
"uq": "%(table_name)s_%(column_0_name)s_key",
|
|
15
|
+
"ck": "%(table_name)s_%(constraint_name)s_check",
|
|
16
|
+
"fk": "%(table_name)s_%(column_0_name)s_fkey",
|
|
17
|
+
"pk": "%(table_name)s_pkey",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# Apply naming convention to SQLModel metadata
|
|
21
|
+
SQLModel.metadata.naming_convention = NAMING_CONVENTION
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TimestampMixin(SQLModel):
|
|
25
|
+
"""Mixin for created_at and updated_at timestamps."""
|
|
26
|
+
|
|
27
|
+
created_at: datetime = Field(
|
|
28
|
+
sa_column=Column(
|
|
29
|
+
DateTime(timezone=True),
|
|
30
|
+
server_default=func.now(),
|
|
31
|
+
nullable=False,
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
updated_at: datetime | None = Field(
|
|
35
|
+
default=None,
|
|
36
|
+
sa_column=Column(
|
|
37
|
+
DateTime(timezone=True),
|
|
38
|
+
onupdate=func.now(),
|
|
39
|
+
nullable=True,
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
{%- else %}
|
|
2
43
|
"""SQLAlchemy base model."""
|
|
3
44
|
|
|
4
45
|
from datetime import datetime
|
|
@@ -36,6 +77,7 @@ class TimestampMixin:
|
|
|
36
77
|
onupdate=func.now(),
|
|
37
78
|
nullable=True,
|
|
38
79
|
)
|
|
80
|
+
{%- endif %}
|
|
39
81
|
{%- else %}
|
|
40
82
|
"""Database base - not using SQLAlchemy."""
|
|
41
83
|
{%- endif %}
|
|
@@ -1,4 +1,151 @@
|
|
|
1
|
-
{%- if cookiecutter.enable_conversation_persistence and cookiecutter.use_postgresql %}
|
|
1
|
+
{%- if cookiecutter.enable_conversation_persistence and cookiecutter.use_postgresql and cookiecutter.use_sqlmodel %}
|
|
2
|
+
"""Conversation and message models for AI chat persistence using SQLModel."""
|
|
3
|
+
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text
|
|
9
|
+
from sqlalchemy.dialects.postgresql import JSONB, UUID as PG_UUID
|
|
10
|
+
from sqlmodel import Field, Relationship, SQLModel
|
|
11
|
+
|
|
12
|
+
from app.db.base import TimestampMixin
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Conversation(TimestampMixin, SQLModel, table=True):
|
|
16
|
+
"""Conversation model - groups messages in a chat session.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
id: Unique conversation identifier
|
|
20
|
+
user_id: Optional user who owns this conversation (if auth enabled)
|
|
21
|
+
title: Auto-generated or user-defined title
|
|
22
|
+
is_archived: Whether the conversation is archived
|
|
23
|
+
messages: List of messages in this conversation
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
__tablename__ = "conversations"
|
|
27
|
+
|
|
28
|
+
id: uuid.UUID = Field(
|
|
29
|
+
default_factory=uuid.uuid4,
|
|
30
|
+
sa_column=Column(PG_UUID(as_uuid=True), primary_key=True),
|
|
31
|
+
)
|
|
32
|
+
{%- if cookiecutter.use_jwt %}
|
|
33
|
+
user_id: uuid.UUID | None = Field(
|
|
34
|
+
default=None,
|
|
35
|
+
sa_column=Column(
|
|
36
|
+
PG_UUID(as_uuid=True),
|
|
37
|
+
ForeignKey("users.id", ondelete="CASCADE"),
|
|
38
|
+
nullable=True,
|
|
39
|
+
index=True,
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
{%- endif %}
|
|
43
|
+
title: str | None = Field(default=None, max_length=255)
|
|
44
|
+
is_archived: bool = Field(default=False)
|
|
45
|
+
|
|
46
|
+
# Relationships
|
|
47
|
+
messages: list["Message"] = Relationship(
|
|
48
|
+
back_populates="conversation",
|
|
49
|
+
sa_relationship_kwargs={"cascade": "all, delete-orphan", "order_by": "Message.created_at"},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def __repr__(self) -> str:
|
|
53
|
+
return f"<Conversation(id={self.id}, title={self.title})>"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Message(TimestampMixin, SQLModel, table=True):
|
|
57
|
+
"""Message model - individual message in a conversation.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
id: Unique message identifier
|
|
61
|
+
conversation_id: The conversation this message belongs to
|
|
62
|
+
role: Message role (user, assistant, system)
|
|
63
|
+
content: Message text content
|
|
64
|
+
model_name: AI model used (for assistant messages)
|
|
65
|
+
tokens_used: Token count for this message
|
|
66
|
+
tool_calls: List of tool calls made in this message
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
__tablename__ = "messages"
|
|
70
|
+
|
|
71
|
+
id: uuid.UUID = Field(
|
|
72
|
+
default_factory=uuid.uuid4,
|
|
73
|
+
sa_column=Column(PG_UUID(as_uuid=True), primary_key=True),
|
|
74
|
+
)
|
|
75
|
+
conversation_id: uuid.UUID = Field(
|
|
76
|
+
sa_column=Column(
|
|
77
|
+
PG_UUID(as_uuid=True),
|
|
78
|
+
ForeignKey("conversations.id", ondelete="CASCADE"),
|
|
79
|
+
nullable=False,
|
|
80
|
+
index=True,
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
role: str = Field(max_length=20) # user, assistant, system
|
|
84
|
+
content: str = Field(sa_column=Column(Text, nullable=False))
|
|
85
|
+
model_name: str | None = Field(default=None, max_length=100)
|
|
86
|
+
tokens_used: int | None = Field(default=None)
|
|
87
|
+
|
|
88
|
+
# Relationships
|
|
89
|
+
conversation: "Conversation" = Relationship(back_populates="messages")
|
|
90
|
+
tool_calls: list["ToolCall"] = Relationship(
|
|
91
|
+
back_populates="message",
|
|
92
|
+
sa_relationship_kwargs={"cascade": "all, delete-orphan", "order_by": "ToolCall.started_at"},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def __repr__(self) -> str:
|
|
96
|
+
return f"<Message(id={self.id}, role={self.role})>"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ToolCall(SQLModel, table=True):
|
|
100
|
+
"""ToolCall model - record of a tool invocation.
|
|
101
|
+
|
|
102
|
+
Attributes:
|
|
103
|
+
id: Unique tool call identifier
|
|
104
|
+
message_id: The assistant message that triggered this call
|
|
105
|
+
tool_call_id: External ID from PydanticAI
|
|
106
|
+
tool_name: Name of the tool that was called
|
|
107
|
+
args: JSON arguments passed to the tool
|
|
108
|
+
result: Result returned by the tool
|
|
109
|
+
status: Current status (pending, running, completed, failed)
|
|
110
|
+
started_at: When the tool call started
|
|
111
|
+
completed_at: When the tool call completed
|
|
112
|
+
duration_ms: Execution time in milliseconds
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
__tablename__ = "tool_calls"
|
|
116
|
+
|
|
117
|
+
id: uuid.UUID = Field(
|
|
118
|
+
default_factory=uuid.uuid4,
|
|
119
|
+
sa_column=Column(PG_UUID(as_uuid=True), primary_key=True),
|
|
120
|
+
)
|
|
121
|
+
message_id: uuid.UUID = Field(
|
|
122
|
+
sa_column=Column(
|
|
123
|
+
PG_UUID(as_uuid=True),
|
|
124
|
+
ForeignKey("messages.id", ondelete="CASCADE"),
|
|
125
|
+
nullable=False,
|
|
126
|
+
index=True,
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
tool_call_id: str = Field(max_length=100)
|
|
130
|
+
tool_name: str = Field(max_length=100)
|
|
131
|
+
args: dict = Field(default_factory=dict, sa_column=Column(JSONB, nullable=False, default=dict))
|
|
132
|
+
result: str | None = Field(default=None, sa_column=Column(Text, nullable=True))
|
|
133
|
+
status: str = Field(default="pending", max_length=20) # pending, running, completed, failed
|
|
134
|
+
started_at: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=False))
|
|
135
|
+
completed_at: datetime | None = Field(
|
|
136
|
+
default=None,
|
|
137
|
+
sa_column=Column(DateTime(timezone=True), nullable=True),
|
|
138
|
+
)
|
|
139
|
+
duration_ms: int | None = Field(default=None)
|
|
140
|
+
|
|
141
|
+
# Relationships
|
|
142
|
+
message: "Message" = Relationship(back_populates="tool_calls")
|
|
143
|
+
|
|
144
|
+
def __repr__(self) -> str:
|
|
145
|
+
return f"<ToolCall(id={self.id}, tool_name={self.tool_name}, status={self.status})>"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
{%- elif cookiecutter.enable_conversation_persistence and cookiecutter.use_postgresql %}
|
|
2
149
|
"""Conversation and message models for AI chat persistence."""
|
|
3
150
|
|
|
4
151
|
import uuid
|
|
@@ -146,6 +293,120 @@ class ToolCall(Base):
|
|
|
146
293
|
return f"<ToolCall(id={self.id}, tool_name={self.tool_name}, status={self.status})>"
|
|
147
294
|
|
|
148
295
|
|
|
296
|
+
{%- elif cookiecutter.enable_conversation_persistence and cookiecutter.use_sqlite and cookiecutter.use_sqlmodel %}
|
|
297
|
+
"""Conversation and message models for AI chat persistence using SQLModel."""
|
|
298
|
+
|
|
299
|
+
import uuid
|
|
300
|
+
from datetime import datetime
|
|
301
|
+
|
|
302
|
+
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text
|
|
303
|
+
from sqlmodel import Field, Relationship, SQLModel
|
|
304
|
+
|
|
305
|
+
from app.db.base import TimestampMixin
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class Conversation(TimestampMixin, SQLModel, table=True):
|
|
309
|
+
"""Conversation model - groups messages in a chat session."""
|
|
310
|
+
|
|
311
|
+
__tablename__ = "conversations"
|
|
312
|
+
|
|
313
|
+
id: str = Field(
|
|
314
|
+
default_factory=lambda: str(uuid.uuid4()),
|
|
315
|
+
sa_column=Column(String(36), primary_key=True),
|
|
316
|
+
)
|
|
317
|
+
{%- if cookiecutter.use_jwt %}
|
|
318
|
+
user_id: str | None = Field(
|
|
319
|
+
default=None,
|
|
320
|
+
sa_column=Column(
|
|
321
|
+
String(36),
|
|
322
|
+
ForeignKey("users.id", ondelete="CASCADE"),
|
|
323
|
+
nullable=True,
|
|
324
|
+
index=True,
|
|
325
|
+
),
|
|
326
|
+
)
|
|
327
|
+
{%- endif %}
|
|
328
|
+
title: str | None = Field(default=None, max_length=255)
|
|
329
|
+
is_archived: bool = Field(default=False)
|
|
330
|
+
|
|
331
|
+
# Relationships
|
|
332
|
+
messages: list["Message"] = Relationship(
|
|
333
|
+
back_populates="conversation",
|
|
334
|
+
sa_relationship_kwargs={"cascade": "all, delete-orphan", "order_by": "Message.created_at"},
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
def __repr__(self) -> str:
|
|
338
|
+
return f"<Conversation(id={self.id}, title={self.title})>"
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class Message(TimestampMixin, SQLModel, table=True):
|
|
342
|
+
"""Message model - individual message in a conversation."""
|
|
343
|
+
|
|
344
|
+
__tablename__ = "messages"
|
|
345
|
+
|
|
346
|
+
id: str = Field(
|
|
347
|
+
default_factory=lambda: str(uuid.uuid4()),
|
|
348
|
+
sa_column=Column(String(36), primary_key=True),
|
|
349
|
+
)
|
|
350
|
+
conversation_id: str = Field(
|
|
351
|
+
sa_column=Column(
|
|
352
|
+
String(36),
|
|
353
|
+
ForeignKey("conversations.id", ondelete="CASCADE"),
|
|
354
|
+
nullable=False,
|
|
355
|
+
index=True,
|
|
356
|
+
),
|
|
357
|
+
)
|
|
358
|
+
role: str = Field(max_length=20)
|
|
359
|
+
content: str = Field(sa_column=Column(Text, nullable=False))
|
|
360
|
+
model_name: str | None = Field(default=None, max_length=100)
|
|
361
|
+
tokens_used: int | None = Field(default=None)
|
|
362
|
+
|
|
363
|
+
# Relationships
|
|
364
|
+
conversation: "Conversation" = Relationship(back_populates="messages")
|
|
365
|
+
tool_calls: list["ToolCall"] = Relationship(
|
|
366
|
+
back_populates="message",
|
|
367
|
+
sa_relationship_kwargs={"cascade": "all, delete-orphan", "order_by": "ToolCall.started_at"},
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
def __repr__(self) -> str:
|
|
371
|
+
return f"<Message(id={self.id}, role={self.role})>"
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class ToolCall(SQLModel, table=True):
|
|
375
|
+
"""ToolCall model - record of a tool invocation."""
|
|
376
|
+
|
|
377
|
+
__tablename__ = "tool_calls"
|
|
378
|
+
|
|
379
|
+
id: str = Field(
|
|
380
|
+
default_factory=lambda: str(uuid.uuid4()),
|
|
381
|
+
sa_column=Column(String(36), primary_key=True),
|
|
382
|
+
)
|
|
383
|
+
message_id: str = Field(
|
|
384
|
+
sa_column=Column(
|
|
385
|
+
String(36),
|
|
386
|
+
ForeignKey("messages.id", ondelete="CASCADE"),
|
|
387
|
+
nullable=False,
|
|
388
|
+
index=True,
|
|
389
|
+
),
|
|
390
|
+
)
|
|
391
|
+
tool_call_id: str = Field(max_length=100)
|
|
392
|
+
tool_name: str = Field(max_length=100)
|
|
393
|
+
args: str = Field(default="{}", sa_column=Column(Text, nullable=False, default="{}")) # JSON as string
|
|
394
|
+
result: str | None = Field(default=None, sa_column=Column(Text, nullable=True))
|
|
395
|
+
status: str = Field(default="pending", max_length=20)
|
|
396
|
+
started_at: datetime = Field(sa_column=Column(DateTime, nullable=False))
|
|
397
|
+
completed_at: datetime | None = Field(
|
|
398
|
+
default=None,
|
|
399
|
+
sa_column=Column(DateTime, nullable=True),
|
|
400
|
+
)
|
|
401
|
+
duration_ms: int | None = Field(default=None)
|
|
402
|
+
|
|
403
|
+
# Relationships
|
|
404
|
+
message: "Message" = Relationship(back_populates="tool_calls")
|
|
405
|
+
|
|
406
|
+
def __repr__(self) -> str:
|
|
407
|
+
return f"<ToolCall(id={self.id}, tool_name={self.tool_name})>"
|
|
408
|
+
|
|
409
|
+
|
|
149
410
|
{%- elif cookiecutter.enable_conversation_persistence and cookiecutter.use_sqlite %}
|
|
150
411
|
"""Conversation and message models for AI chat persistence."""
|
|
151
412
|
|
|
@@ -1,4 +1,42 @@
|
|
|
1
|
-
{%- if cookiecutter.include_example_crud and cookiecutter.use_postgresql %}
|
|
1
|
+
{%- if cookiecutter.include_example_crud and cookiecutter.use_postgresql and cookiecutter.use_sqlmodel %}
|
|
2
|
+
"""Item database model using SQLModel - example CRUD entity."""
|
|
3
|
+
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import Column, String, Text
|
|
7
|
+
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
|
8
|
+
from sqlmodel import Field, SQLModel
|
|
9
|
+
|
|
10
|
+
from app.db.base import TimestampMixin
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Item(TimestampMixin, SQLModel, table=True):
|
|
14
|
+
"""Item model - example entity for demonstrating CRUD operations.
|
|
15
|
+
|
|
16
|
+
This is a simple example model. You can use it as a template
|
|
17
|
+
for creating your own models or remove it if not needed.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__tablename__ = "items"
|
|
21
|
+
|
|
22
|
+
id: uuid.UUID = Field(
|
|
23
|
+
default_factory=uuid.uuid4,
|
|
24
|
+
sa_column=Column(PG_UUID(as_uuid=True), primary_key=True),
|
|
25
|
+
)
|
|
26
|
+
title: str = Field(
|
|
27
|
+
sa_column=Column(String(255), nullable=False, index=True),
|
|
28
|
+
)
|
|
29
|
+
description: str | None = Field(
|
|
30
|
+
default=None,
|
|
31
|
+
sa_column=Column(Text, nullable=True),
|
|
32
|
+
)
|
|
33
|
+
is_active: bool = Field(default=True)
|
|
34
|
+
|
|
35
|
+
def __repr__(self) -> str:
|
|
36
|
+
return f"<Item(id={self.id}, title={self.title})>"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
{%- elif cookiecutter.include_example_crud and cookiecutter.use_postgresql %}
|
|
2
40
|
"""Item database model - example CRUD entity."""
|
|
3
41
|
|
|
4
42
|
import uuid
|
|
@@ -30,6 +68,43 @@ class Item(Base, TimestampMixin):
|
|
|
30
68
|
return f"<Item(id={self.id}, title={self.title})>"
|
|
31
69
|
|
|
32
70
|
|
|
71
|
+
{%- elif cookiecutter.include_example_crud and cookiecutter.use_sqlite and cookiecutter.use_sqlmodel %}
|
|
72
|
+
"""Item database model using SQLModel - example CRUD entity."""
|
|
73
|
+
|
|
74
|
+
import uuid
|
|
75
|
+
|
|
76
|
+
from sqlalchemy import Column, String, Text
|
|
77
|
+
from sqlmodel import Field, SQLModel
|
|
78
|
+
|
|
79
|
+
from app.db.base import TimestampMixin
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Item(TimestampMixin, SQLModel, table=True):
|
|
83
|
+
"""Item model - example entity for demonstrating CRUD operations.
|
|
84
|
+
|
|
85
|
+
This is a simple example model. You can use it as a template
|
|
86
|
+
for creating your own models or remove it if not needed.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
__tablename__ = "items"
|
|
90
|
+
|
|
91
|
+
id: str = Field(
|
|
92
|
+
default_factory=lambda: str(uuid.uuid4()),
|
|
93
|
+
sa_column=Column(String(36), primary_key=True),
|
|
94
|
+
)
|
|
95
|
+
title: str = Field(
|
|
96
|
+
sa_column=Column(String(255), nullable=False, index=True),
|
|
97
|
+
)
|
|
98
|
+
description: str | None = Field(
|
|
99
|
+
default=None,
|
|
100
|
+
sa_column=Column(Text, nullable=True),
|
|
101
|
+
)
|
|
102
|
+
is_active: bool = Field(default=True)
|
|
103
|
+
|
|
104
|
+
def __repr__(self) -> str:
|
|
105
|
+
return f"<Item(id={self.id}, title={self.title})>"
|
|
106
|
+
|
|
107
|
+
|
|
33
108
|
{%- elif cookiecutter.include_example_crud and cookiecutter.use_sqlite %}
|
|
34
109
|
"""Item database model - example CRUD entity."""
|
|
35
110
|
|