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.
Files changed (71) hide show
  1. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/METADATA +9 -2
  2. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/RECORD +71 -55
  3. fastapi_gen/__init__.py +6 -1
  4. fastapi_gen/cli.py +9 -0
  5. fastapi_gen/config.py +154 -2
  6. fastapi_gen/generator.py +34 -14
  7. fastapi_gen/prompts.py +172 -31
  8. fastapi_gen/template/VARIABLES.md +33 -4
  9. fastapi_gen/template/cookiecutter.json +10 -0
  10. fastapi_gen/template/hooks/post_gen_project.py +87 -2
  11. fastapi_gen/template/{{cookiecutter.project_slug}}/.env.prod.example +9 -0
  12. fastapi_gen/template/{{cookiecutter.project_slug}}/.gitlab-ci.yml +178 -0
  13. fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md +3 -0
  14. fastapi_gen/template/{{cookiecutter.project_slug}}/README.md +334 -0
  15. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.env.example +32 -0
  16. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py +10 -1
  17. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/admin.py +1 -1
  18. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/__init__.py +31 -0
  19. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/crewai_assistant.py +563 -0
  20. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/deepagents_assistant.py +526 -0
  21. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langchain_assistant.py +4 -3
  22. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langgraph_assistant.py +371 -0
  23. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/agent.py +1472 -0
  24. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/oauth.py +3 -7
  25. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/cleanup.py +2 -2
  26. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/seed.py +7 -2
  27. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/config.py +44 -7
  28. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/__init__.py +7 -0
  29. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/base.py +42 -0
  30. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/conversation.py +262 -1
  31. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/item.py +76 -1
  32. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/session.py +118 -1
  33. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/user.py +158 -1
  34. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/webhook.py +185 -3
  35. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/main.py +29 -2
  36. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/base.py +6 -0
  37. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/session.py +4 -4
  38. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/conversation.py +9 -9
  39. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/session.py +6 -6
  40. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py +7 -7
  41. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/__init__.py +1 -1
  42. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/arq_app.py +165 -0
  43. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/__init__.py +10 -1
  44. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +40 -0
  45. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_metrics.py +53 -0
  46. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_agents.py +2 -0
  47. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml +6 -0
  48. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml +100 -0
  49. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml +39 -0
  50. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.env.example +5 -0
  51. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-container.tsx +28 -1
  52. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/index.ts +1 -0
  53. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-item.tsx +22 -4
  54. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-list.tsx +23 -3
  55. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-approval-dialog.tsx +138 -0
  56. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-chat.ts +242 -18
  57. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-local-chat.ts +242 -17
  58. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/constants.ts +1 -1
  59. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/chat.ts +57 -1
  60. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/configmap.yaml +63 -0
  61. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/deployment.yaml +242 -0
  62. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/ingress.yaml +44 -0
  63. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/kustomization.yaml +28 -0
  64. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/namespace.yaml +12 -0
  65. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/secret.yaml +59 -0
  66. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/service.yaml +23 -0
  67. fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/nginx.conf +225 -0
  68. fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/ssl/.gitkeep +18 -0
  69. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/WHEEL +0 -0
  70. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/entry_points.txt +0 -0
  71. {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
- # Run async OAuth in sync context
154
- loop = asyncio.new_event_loop()
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.utcnow() - timedelta(days=days)
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.use_postgresql %}
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