mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-16 08:25:18 +02:00
feat: allow recordings in tool transitions
This commit is contained in:
parent
3a272d3a44
commit
ffe9a99401
38 changed files with 1555 additions and 692 deletions
108
api/alembic/versions/3cd3155084a2_dedup_org_scoped_recordings.py
Normal file
108
api/alembic/versions/3cd3155084a2_dedup_org_scoped_recordings.py
Normal 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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue