feat: allow recordings in tool transitions

This commit is contained in:
Abhishek Kumar 2026-04-10 16:18:01 +05:30
parent 3a272d3a44
commit ffe9a99401
38 changed files with 1555 additions and 692 deletions

View file

@ -0,0 +1,108 @@
"""dedup recordings to org-scoped unique audio
Revision ID: 3cd3155084a2
Revises: e7254d2c6c18
Create Date: 2026-04-10 12:00:00.000000
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "3cd3155084a2"
down_revision: Union[str, None] = "e7254d2c6c18"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
conn = op.get_bind()
# 1. Identify duplicate groups: same (org, transcript, tts config).
# Within each group the earliest row (by created_at) is canonical;
# every other row is an alias that will be remapped and soft-deleted.
rows = conn.execute(
sa.text("""
WITH ranked AS (
SELECT
recording_id,
organization_id,
transcript,
tts_provider,
tts_model,
tts_voice_id,
ROW_NUMBER() OVER (
PARTITION BY organization_id, transcript,
tts_provider, tts_model, tts_voice_id
ORDER BY created_at ASC
) AS rn
FROM workflow_recordings
WHERE is_active = true
),
canonical AS (
SELECT recording_id AS canonical_id,
organization_id, transcript,
tts_provider, tts_model, tts_voice_id
FROM ranked
WHERE rn = 1
)
SELECT r.recording_id AS alias_id, c.canonical_id
FROM ranked r
JOIN canonical c
ON r.organization_id = c.organization_id
AND r.transcript = c.transcript
AND r.tts_provider = c.tts_provider
AND r.tts_model = c.tts_model
AND r.tts_voice_id = c.tts_voice_id
WHERE r.rn > 1
""")
).fetchall()
if not rows:
return
# 2. Replace alias recording_ids with canonical ones in workflow JSON.
# Both draft definitions (workflows.workflow_definition) and published
# versions (workflow_definitions.workflow_json) are updated.
for alias_id, canonical_id in rows:
alias_pattern = f"RECORDING_ID: {alias_id}"
canonical_pattern = f"RECORDING_ID: {canonical_id}"
conn.execute(
sa.text("""
UPDATE workflows
SET workflow_definition =
REPLACE(workflow_definition::text, :alias, :canonical)::json
WHERE workflow_definition::text LIKE '%%' || :alias || '%%'
"""),
{"alias": alias_pattern, "canonical": canonical_pattern},
)
conn.execute(
sa.text("""
UPDATE workflow_definitions
SET workflow_json =
REPLACE(workflow_json::text, :alias, :canonical)::json
WHERE workflow_json::text LIKE '%%' || :alias || '%%'
"""),
{"alias": alias_pattern, "canonical": canonical_pattern},
)
# 3. Soft-delete every alias row.
alias_ids = [r[0] for r in rows]
conn.execute(
sa.text("""
UPDATE workflow_recordings
SET is_active = false
WHERE recording_id = ANY(:ids)
AND is_active = true
"""),
{"ids": alias_ids},
)
def downgrade() -> None:
# Deduplication is a one-way data migration. The soft-deleted rows
# still exist in the table; a manual restore is possible if needed.
pass

View file

@ -1,7 +1,7 @@
"""unique recording id per org and workflow
"""make recordings org-scoped instead of workflow-scoped
Revision ID: 67a5cf3e09d0
Revises: e7254d2c6c18
Revises: 3cd3155084a2
Create Date: 2026-04-09 17:03:38.302041
"""
@ -13,13 +13,13 @@ from alembic import op
# revision identifiers, used by Alembic.
revision: str = "67a5cf3e09d0"
down_revision: Union[str, None] = "e7254d2c6c18"
down_revision: Union[str, None] = "3cd3155084a2"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Widen column from 16 to 64 chars for descriptive names
# 1. Widen recording_id from 16 to 64 chars for descriptive names
op.alter_column(
"workflow_recordings",
"recording_id",
@ -27,40 +27,79 @@ def upgrade() -> None:
type_=sa.String(length=64),
existing_nullable=False,
)
# Drop the old globally-unique index
op.drop_index(
op.f("ix_workflow_recordings_recording_id"), table_name="workflow_recordings"
# 2. Make workflow_id nullable — recordings are now org-scoped
op.alter_column(
"workflow_recordings",
"workflow_id",
existing_type=sa.Integer(),
nullable=True,
)
# Re-create as non-unique index for lookups
# 3. Drop the old globally-unique index on recording_id
op.drop_index(
"ix_workflow_recordings_recording_id",
table_name="workflow_recordings",
)
# 4. Re-create as non-unique index (for lookups)
op.create_index(
"ix_workflow_recordings_recording_id",
"workflow_recordings",
["recording_id"],
unique=False,
)
# Add composite unique constraint (recording_id, organization_id, workflow_id)
# 5. Add unique constraint (recording_id, organization_id)
op.create_unique_constraint(
"uq_workflow_recordings_recording_id_org_wf",
"uq_workflow_recordings_recording_id_org",
"workflow_recordings",
["recording_id", "organization_id", "workflow_id"],
["recording_id", "organization_id"],
)
# 6. Drop the workflow+TTS scope index (no longer relevant)
op.drop_index(
"ix_workflow_recordings_tts_scope",
table_name="workflow_recordings",
)
def downgrade() -> None:
# Re-create the TTS scope index
op.create_index(
"ix_workflow_recordings_tts_scope",
"workflow_recordings",
["workflow_id", "tts_provider", "tts_model", "tts_voice_id"],
)
# Drop the org-scoped unique constraint
op.drop_constraint(
"uq_workflow_recordings_recording_id_org_wf",
"uq_workflow_recordings_recording_id_org",
"workflow_recordings",
type_="unique",
)
# Drop non-unique index and re-create as unique
op.drop_index(
"ix_workflow_recordings_recording_id", table_name="workflow_recordings"
"ix_workflow_recordings_recording_id",
table_name="workflow_recordings",
)
op.create_index(
op.f("ix_workflow_recordings_recording_id"),
"ix_workflow_recordings_recording_id",
"workflow_recordings",
["recording_id"],
unique=True,
)
# Make workflow_id NOT NULL again
op.alter_column(
"workflow_recordings",
"workflow_id",
existing_type=sa.Integer(),
nullable=False,
)
# Revert recording_id width
op.alter_column(
"workflow_recordings",
"recording_id",

View file

@ -0,0 +1,42 @@
"""make tts columns nullable on workflow_recordings
Revision ID: a1b2c3d4e5f6
Revises: 67a5cf3e09d0
Create Date: 2026-04-10 12:00:00.000000
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "a1b2c3d4e5f6"
down_revision: str = "67a5cf3e09d0"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.alter_column(
"workflow_recordings", "tts_provider", existing_type=sa.String(), nullable=True
)
op.alter_column(
"workflow_recordings", "tts_model", existing_type=sa.String(), nullable=True
)
op.alter_column(
"workflow_recordings", "tts_voice_id", existing_type=sa.String(), nullable=True
)
def downgrade() -> None:
op.alter_column(
"workflow_recordings", "tts_voice_id", existing_type=sa.String(), nullable=False
)
op.alter_column(
"workflow_recordings", "tts_model", existing_type=sa.String(), nullable=False
)
op.alter_column(
"workflow_recordings", "tts_provider", existing_type=sa.String(), nullable=False
)