Skip to content

Commit ff45301

Browse files
authored
feat: Lecture_Video interaction mode, create/update assistant endpoint upgrades (#1363)
## Assistants ### New Features - Introduces a Lecture Video assistant mode for upcoming features. - Adds support for creating and updating assistants, including associating a lecture video from a video store. ### Notes - Creating and editing a Lecture Video assistant will become available in preview in the PingPong web client with a future update. Only admins will be able to create and edit Lecture Video assistants during the preview period.
1 parent e3bf3e1 commit ff45301

6 files changed

Lines changed: 544 additions & 15 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""added_video_interaction_mode
2+
3+
Revision ID: 700e871f0775
4+
Revises: 5f2cb8d3f4a1
5+
Create Date: 2026-02-13 13:41:32.001455
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
from alembic import op
12+
import sqlalchemy as sa
13+
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = "700e871f0775"
17+
down_revision: Union[str, None] = "5f2cb8d3f4a1"
18+
branch_labels: Union[str, Sequence[str], None] = None
19+
depends_on: Union[str, Sequence[str], None] = None
20+
21+
22+
def upgrade() -> None:
23+
# ### commands auto generated by Alembic - please adjust! ###
24+
op.create_table(
25+
"lecture_videos",
26+
sa.Column("id", sa.Integer(), nullable=False),
27+
sa.Column("key", sa.String(), nullable=False),
28+
sa.Column("name", sa.String(), nullable=True),
29+
sa.Column("uploader_id", sa.Integer(), nullable=True),
30+
sa.Column(
31+
"created",
32+
sa.DateTime(timezone=True),
33+
server_default=sa.text("now()"),
34+
nullable=True,
35+
),
36+
sa.Column("updated", sa.DateTime(timezone=True), nullable=True),
37+
sa.ForeignKeyConstraint(
38+
["uploader_id"],
39+
["users.id"],
40+
),
41+
sa.PrimaryKeyConstraint("id"),
42+
sa.UniqueConstraint("key"),
43+
)
44+
op.create_index(
45+
op.f("ix_lecture_videos_updated"), "lecture_videos", ["updated"], unique=False
46+
)
47+
op.add_column(
48+
"assistants", sa.Column("lecture_video_id", sa.Integer(), nullable=True)
49+
)
50+
op.create_foreign_key(
51+
"fk_assistants_lecture_video_id_lecture_video",
52+
"assistants",
53+
"lecture_videos",
54+
["lecture_video_id"],
55+
["id"],
56+
)
57+
# ### end Alembic commands ###
58+
59+
60+
def downgrade() -> None:
61+
# ### commands auto generated by Alembic - please adjust! ###
62+
op.drop_constraint(
63+
"fk_assistants_lecture_video_id_lecture_video", "assistants", type_="foreignkey"
64+
)
65+
op.drop_column("assistants", "lecture_video_id")
66+
op.drop_index(op.f("ix_lecture_videos_updated"), table_name="lecture_videos")
67+
op.drop_table("lecture_videos")
68+
# ### end Alembic commands ###
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"""Added Lecture Video case in the Interaction Mode Enum
2+
3+
Revision ID: d4843142e8de
4+
Revises: 700e871f0775
5+
Create Date: 2026-02-06 15:05:33.549155
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
from alembic import op
12+
import sqlalchemy as sa
13+
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = "d4843142e8de"
17+
down_revision: Union[str, None] = "700e871f0775"
18+
branch_labels: Union[str, Sequence[str], None] = None
19+
depends_on: Union[str, Sequence[str], None] = None
20+
21+
22+
enum_name = "interactionmode"
23+
temp_enum_name = f"temp_{enum_name}"
24+
old_values = ("CHAT", "VOICE")
25+
new_values = (*old_values, "LECTURE_VIDEO")
26+
old_type = sa.Enum(*old_values, name=enum_name)
27+
new_type = sa.Enum(*new_values, name=enum_name)
28+
temp_type = sa.Enum(*new_values, name=temp_enum_name)
29+
30+
column_name = "interaction_mode"
31+
32+
assistants_table = "assistants"
33+
assistants_simplified = sa.sql.table(
34+
assistants_table, sa.Column(column_name, new_type, nullable=False)
35+
)
36+
37+
threads_table = "threads"
38+
threads_simplified = sa.sql.table(
39+
threads_table, sa.Column(column_name, new_type, nullable=False)
40+
)
41+
42+
43+
def upgrade() -> None:
44+
# ### commands auto generated by Alembic - please adjust! ###
45+
46+
# Drop server defaults before type change
47+
op.execute(
48+
f"ALTER TABLE {assistants_table} ALTER COLUMN {column_name} DROP DEFAULT"
49+
)
50+
op.execute(f"ALTER TABLE {threads_table} ALTER COLUMN {column_name} DROP DEFAULT")
51+
52+
temp_type.create(op.get_bind(), checkfirst=False)
53+
54+
with op.batch_alter_table(assistants_table) as batch_op:
55+
batch_op.alter_column(
56+
column_name,
57+
existing_type=old_type,
58+
type_=temp_type,
59+
postgresql_using=f"{column_name}::text::{temp_enum_name}",
60+
existing_nullable=False,
61+
)
62+
63+
with op.batch_alter_table(threads_table) as batch_op:
64+
batch_op.alter_column(
65+
column_name,
66+
existing_type=old_type,
67+
type_=temp_type,
68+
postgresql_using=f"{column_name}::text::{temp_enum_name}",
69+
existing_nullable=False,
70+
)
71+
72+
old_type.drop(op.get_bind(), checkfirst=False)
73+
new_type.create(op.get_bind(), checkfirst=False)
74+
75+
with op.batch_alter_table(assistants_table) as batch_op:
76+
batch_op.alter_column(
77+
column_name,
78+
existing_type=temp_type,
79+
type_=new_type,
80+
postgresql_using=f"{column_name}::text::{enum_name}",
81+
existing_nullable=False,
82+
)
83+
84+
with op.batch_alter_table(threads_table) as batch_op:
85+
batch_op.alter_column(
86+
column_name,
87+
existing_type=temp_type,
88+
type_=new_type,
89+
postgresql_using=f"{column_name}::text::{enum_name}",
90+
existing_nullable=False,
91+
)
92+
93+
temp_type.drop(op.get_bind(), checkfirst=False)
94+
95+
# Re-add server defaults
96+
op.execute(
97+
f"ALTER TABLE {assistants_table} ALTER COLUMN {column_name} SET DEFAULT 'CHAT'::{enum_name}"
98+
)
99+
op.execute(
100+
f"ALTER TABLE {threads_table} ALTER COLUMN {column_name} SET DEFAULT 'CHAT'::{enum_name}"
101+
)
102+
103+
# ### end Alembic commands ###
104+
105+
106+
def downgrade() -> None:
107+
# ### commands auto generated by Alembic - please adjust! ###
108+
# Remove rows with the new enum value to avoid issues during downgrade
109+
op.execute(
110+
threads_simplified.delete().where(
111+
threads_simplified.c.interaction_mode == "LECTURE_VIDEO"
112+
)
113+
)
114+
op.execute(
115+
assistants_simplified.delete().where(
116+
assistants_simplified.c.interaction_mode == "LECTURE_VIDEO"
117+
)
118+
)
119+
120+
# Drop server defaults before type change
121+
op.execute(
122+
f"ALTER TABLE {assistants_table} ALTER COLUMN {column_name} DROP DEFAULT"
123+
)
124+
op.execute(f"ALTER TABLE {threads_table} ALTER COLUMN {column_name} DROP DEFAULT")
125+
126+
temp_type.create(op.get_bind(), checkfirst=False)
127+
128+
# Alter column types to temp enum
129+
with op.batch_alter_table(assistants_table) as batch_op:
130+
batch_op.alter_column(
131+
column_name,
132+
existing_type=new_type,
133+
type_=temp_type,
134+
postgresql_using=f"{column_name}::text::{temp_enum_name}",
135+
existing_nullable=False,
136+
)
137+
138+
with op.batch_alter_table(threads_table) as batch_op:
139+
batch_op.alter_column(
140+
column_name,
141+
existing_type=new_type,
142+
type_=temp_type,
143+
postgresql_using=f"{column_name}::text::{temp_enum_name}",
144+
existing_nullable=False,
145+
)
146+
new_type.drop(op.get_bind(), checkfirst=False)
147+
old_type.create(op.get_bind(), checkfirst=False)
148+
149+
# Alter column types to old enum
150+
with op.batch_alter_table(assistants_table) as batch_op:
151+
batch_op.alter_column(
152+
column_name,
153+
existing_type=temp_type,
154+
type_=old_type,
155+
postgresql_using=f"{column_name}::text::{enum_name}",
156+
existing_nullable=False,
157+
)
158+
159+
with op.batch_alter_table(threads_table) as batch_op:
160+
batch_op.alter_column(
161+
column_name,
162+
existing_type=temp_type,
163+
type_=old_type,
164+
postgresql_using=f"{column_name}::text::{enum_name}",
165+
existing_nullable=False,
166+
)
167+
168+
temp_type.drop(op.get_bind(), checkfirst=False)
169+
170+
# Re-add server defaults
171+
op.execute(
172+
f"ALTER TABLE {assistants_table} ALTER COLUMN {column_name} SET DEFAULT 'CHAT'::{enum_name}"
173+
)
174+
op.execute(
175+
f"ALTER TABLE {threads_table} ALTER COLUMN {column_name} SET DEFAULT 'CHAT'::{enum_name}"
176+
)
177+
# ### end Alembic commands ###

pingpong/models.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1920,6 +1920,44 @@ async def update(
19201920
)
19211921

19221922

1923+
class LectureVideo(Base):
1924+
__tablename__ = "lecture_videos"
1925+
1926+
id: Mapped[int] = mapped_column(primary_key=True)
1927+
assistants = relationship("Assistant", back_populates="lecture_video")
1928+
key = Column(String, nullable=False, unique=True)
1929+
name = Column(String)
1930+
uploader_id = Column(Integer, ForeignKey("users.id"), nullable=True, default=None)
1931+
created = Column(DateTime(timezone=True), server_default=func.now())
1932+
updated = Column(DateTime(timezone=True), index=True, onupdate=func.now())
1933+
1934+
@classmethod
1935+
async def create(
1936+
cls, session: AsyncSession, key: str, user_id: int
1937+
) -> "LectureVideo":
1938+
stmt = (
1939+
_get_upsert_stmt(session)(LectureVideo)
1940+
.values(
1941+
key=key,
1942+
uploader_id=user_id,
1943+
)
1944+
.on_conflict_do_update(index_elements=["key"], set_=dict(key=key))
1945+
.returning(LectureVideo)
1946+
)
1947+
result = await session.scalar(stmt)
1948+
return result
1949+
1950+
@classmethod
1951+
async def delete(cls, session: AsyncSession, id_: int) -> None:
1952+
stmt = delete(LectureVideo).where(LectureVideo.id == id_)
1953+
await session.execute(stmt)
1954+
1955+
@classmethod
1956+
async def get_by_key(cls, session: AsyncSession, key: str) -> "LectureVideo | None":
1957+
stmt = select(LectureVideo).where(LectureVideo.key == key)
1958+
return await session.scalar(stmt)
1959+
1960+
19231961
class S3File(Base):
19241962
__tablename__ = "s3_files"
19251963

@@ -2723,6 +2761,15 @@ class Assistant(Base):
27232761
secondary=code_interpreter_file_assistant_association,
27242762
back_populates="assistants_v2",
27252763
)
2764+
lecture_video_id = Column(
2765+
Integer,
2766+
ForeignKey(
2767+
"lecture_videos.id", name="fk_assistants_lecture_video_id_lecture_video"
2768+
),
2769+
)
2770+
lecture_video = relationship(
2771+
"LectureVideo", back_populates="assistants", uselist=False
2772+
)
27262773
vector_store_id = Column(
27272774
Integer,
27282775
ForeignKey(
@@ -2827,6 +2874,7 @@ async def create(
28272874
user_id: int,
28282875
assistant_id: str | None = None,
28292876
vector_store_id: int | None = None,
2877+
lecture_video_id: int | None = None,
28302878
version: int = 1,
28312879
) -> "Assistant":
28322880
params = data.dict()
@@ -2839,6 +2887,7 @@ async def create(
28392887
params["use_latex"] = data.use_latex
28402888
params["use_image_descriptions"] = data.use_image_descriptions
28412889
params["vector_store_id"] = vector_store_id
2890+
params["lecture_video_id"] = lecture_video_id
28422891
params["version"] = version
28432892

28442893
assistant = Assistant(**params)

0 commit comments

Comments
 (0)