Skip to content

Commit e67e84c

Browse files
committed
feat: structured user templates
1 parent b4afede commit e67e84c

20 files changed

Lines changed: 982 additions & 256 deletions
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Add template_question table
2+
3+
Revision ID: 9d080ca9fe6c
4+
Revises: fe0e69c8d4db
5+
Create Date: 2025-10-03 16:38:42.892342
6+
7+
"""
8+
9+
from collections.abc import Sequence
10+
11+
import sqlalchemy as sa
12+
import sqlmodel
13+
14+
from alembic import op
15+
16+
# revision identifiers, used by Alembic.
17+
revision: str = "9d080ca9fe6c"
18+
down_revision: str | None = "fe0e69c8d4db"
19+
branch_labels: str | Sequence[str] | None = None
20+
depends_on: str | Sequence[str] | None = None
21+
22+
templatetype = sa.Enum("DOCUMENT", "FORM", name="templatetype")
23+
24+
25+
def upgrade() -> None:
26+
# ### commands auto generated by Alembic - please adjust! ###
27+
op.create_table(
28+
"template_question",
29+
sa.Column("id", sa.Uuid(), server_default=sa.text("gen_random_uuid()"), nullable=False),
30+
sa.Column("position", sa.Integer(), nullable=False),
31+
sa.Column("title", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
32+
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
33+
sa.Column("user_template_id", sa.Uuid(), nullable=False),
34+
sa.ForeignKeyConstraint(["user_template_id"], ["user_template.id"], ondelete="CASCADE"),
35+
sa.PrimaryKeyConstraint("id"),
36+
)
37+
templatetype.create(op.get_bind())
38+
op.add_column("user_template", sa.Column("type", templatetype, nullable=False, server_default="DOCUMENT"))
39+
op.alter_column(
40+
"user_template", "description", existing_type=sa.VARCHAR(), server_default=None, existing_nullable=False
41+
)
42+
# ### end Alembic commands ###
43+
44+
45+
def downgrade() -> None:
46+
# ### commands auto generated by Alembic - please adjust! ###
47+
op.alter_column(
48+
"user_template",
49+
"description",
50+
existing_type=sa.VARCHAR(),
51+
server_default=sa.text("''::character varying"),
52+
existing_nullable=False,
53+
)
54+
op.drop_column("user_template", "type")
55+
op.drop_table("template_question")
56+
templatetype.drop(op.get_bind())
57+
# ### end Alembic commands ###

backend/api/routes/templates.py

Lines changed: 121 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
import datetime
2-
from collections.abc import Sequence
32
from uuid import UUID
43

54
from fastapi import APIRouter, HTTPException
5+
from sqlalchemy.orm import selectinload
66
from sqlmodel import col, select
77

88
from backend.api.dependencies import UserDep
99
from backend.api.dependencies.get_session import SQLSessionDep
10-
from common.database.postgres_models import UserTemplate
10+
from common.database.postgres_models import TemplateQuestion, TemplateType, UserTemplate
1111
from common.services.template_manager import TemplateManager
1212
from common.settings import get_settings
1313
from common.types import (
1414
CreateUserTemplateRequest,
1515
PatchUserTemplateRequest,
16+
Question,
1617
TemplateMetadata,
18+
TemplateResponse,
1719
)
1820

1921
templates_router = APIRouter(tags=["Templates"])
@@ -34,45 +36,84 @@ def get_templates(user: UserDep) -> list[TemplateMetadata]: # noqa: ARG001
3436

3537

3638
@templates_router.get("/user-templates")
37-
async def get_user_templates(user: UserDep, session: SQLSessionDep) -> Sequence[UserTemplate]:
38-
return (
39+
async def get_user_templates(user: UserDep, session: SQLSessionDep) -> list[TemplateResponse]:
40+
templates = (
3941
await session.exec(
4042
select(UserTemplate)
4143
.where(UserTemplate.user_id == user.id)
4244
.order_by(col(UserTemplate.updated_datetime).desc())
4345
)
4446
).all()
4547

48+
return [
49+
TemplateResponse(
50+
id=template.id,
51+
updated_datetime=template.updated_datetime,
52+
name=template.name,
53+
content=template.content,
54+
description=template.description,
55+
type=template.type,
56+
questions=None,
57+
)
58+
for template in templates
59+
]
60+
4661

4762
@templates_router.get("/user-templates/{template_id}")
48-
async def get_user_template(user: UserDep, session: SQLSessionDep, template_id: UUID) -> UserTemplate:
63+
async def get_user_template(user: UserDep, session: SQLSessionDep, template_id: UUID) -> TemplateResponse:
4964
template = (
50-
await session.exec(select(UserTemplate).where(UserTemplate.id == template_id, UserTemplate.user_id == user.id))
65+
await session.exec(
66+
select(UserTemplate)
67+
.where(UserTemplate.id == template_id, UserTemplate.user_id == user.id)
68+
.options(selectinload(UserTemplate.questions))
69+
)
5170
).first()
5271

5372
if not template:
5473
raise HTTPException(404)
5574

56-
return template
75+
return TemplateResponse(
76+
id=template.id,
77+
name=template.name,
78+
updated_datetime=template.updated_datetime,
79+
content=template.content,
80+
description=template.description,
81+
type=template.type,
82+
questions=None
83+
if template.type == TemplateType.DOCUMENT
84+
else [
85+
Question(id=question.id, title=question.title, description=question.description, position=question.position)
86+
for question in template.questions
87+
],
88+
)
5789

5890

5991
@templates_router.post("/user-templates")
60-
async def create_user_template(
61-
user: UserDep, session: SQLSessionDep, request: CreateUserTemplateRequest
62-
) -> UserTemplate:
92+
async def create_user_template(user: UserDep, session: SQLSessionDep, request: CreateUserTemplateRequest):
6393
template = UserTemplate(
64-
name=request.name, content=request.content, description=request.description, user_id=user.id
94+
name=request.name,
95+
content=request.content,
96+
description=request.description,
97+
user_id=user.id,
98+
type=request.type,
99+
questions=[
100+
TemplateQuestion(
101+
position=question.position,
102+
title=question.title,
103+
description=question.description,
104+
) # type: ignore # noqa: PGH003
105+
for question in (request.questions or [])
106+
],
65107
)
108+
66109
session.add(template)
67110
await session.commit()
68-
await session.refresh(template)
69-
return template
70111

71112

72113
@templates_router.patch("/user-templates/{template_id}")
73114
async def edit_user_template(
74115
user: UserDep, session: SQLSessionDep, template_id: UUID, request: PatchUserTemplateRequest
75-
) -> UserTemplate:
116+
) -> TemplateResponse:
76117
template = (
77118
await session.exec(select(UserTemplate).where(UserTemplate.id == template_id, UserTemplate.user_id == user.id))
78119
).first()
@@ -86,12 +127,44 @@ async def edit_user_template(
86127
template.content = request.content
87128
if request.description is not None:
88129
template.description = request.description
130+
if request.questions is not None:
131+
questions = list(
132+
(await session.exec(select(TemplateQuestion).where(TemplateQuestion.user_template_id == template_id))).all()
133+
)
134+
for question in request.questions:
135+
if isinstance(question, Question):
136+
existing_idx = next((i for i, q in enumerate(questions) if q.id == question.id), None)
137+
if existing_idx:
138+
existing = questions.pop(existing_idx)
139+
existing.title = question.title
140+
existing.description = question.description
141+
existing.position = question.position
142+
continue
143+
144+
session.add(
145+
TemplateQuestion(
146+
user_template_id=template_id,
147+
position=question.position,
148+
title=question.title,
149+
description=question.description,
150+
)
151+
)
152+
for remaining_question in questions:
153+
await session.delete(remaining_question)
89154

90155
template.updated_datetime = datetime.datetime.now(tz=datetime.UTC)
91156

92157
await session.commit()
93158

94-
return template
159+
return TemplateResponse(
160+
id=template.id,
161+
name=template.name,
162+
updated_datetime=template.updated_datetime,
163+
content=template.content,
164+
description=template.description,
165+
type=template.type,
166+
questions=None,
167+
)
95168

96169

97170
@templates_router.delete("/user-templates/{template_id}")
@@ -108,20 +181,44 @@ async def delete_user_template(user: UserDep, session: SQLSessionDep, template_i
108181

109182

110183
@templates_router.post("/user-templates/{template_id}/duplicate")
111-
async def duplicate_user_template(user: UserDep, session: SQLSessionDep, template_id: UUID) -> UserTemplate:
112-
template = (
113-
await session.exec(select(UserTemplate).where(UserTemplate.id == template_id, UserTemplate.user_id == user.id))
184+
async def duplicate_user_template(user: UserDep, session: SQLSessionDep, template_id: UUID) -> TemplateResponse:
185+
original_template = (
186+
await session.exec(
187+
select(UserTemplate)
188+
.where(UserTemplate.id == template_id, UserTemplate.user_id == user.id)
189+
.options(selectinload(UserTemplate.questions))
190+
)
114191
).first()
115192

116-
if not template:
193+
if not original_template:
117194
raise HTTPException(404)
118195

119-
new_template = UserTemplate(
120-
user_id=user.id, name=template.name + " (Copy)", description=template.description, content=template.content
196+
template = UserTemplate(
197+
user_id=user.id,
198+
name=original_template.name + " (Copy)",
199+
description=original_template.description,
200+
content=original_template.content,
201+
type=original_template.type,
202+
questions=[
203+
TemplateQuestion(
204+
position=question.position,
205+
title=question.title,
206+
description=question.description,
207+
) # type: ignore # noqa: PGH003
208+
for question in original_template.questions
209+
],
121210
)
122211

123-
session.add(new_template)
212+
session.add(template)
124213
await session.commit()
125-
await session.refresh(new_template)
214+
await session.refresh(template)
126215

127-
return new_template
216+
return TemplateResponse(
217+
id=template.id,
218+
updated_datetime=template.updated_datetime,
219+
name=template.name,
220+
content=template.content,
221+
description=template.description,
222+
type=template.type,
223+
questions=None,
224+
)

common/database/postgres_models.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,22 @@ class Transcription(BaseTableMixin, table=True):
185185
)
186186

187187

188+
class TemplateType(StrEnum):
189+
DOCUMENT = auto()
190+
FORM = auto()
191+
192+
193+
class TemplateQuestion(BaseTableMixin, table=True):
194+
__tablename__ = "template_question"
195+
196+
position: int
197+
title: str
198+
description: str
199+
200+
user_template_id: UUID = Field(foreign_key="user_template.id", ondelete="CASCADE")
201+
user_template: "UserTemplate" = Relationship(back_populates="questions")
202+
203+
188204
class UserTemplate(BaseTableMixin, table=True):
189205
__tablename__ = "user_template"
190206
created_datetime: datetime = Field(sa_column=created_datetime_column(), default=None)
@@ -194,6 +210,14 @@ class UserTemplate(BaseTableMixin, table=True):
194210
content: str
195211
description: str = ""
196212

213+
type: TemplateType = TemplateType.DOCUMENT
214+
197215
user_id: UUID | None = Field(default=None, foreign_key="user.id")
198216

199217
minutes: list[Minute] = Relationship(back_populates="user_template")
218+
219+
questions: list[TemplateQuestion] = Relationship(
220+
back_populates="user_template",
221+
passive_deletes="all",
222+
sa_relationship_kwargs={"order_by": TemplateQuestion.position},
223+
)

common/services/minute_handler_service.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
)
1818
from common.services.template_manager import TemplateManager
1919
from common.settings import get_settings
20-
from common.templates.user_template import UserMarkdownTemplate
20+
from common.templates.user_template import generate_user_template
2121
from common.types import (
2222
LLMHallucination,
2323
MeetingType,
@@ -166,18 +166,13 @@ async def process_minute_edit_message(cls, source_minute_version_id: UUID, targe
166166
@classmethod
167167
async def generate_minute_from_user_template(cls, minute: Minute):
168168
with SessionLocal() as session:
169-
template = session.get(UserTemplate, minute.user_template_id)
169+
template = session.get(
170+
UserTemplate, minute.user_template_id, options=[selectinload(UserTemplate.questions)]
171+
)
170172
if not template:
171173
msg = f"No template with id {minute.user_template_id}"
172174
raise RuntimeError(msg)
173-
174-
chatbot = create_default_chatbot(FastOrBestLLM.BEST)
175-
minutes = await chatbot.chat(
176-
UserMarkdownTemplate.prompt(
177-
template.content, minute.transcription.dialogue_entries or [], transcription=minute.transcription
178-
)
179-
)
180-
hallucinations = await chatbot.hallucination_check()
175+
minutes, hallucinations = await generate_user_template(template=template, transcription=minute.transcription)
181176
return minutes, hallucinations
182177

183178
@classmethod

0 commit comments

Comments
 (0)