Alembic replaces create_all as the schema authority: async env.py against Base.metadata (CLI and in-app entry paths share it via config.attributes["connection"]), an autogenerated baseline of the create_all-era schema, and init_db now runs upgrade head — stamping the baseline first on a pre-Alembic database so existing deployments adopt cleanly. create_all remains for tests only. Calls are durable from the start: CallManager gains an on_call_created hook (wired to persist_call_on_create) that inserts an in_progress CallRecord the moment a call is created; persist_call_on_end finalizes that same row. A SIGKILL mid-call now leaves an in_progress row instead of erasing the call from history (verified live against the dev database). One transcript representation: ActiveCall.transcript_chunks holds TranscriptEntry (t_offset_ms, speaker, text) — add_transcript stamps real offsets from connect time, receptionist passes speaker instead of encoding it into "caller: ..." strings, persisted chunks carry real seek offsets, and the dead CallRecord.transcript Text column is dropped by migration. Device.is_online migrates String → Boolean (with a USING cast for existing rows). Model de-triplication: CallResponse/CallStatusResponse build via from_call classmethods (one ActiveCall→response mapping); DeviceStatus deleted — can_receive_call is a computed field on Device and the list endpoint returns the domain model; all row↔dict and row↔domain mapping now lives in call_persistence.py (record_summary/record_detail/chunk_to_dict + device row functions). New tests/test_data_layer.py: upgrade-head-matches-models, pre-Alembic adoption, durable in_progress rows, end-without-create fallback, transcript offsets, consolidated response models. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
130 lines
5.8 KiB
Python
130 lines
5.8 KiB
Python
"""baseline schema
|
|
|
|
Revision ID: 1173a71329ed
|
|
Revises:
|
|
Create Date: 2026-07-10 07:19:08.321778
|
|
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = '1173a71329ed'
|
|
down_revision: Union[str, None] = None
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
op.create_table('call_flows',
|
|
sa.Column('id', sa.String(), nullable=False),
|
|
sa.Column('name', sa.String(), nullable=False),
|
|
sa.Column('phone_number', sa.String(), nullable=False),
|
|
sa.Column('description', sa.Text(), nullable=True),
|
|
sa.Column('steps', sa.JSON(), nullable=False),
|
|
sa.Column('last_verified', sa.DateTime(), nullable=True),
|
|
sa.Column('avg_hold_time', sa.Integer(), nullable=True),
|
|
sa.Column('success_rate', sa.Float(), nullable=True),
|
|
sa.Column('times_used', sa.Integer(), nullable=True),
|
|
sa.Column('last_used', sa.DateTime(), nullable=True),
|
|
sa.Column('notes', sa.Text(), nullable=True),
|
|
sa.Column('tags', sa.JSON(), nullable=True),
|
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index(op.f('ix_call_flows_phone_number'), 'call_flows', ['phone_number'], unique=False)
|
|
op.create_table('call_records',
|
|
sa.Column('id', sa.String(), nullable=False),
|
|
sa.Column('direction', sa.String(), nullable=False),
|
|
sa.Column('remote_number', sa.String(), nullable=False),
|
|
sa.Column('status', sa.String(), nullable=False),
|
|
sa.Column('mode', sa.String(), nullable=False),
|
|
sa.Column('intent', sa.Text(), nullable=True),
|
|
sa.Column('started_at', sa.DateTime(), nullable=True),
|
|
sa.Column('ended_at', sa.DateTime(), nullable=True),
|
|
sa.Column('duration', sa.Integer(), nullable=True),
|
|
sa.Column('hold_time', sa.Integer(), nullable=True),
|
|
sa.Column('device_used', sa.String(), nullable=True),
|
|
sa.Column('recording_path', sa.String(), nullable=True),
|
|
sa.Column('transcript', sa.Text(), nullable=True),
|
|
sa.Column('summary', sa.Text(), nullable=True),
|
|
sa.Column('action_items', sa.JSON(), nullable=True),
|
|
sa.Column('sentiment', sa.String(), nullable=True),
|
|
sa.Column('call_flow_id', sa.String(), nullable=True),
|
|
sa.Column('classification_timeline', sa.JSON(), nullable=True),
|
|
sa.Column('metadata', sa.JSON(), nullable=True),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index(op.f('ix_call_records_remote_number'), 'call_records', ['remote_number'], unique=False)
|
|
op.create_table('devices',
|
|
sa.Column('id', sa.String(), nullable=False),
|
|
sa.Column('name', sa.String(), nullable=False),
|
|
sa.Column('type', sa.String(), nullable=False),
|
|
sa.Column('sip_uri', sa.String(), nullable=True),
|
|
sa.Column('phone_number', sa.String(), nullable=True),
|
|
sa.Column('priority', sa.Integer(), nullable=True),
|
|
sa.Column('is_online', sa.String(), nullable=True),
|
|
sa.Column('capabilities', sa.JSON(), nullable=True),
|
|
sa.Column('dnd', sa.Boolean(), nullable=False),
|
|
sa.Column('last_seen', sa.DateTime(), nullable=True),
|
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_table('recordings',
|
|
sa.Column('id', sa.String(), nullable=False),
|
|
sa.Column('call_id', sa.String(), nullable=False),
|
|
sa.Column('path', sa.String(), nullable=False),
|
|
sa.Column('format', sa.String(), nullable=True),
|
|
sa.Column('duration_s', sa.Float(), nullable=True),
|
|
sa.Column('size_bytes', sa.Integer(), nullable=True),
|
|
sa.Column('channels', sa.Integer(), nullable=True),
|
|
sa.Column('started_at', sa.DateTime(), nullable=True),
|
|
sa.Column('ended_at', sa.DateTime(), nullable=True),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index(op.f('ix_recordings_call_id'), 'recordings', ['call_id'], unique=False)
|
|
op.create_table('routing_rules',
|
|
sa.Column('id', sa.String(), nullable=False),
|
|
sa.Column('name', sa.String(), nullable=False),
|
|
sa.Column('priority', sa.Integer(), nullable=False),
|
|
sa.Column('enabled', sa.Boolean(), nullable=False),
|
|
sa.Column('match', sa.JSON(), nullable=False),
|
|
sa.Column('action', sa.JSON(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_table('transcript_chunks',
|
|
sa.Column('id', sa.String(), nullable=False),
|
|
sa.Column('call_id', sa.String(), nullable=False),
|
|
sa.Column('seq', sa.Integer(), nullable=False),
|
|
sa.Column('t_offset_ms', sa.Integer(), nullable=True),
|
|
sa.Column('speaker', sa.String(), nullable=True),
|
|
sa.Column('text', sa.Text(), nullable=False),
|
|
sa.Column('confidence', sa.Float(), nullable=True),
|
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index(op.f('ix_transcript_chunks_call_id'), 'transcript_chunks', ['call_id'], unique=False)
|
|
# ### end Alembic commands ###
|
|
|
|
|
|
def downgrade() -> None:
|
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
op.drop_index(op.f('ix_transcript_chunks_call_id'), table_name='transcript_chunks')
|
|
op.drop_table('transcript_chunks')
|
|
op.drop_table('routing_rules')
|
|
op.drop_index(op.f('ix_recordings_call_id'), table_name='recordings')
|
|
op.drop_table('recordings')
|
|
op.drop_table('devices')
|
|
op.drop_index(op.f('ix_call_records_remote_number'), table_name='call_records')
|
|
op.drop_table('call_records')
|
|
op.drop_index(op.f('ix_call_flows_phone_number'), table_name='call_flows')
|
|
op.drop_table('call_flows')
|
|
# ### end Alembic commands ###
|