mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-13 08:15:21 +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
|
|
@ -1005,7 +1005,7 @@ class KnowledgeBaseDocumentModel(Base):
|
|||
|
||||
|
||||
class WorkflowRecordingModel(Base):
|
||||
"""Model for storing audio recordings scoped to a workflow and TTS configuration.
|
||||
"""Model for storing audio recordings scoped to an organization.
|
||||
|
||||
Recordings are used in hybrid prompts where parts of the output are pre-recorded
|
||||
audio rather than dynamically generated TTS.
|
||||
|
|
@ -1020,16 +1020,16 @@ class WorkflowRecordingModel(Base):
|
|||
|
||||
# Scoping
|
||||
workflow_id = Column(
|
||||
Integer, ForeignKey("workflows.id", ondelete="CASCADE"), nullable=False
|
||||
Integer, ForeignKey("workflows.id", ondelete="CASCADE"), nullable=True
|
||||
)
|
||||
organization_id = Column(
|
||||
Integer, ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
|
||||
# TTS configuration scope
|
||||
tts_provider = Column(String, nullable=False)
|
||||
tts_model = Column(String, nullable=False)
|
||||
tts_voice_id = Column(String, nullable=False)
|
||||
# TTS configuration metadata (optional, legacy)
|
||||
tts_provider = Column(String, nullable=True)
|
||||
tts_model = Column(String, nullable=True)
|
||||
tts_voice_id = Column(String, nullable=True)
|
||||
|
||||
# Content
|
||||
transcript = Column(Text, nullable=False)
|
||||
|
|
@ -1065,19 +1065,11 @@ class WorkflowRecordingModel(Base):
|
|||
UniqueConstraint(
|
||||
"recording_id",
|
||||
"organization_id",
|
||||
"workflow_id",
|
||||
name="uq_workflow_recordings_recording_id_org_wf",
|
||||
name="uq_workflow_recordings_recording_id_org",
|
||||
),
|
||||
Index("ix_workflow_recordings_workflow_id", "workflow_id"),
|
||||
Index("ix_workflow_recordings_org_id", "organization_id"),
|
||||
Index("ix_workflow_recordings_recording_id", "recording_id"),
|
||||
Index(
|
||||
"ix_workflow_recordings_tts_scope",
|
||||
"workflow_id",
|
||||
"tts_provider",
|
||||
"tts_model",
|
||||
"tts_voice_id",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import string
|
|||
from typing import List, Optional
|
||||
|
||||
from loguru import logger
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import func, select, text
|
||||
|
||||
from api.db.base_client import BaseDBClient
|
||||
from api.db.models import WorkflowRecordingModel
|
||||
|
|
@ -23,30 +23,30 @@ class WorkflowRecordingClient(BaseDBClient):
|
|||
async def create_recording(
|
||||
self,
|
||||
recording_id: str,
|
||||
workflow_id: int,
|
||||
organization_id: int,
|
||||
tts_provider: str,
|
||||
tts_model: str,
|
||||
tts_voice_id: str,
|
||||
transcript: str,
|
||||
storage_key: str,
|
||||
storage_backend: str,
|
||||
created_by: int,
|
||||
workflow_id: Optional[int] = None,
|
||||
tts_provider: Optional[str] = None,
|
||||
tts_model: Optional[str] = None,
|
||||
tts_voice_id: Optional[str] = None,
|
||||
metadata: Optional[dict] = None,
|
||||
) -> WorkflowRecordingModel:
|
||||
"""Create a new workflow recording record.
|
||||
|
||||
Args:
|
||||
recording_id: Short unique recording identifier
|
||||
workflow_id: ID of the workflow
|
||||
organization_id: ID of the organization
|
||||
tts_provider: TTS provider name
|
||||
tts_model: TTS model name
|
||||
tts_voice_id: TTS voice identifier
|
||||
transcript: User-provided transcript
|
||||
storage_key: S3/MinIO storage key
|
||||
storage_backend: Storage backend (s3 or minio)
|
||||
created_by: ID of the user
|
||||
workflow_id: Optional workflow ID (legacy)
|
||||
tts_provider: Optional TTS provider name
|
||||
tts_model: Optional TTS model name
|
||||
tts_voice_id: Optional TTS voice identifier
|
||||
metadata: Optional extra metadata
|
||||
|
||||
Returns:
|
||||
|
|
@ -71,10 +71,7 @@ class WorkflowRecordingClient(BaseDBClient):
|
|||
await session.commit()
|
||||
await session.refresh(recording)
|
||||
|
||||
logger.info(
|
||||
f"Created recording {recording_id} for workflow {workflow_id}, "
|
||||
f"org {organization_id}"
|
||||
)
|
||||
logger.info(f"Created recording {recording_id} for org {organization_id}")
|
||||
return recording
|
||||
|
||||
async def get_recordings(
|
||||
|
|
@ -85,7 +82,7 @@ class WorkflowRecordingClient(BaseDBClient):
|
|||
tts_model: Optional[str] = None,
|
||||
tts_voice_id: Optional[str] = None,
|
||||
) -> List[WorkflowRecordingModel]:
|
||||
"""Get recordings for an organization, optionally filtered by workflow and TTS config.
|
||||
"""Get recordings for an organization, optionally filtered.
|
||||
|
||||
Args:
|
||||
organization_id: ID of the organization
|
||||
|
|
@ -121,14 +118,12 @@ class WorkflowRecordingClient(BaseDBClient):
|
|||
self,
|
||||
recording_id: str,
|
||||
organization_id: int,
|
||||
workflow_id: int,
|
||||
) -> Optional[WorkflowRecordingModel]:
|
||||
"""Get a recording by its string recording_id (unique per org + workflow).
|
||||
"""Get a recording by its short ID.
|
||||
|
||||
Args:
|
||||
recording_id: The descriptive recording ID
|
||||
recording_id: The short unique recording ID
|
||||
organization_id: ID of the organization
|
||||
workflow_id: ID of the workflow
|
||||
|
||||
Returns:
|
||||
WorkflowRecordingModel if found, None otherwise
|
||||
|
|
@ -137,7 +132,6 @@ class WorkflowRecordingClient(BaseDBClient):
|
|||
query = select(WorkflowRecordingModel).where(
|
||||
WorkflowRecordingModel.recording_id == recording_id,
|
||||
WorkflowRecordingModel.organization_id == organization_id,
|
||||
WorkflowRecordingModel.workflow_id == workflow_id,
|
||||
WorkflowRecordingModel.is_active == True,
|
||||
)
|
||||
|
||||
|
|
@ -170,13 +164,11 @@ class WorkflowRecordingClient(BaseDBClient):
|
|||
|
||||
async def has_active_recordings(
|
||||
self,
|
||||
workflow_id: int,
|
||||
organization_id: int,
|
||||
) -> bool:
|
||||
"""Check if a workflow has any active recordings.
|
||||
"""Check if an organization has any active recordings.
|
||||
|
||||
Args:
|
||||
workflow_id: ID of the workflow
|
||||
organization_id: ID of the organization
|
||||
|
||||
Returns:
|
||||
|
|
@ -187,7 +179,6 @@ class WorkflowRecordingClient(BaseDBClient):
|
|||
select(func.count())
|
||||
.select_from(WorkflowRecordingModel)
|
||||
.where(
|
||||
WorkflowRecordingModel.workflow_id == workflow_id,
|
||||
WorkflowRecordingModel.organization_id == organization_id,
|
||||
WorkflowRecordingModel.is_active == True,
|
||||
)
|
||||
|
|
@ -196,14 +187,13 @@ class WorkflowRecordingClient(BaseDBClient):
|
|||
return result.scalar_one() > 0
|
||||
|
||||
async def check_recording_id_exists(
|
||||
self, recording_id: str, organization_id: int, workflow_id: int
|
||||
self, recording_id: str, organization_id: int
|
||||
) -> bool:
|
||||
"""Check if a recording ID already exists within an organization and workflow.
|
||||
"""Check if a recording ID already exists within an organization.
|
||||
|
||||
Args:
|
||||
recording_id: The recording ID to check
|
||||
organization_id: ID of the organization
|
||||
workflow_id: ID of the workflow
|
||||
|
||||
Returns:
|
||||
True if exists, False otherwise
|
||||
|
|
@ -212,7 +202,6 @@ class WorkflowRecordingClient(BaseDBClient):
|
|||
query = select(WorkflowRecordingModel.id).where(
|
||||
WorkflowRecordingModel.recording_id == recording_id,
|
||||
WorkflowRecordingModel.organization_id == organization_id,
|
||||
WorkflowRecordingModel.workflow_id == workflow_id,
|
||||
WorkflowRecordingModel.is_active == True,
|
||||
)
|
||||
result = await session.execute(query)
|
||||
|
|
@ -257,6 +246,80 @@ class WorkflowRecordingClient(BaseDBClient):
|
|||
)
|
||||
return recording
|
||||
|
||||
async def replace_recording_id_in_workflows(
|
||||
self,
|
||||
old_id: str,
|
||||
new_id: str,
|
||||
organization_id: int,
|
||||
) -> int:
|
||||
"""Replace all occurrences of a recording ID in workflow definitions.
|
||||
|
||||
Updates both draft definitions (workflows.workflow_definition) and
|
||||
versioned definitions (workflow_definitions.workflow_json), skipping
|
||||
workflow_definitions with status 'legacy'.
|
||||
|
||||
Args:
|
||||
old_id: The old recording ID to find
|
||||
new_id: The new recording ID to replace with
|
||||
organization_id: ID of the organization (scopes to org workflows)
|
||||
|
||||
Returns:
|
||||
Total number of rows updated across both tables
|
||||
"""
|
||||
# Match the exact pattern used in prompts: "RECORDING_ID: <id>"
|
||||
old_pattern = f"RECORDING_ID: {old_id}"
|
||||
new_pattern = f"RECORDING_ID: {new_id}"
|
||||
|
||||
total = 0
|
||||
async with self.async_session() as session:
|
||||
# Update workflows.workflow_definition (draft definitions)
|
||||
result = await session.execute(
|
||||
text("""
|
||||
UPDATE workflows
|
||||
SET workflow_definition =
|
||||
REPLACE(workflow_definition::text, :old_pat, :new_pat)::json
|
||||
WHERE organization_id = :org_id
|
||||
AND workflow_definition::text LIKE '%%' || :old_pat || '%%'
|
||||
"""),
|
||||
{
|
||||
"old_pat": old_pattern,
|
||||
"new_pat": new_pattern,
|
||||
"org_id": organization_id,
|
||||
},
|
||||
)
|
||||
total += result.rowcount
|
||||
|
||||
# Update workflow_definitions.workflow_json (versioned definitions)
|
||||
# Skip legacy definitions
|
||||
result = await session.execute(
|
||||
text("""
|
||||
UPDATE workflow_definitions wd
|
||||
SET workflow_json =
|
||||
REPLACE(wd.workflow_json::text, :old_pat, :new_pat)::json
|
||||
FROM workflows w
|
||||
WHERE wd.workflow_id = w.id
|
||||
AND w.organization_id = :org_id
|
||||
AND wd.status != 'legacy'
|
||||
AND wd.workflow_json::text LIKE '%%' || :old_pat || '%%'
|
||||
"""),
|
||||
{
|
||||
"old_pat": old_pattern,
|
||||
"new_pat": new_pattern,
|
||||
"org_id": organization_id,
|
||||
},
|
||||
)
|
||||
total += result.rowcount
|
||||
|
||||
await session.commit()
|
||||
|
||||
if total > 0:
|
||||
logger.info(
|
||||
f"Replaced recording ID '{old_id}' -> '{new_id}' "
|
||||
f"in {total} workflow definition(s), org {organization_id}"
|
||||
)
|
||||
|
||||
return total
|
||||
|
||||
async def delete_recording(
|
||||
self,
|
||||
recording_id: str,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue